java removeAll和重写equals、hashcode引起的性能问题
问题背景:
上周发现了一个spark job的执行时间从原来的10-15分钟延迟到了7个小时!wtf,这是出了什么事引起了这么大的性能问题!!
立马查看job的运行日志,发现多次运行都是在某一个固定的stage速度特别慢,大概在5000-6000s,这样的stage一共有3-4次。究竟是什么样的原因引起这样的问题,第一个想法是寻找之前执行时间短的任务和现在执行时间长的任务有哪些不同的地方:1,检查spark提交的参数,包括executor个数,内存配置和核数配置,发现前后都没有改动;2,检查git代码仓库master的代码变更,发现前后有3次提交。现在我把问题的最大可能性放在这些代码的改动上。
问题排查:
查看代码改动,首先想到的就是diff两个版本的代码:发布master在git上都会留下tag,在发布系统jenkins上找到两个发布的release tag,diff之。
从diff结果看,这几次提交主要是添加了新功能,添加新的工具类,java bean的重构(抽取公共的属性作为父类属性),和我们的job逻辑相关的代码基本没有改动,从代码逻辑上并没有看出什么大的性能问题。
排查陷入困境。。。
受到同事启发,决定把改动之前的tag复制出一个新的branch(我们称之为before),把改动之后的tag复制一个新的branch(称之为after),把before和after之间的差异分批次加到before上,执行before,看加那些代码时会出现问题(实在差不出了问题所在,只能选择笨方法,一点一点试)。分批次加到before上,在Intelij idea上有个简单方法,分支切换到before,右键项目->Git->Compare with branch,选择after分支,diff两者区别,这时候diff页面上两分支不同的代码旁边会有>>的箭头形状,可以快速把要添加的代码加到before分支。
先把最可能影响我们job执行的代码加到了before分支,执行都没有问题。。。
继续把其他的代码分批加到before分支,(这是个体力活,添加代码-run job-添加代码-run job)
分了5-6次之后,发现加了三个java bean的重构后job变慢,???改了bean能导致job性能变差,有点怀疑人生
检查跟job相关的bean,除了抽取了公共属性为父类之外,重写了hashcode和equals方法!!!会不会是这个引起的
- @Override
- public boolean equals(Object obj) {
- if(Objects.nonNull(obj) && obj instanceof XXX){
- return Objects.equals(hashCode(),obj.hashCode());
- }
- return false;
- }
- @Override
- public int hashCode() {
- String value = x1 +
- x2 +
- x3 +
- x4 +
- x5 +
- x6 +
- x7 +
- x8 +
- x9;
- return StringUtils.trim(value).hashCode();
- }
代码如上,类名这里修改为XXX,字段修改为x1,x2,x3,x4,x5,x6,x7,x8,x9
写一个test,循环100w次 执行equals,发现也是秒秒钟跑完!!!
继续回到性能差的那块逻辑里排查,找可能会用到model的equals方法的地方
有重大发现,对于修改了equals方法的model,有一个removeAll操作:从一个List<XXX> source中安条件filter出一些实例,作为list1,又list2 = source.removeAll(list1);作为list2,这里的source大概在几十万到100万的数据量,list1里几乎是source的全量(此次聚合对应的分组区分度不高,所以在每一个执行器上数据量较大)
而removeAll时会调用model的equals方法,时间复杂度为m*n(n为source的数量,m为list1的数量),在千亿-万亿的equals操作下,任何耗时的操作都会成千亿-万亿倍增加,所以会出现没有修改任何逻辑,只重写了equals方法就会出现性能问题。
贴一下ArrayList的源码:
- /**
- * Removes from this list all of its elements that are contained in the
- * specified collection.
- *
- * @param c collection containing elements to be removed from this list
- * @return {@code true} if this list changed as a result of the call
- * @throws ClassCastException if the class of an element of this list
- * is incompatible with the specified collection
- * (<a href="Collection.html#optional-restrictions">optional</a>)
- * @throws NullPointerException if this list contains a null element and the
- * specified collection does not permit null elements
- * (<a href="Collection.html#optional-restrictions">optional</a>),
- * or if the specified collection is null
- * @see Collection#contains(Object)
- */
- public boolean removeAll(Collection<?> c) {
- Objects.requireNonNull(c);
- return batchRemove(c, false);
- }
- private boolean batchRemove(Collection<?> c, boolean complement) {
- final Object[] elementData = this.elementData;
- int r = 0, w = 0;
- boolean modified = false;
- try {
- for (; r < size; r++)
- if (c.contains(elementData[r]) == complement)
- elementData[w++] = elementData[r];
- } finally {
- // Preserve behavioral compatibility with AbstractCollection,
- // even if c.contains() throws.
- if (r != size) {
- System.arraycopy(elementData, r,
- elementData, w,
- size - r);
- w += size - r;
- }
- if (w != size) {
- // clear to let GC do its work
- for (int i = w; i < size; i++)
- elementData[i] = null;
- modCount += size - w;
- size = w;
- modified = true;
- }
- }
- return modified;
- }
可以看出,先按照source的size做循环,循环内判断contains,
- for (; r < size; r++)
- if (c.contains(elementData[r]) == complement)
- elementData[w++] = elementData[r];
我们再看一下ArrayList(被remove的也是ArrayList类型)的contains
- /**
- * Returns <tt>true</tt> if this list contains the specified element.
- * More formally, returns <tt>true</tt> if and only if this list contains
- * at least one element <tt>e</tt> such that
- * <tt>(o==null ? e==null : o.equals(e))</tt>.
- *
- * @param o element whose presence in this list is to be tested
- * @return <tt>true</tt> if this list contains the specified element
- */
- public boolean contains(Object o) {
- return indexOf(o) >= 0;
- }
- /**
- * Returns the index of the first occurrence of the specified element
- * in this list, or -1 if this list does not contain the element.
- * More formally, returns the lowest index <tt>i</tt> such that
- * <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>,
- * or -1 if there is no such index.
- */
- public int indexOf(Object o) {
- if (o == null) {
- for (int i = 0; i < size; i++)
- if (elementData[i]==null)
- return i;
- } else {
- for (int i = 0; i < size; i++)
- if (o.equals(elementData[i]))
- return i;
- }
- return -1;
- }
indexOf内部又一层循环,时间复杂度为m*n
虽然问题是equals由 == 操作变为9个字段拼接做hashcode 这个变更引起的,但核心问题还在removeAll,before没有出现问题只是因为==操作快,大概产生2-3分钟的执行时间并没有引起问题和关注。重写equals会增加一些时间,在极大的基数上就产生了性能问题
问题解决:
去掉removeAll,用两个filter代替(满足业务逻辑为准)
效果:
job执行7-8分钟,比before版本还快2-3分钟,因为去掉了千亿-万亿次 equals(虽然==很快)
经过两天多的排查,终于解决掉了问题。这个事情让我重新对List 的removeAll有了新认识,也认识到一个道理,对于你认为简单的东西 才是最容易挖坑的地方
java removeAll和重写equals、hashcode引起的性能问题的更多相关文章
- java构造方法和重写equals
Cell的构造函数 package Test; import java.util.Objects; public class Cell { int a; int b; public int getA( ...
- java中为什么重写equals时必须重写hashCode方法?
在上一篇博文Java中equals和==的区别中介绍了Object类的equals方法,并且也介绍了我们可在重写equals方法,本章我们来说一下为什么重写equals方法的时候也要重写hashCod ...
- 【原创】关于java对象需要重写equals方法,hashcode方法,toString方法 ,compareto()方法的说明
在项目开发中,我们都有这样的经历,就是在新增表时,会相应的增加java类,在java类中都存在常见的几个方法,包括:equals(),hashcode(),toString() ,compareto( ...
- java 中为什么重写 equals 后需要重写 hashCode
本文为博主原创,未经允许不得转载: 1. equals 和 hashCode 方法之间的关系 这两个方法都是 Object 的方法,意味着 若一个对象在没有重写 这两个方法时,都会默认采用 Objec ...
- 【Java基础】重写equals需要重写hashcode
Object里的equals用来比较两个对象的相等性,一般情况下,当重写这个方法时,通常有必要也重写hashcode,以维护hashcode方法的常规协定,或者说这是JDK的规范,该协定声明相等对象必 ...
- RemoveAll 要重写equals方法
public class User { private String name; private int age; //setter and getter public String getName( ...
- Java 基础 - 如何重写equals()
ref:https://www.cnblogs.com/TinyWalker/p/4834685.html -------------------- 编写equals方法的建议: 显示参数命名为oth ...
- 重写Euqals & HashCode
package com.test.collection; import java.util.HashMap; import java.util.Map; /** * 重写equals & ha ...
- 为什么要重写equals和hashcode方法
equals hashcode 当新建一个java类时,需要重写equals和hashcode方法,大家都知道!但是,为什么要重写呢? 需要保证对象调用equals方法为true时,hashcode ...
随机推荐
- javaScript中两个等于号和三个等于号之间的区别
一言以蔽之:==先转换类型再比较,===先判断类型,如果不是同一类型直接为false. ===表示恒等于,比较的两边要绝对的相同 alert(0 == ""); // trueal ...
- 6.azkban的监控
azkaban自带的监控flow自带的邮件功能SLA总结写程序监控job情况监控azkaban的元数据库使用azkaban API监控总结 azkaban自带的监控 azkban目前仅仅支持邮件监控, ...
- Python中的Numeric
整型Integer 在Python2.X中,Integer有两种类型,一种是32bit的普通类型,一种是精度无限制的long类型,在数字后面标识l或者L来标识long类型,并且,当32bit发生ove ...
- c语言乐曲演奏——《千本樱》
这个程序着实花费了我好长的时间,我本身对音乐一窍不通,先是跟着girl friend学习了简谱,根据c调44拍的<千本樱>写下了下面的程序. #include<stdio.h> ...
- 软件工程 part4 评价3作品
作品1 抢答器 地址: https://modao.cc/app/ylGTXobcMU7ePNi6tY53gG4iraLl0md评价: 挺好玩,但是字体大小是个缺陷,简单大方. 作品2:连连看 软件工 ...
- centos7 下pycharm无法输入中文问题解决方案
作者使用的pycharm是2017.2 在pycharm.sh脚本的如下行(大约在201行): # -------------------------------------------------- ...
- html 怎么去掉网页的滚动条
<style type="text/css"> body{ overflow:scroll; overflow-x:hidden; } </style> 这 ...
- asp.net 间隔一段时间执行某方法
设想网站后台每秒自动更新一下Cache["test"]中的值,通过这个实现就可以完成一些在间隔多少时间更新一下数据库的操作. 1.定义一个事件类BMAEvent,在Processo ...
- BZOJ 1095 捉迷藏(线段树维护括号序列)
对于树的一个括号序列,树上两点的距离就是在括号序列中两点之间的括号匹配完之后的括号数... 由此可以得出线段树的做法.. #include<cstdio> #include<iost ...
- [洛谷P5173]传球
题目大意:有$n(n\leqslant3500)$个人坐成一个环,$0$号手上有个球,每秒钟可以向左或向右传球,问$m$秒后球在$0$号手上的方案数. 题解:一个$O(nm)$的$DP$,$f_{i, ...