当前位置: 首页 > 后端技术 > Java

如何用优化内存回收(GC)编写高性能代码

时间:2023-04-01 23:48:31 Java

引言同一个逻辑,不同人实现的代码性能会相差几个数量级;同样的代码,微调几个字符或某行代码的顺序,会有数倍的性能提升;同样的代码在不同的处理器上运行也可能有数倍的性能差异;十倍程序员不仅存在于传说中,可能在我们身边也比比皆是。十倍体现在程序员方法的方方面面,代码性能是最直观的方面。本文是《如何写出高性能代码》系列的第三篇。本文将告诉你如何编写GC优化的代码,以达到提高代码性能和优化内存回收的目的。该方法也是高级语言的一个基本特征。比如大家熟知的Java、python、go都有自己的GC,甚至C++也开始有了GC的影子。GC可以自动清理不用的垃圾对象,释放内存空间。这个特性对新手程序员极其友好。对比没有GC机制的语言,比如C++,需要程序员自己管理和释放内存,容易出现内存泄漏。bug,这是为什么C++比许多语言更难学习的原因之一。GC的出现降低了编程语言的入门难度,但是过度依赖GC也会影响你程序的性能。这里不得不提一个臭名昭著的词——STW(stoptheworld),意思是应用进程暂停所有工作,把所有时间都让给GC线程清理垃圾。别小看这个STW,如果时间过长,显然会影响用户体验。像我之前从事的广告业务,有研究表明,广告系统的响应时间越长,广告点击次数越少,也就意味着赚的钱越少。GC还有一个关键的性能指标——吞吐量(Throughput),它的定义是运行用户代码所花费的时间与CPU总运行时间的比值。例如,假设吞吐率为60%,则意味着60%的CPU时间在运行用户代码,剩下的40%的CPU时间被GC占用。从它的定义来看,吞吐率当然是越高越好,那么如何提高应用的GC吞吐率呢?这里我总结了三点。减少对象数量这个很好理解。生成的垃圾对象越少,需要的GC次数就越少。那么如何减少对象的数量呢?这就不得不回顾一下我们在上一讲巧妙使用数据特征时提到的两个特性——可复用性和非必要性。忘记的同学可以点击上面的链接进行复习。让我们谈谈这两个特性如何减少对象生成。Reusability这里的可重用性是指大部分对象都可以被重用,这些可重用的对象不需要每次都创建,浪费内存空间。这里有一个已经在Java中使用过的例子,这得从一段奇怪的代码说起。整数i1=Integer.valueOf(111);整数i2=Integer.valueOf(111);System.out.println(i1==i2);整数i3=Integer.valueOf(222);整数i4=Integer.valueOf(222);System.out.println(i3==i4);上面代码的输出是什么?你以为是真+真,其实是真+假。什么??在Java中,222不等于222。是否有错误?其实这是新手在比较数值时常犯的错误。你应该使用equals而不是'=='来判断包类型之间的相等性。'=='只会判断这两个对象是否是同一个对象,而不会判断对象中包的具体值是否相等。 1,2,3,4...这样的一批数字在任何场景中都是非常常用的。每次使用时都创建一个新对象会很浪费。Java开发人员也考虑到了这一点。因此提取了一批整型对象(-128到127)缓存在Jdk中。这些数字每次都可以直接使用,而不用创建一个新的对象。而在-128到127范围外的数字,每次都会是新对象,下面是Integer.valueOf()的源码及注解:/***Returnsan{@codeInteger}instancerepresentingthespecified*{@codeint}价值。如果一个新的{@codeInteger}实例不是*必需的,这个方法通常应该优先于*构造函数{@link#Integer(int)}使用,因为这个方法很可能*产生更好的空间和时间性能通过*缓存经常请求的值。**此方法将始终缓存-128到127范围内的值,*包括在内,并可能缓存此范围之外的其他值。**@parami一个{@codeint}值。*@return一个代表{@codei}的{@codeInteger}实例。*@since1.5*/publicstaticIntegervalueOf(inti){if(i>=IntegerCache.low&&i<=IntegerCache.high)returnIntegerCache.cache[i+(-IntegerCache.low)];返回新整数(i);}我在Idea中通过Debug看说到i1-i4这两个对象,111的两个对象确实是一样的,而222的两个对象确实是不同的,这就解释了上面代码中的怪异现象。Non-necessityNon-necessity是指某些对象可能不需要生成。这里我举个例子,可能类似于下面的代码,在业务系统中很常见。privateListgetUserInfos(Listids){Listres=newArrayList<>(ids.size());if(ids==null||res.size()==0){returnnewnewArrayList<>();}ListvalidUsers=ids.stream().filter(id->isValid(id)).map(id->getUserInfos(id)).filter(Objects::nonNull).collect(Collectors.toList());res.addAll(有效用户);返回资源;}上面的代码很简单,就是通过一批用户id获取完整的用户信息,并在获取前检查入参,然后检查id的合法性。上面代码的问题是res对象初始化的太早了。如果没有找到UserInfo,res对象将被初始化。另外最后直接返回validUsers就可以了,没必要安装到res里面,这里res就不用了。像上面这样的情况在很多业务系统中可能随处可见(但不一定那么直观)。初始化一些后面不会用到的对象,不仅会浪费内存和CPU,还会增加GC的负担。减小对象的尺寸减小对象的尺寸也很容易理解。如果一个对象在单位时间内产生的对象数量是固定的,但是减小尺寸之后,同样大小的内存可以加载更多的对象,GC触发的晚一些,GC的频率就会降低,频率越低,对性能的自然影响就越小。关于减小对象的大小,这里推荐一个jar包——eclipse-collections,里面提供了很多原始类型的集合,比如IntMap,LongMap……用原始类型(int,long,double……)代替封装类型(Integer,Long,Double...),在一些取值比较多的业务中非常有优势。下图是HashSet中IntSet和eclipse-collections在不同数据量下的内存占用对比。IntSet的内存使用量仅为HashSet的四分之一。另外,我们在写业务代码的时候,不要在写一些DO、BO、DTO的时候加入不必要的字段。检查数据库时,不要检查未使用的字段。之前看过很多业务代码。当我检查数据库时,我会检查整行。例如,我想查看用户的年龄,但我查出了他的姓名、地址、生日、电话号码……所有这些。在Java中,需要一个一个地存储对象。如果您不使用某些字段,您将白白获得它们。事实上,存储它们是一种内存空间的浪费。缩短对象的生存时间?为什么减少对象的生存时间可以提高GC的性能?垃圾对象总数并没有减少!是的,没错,单纯的缩短对象的生存时间并不会减少垃圾对象的数量,反而会减少GC的次数。要理解这一点,首先要知道GC的触发机制。比如在Java中,当堆空间使用量超过某个阈值时,就会触发GC。如果能缩短对象时间,每次GC就可以释放更多的空间。下一次GC会来的更晚,整体GC的次数会减少。这里我举一个我亲身经历的真实案例。我们以前的系统有一个界面。仅仅通过调整两行代码的顺序,这个接口的性能就提升了40%,整个服务的CPU占用率降低了10%+,而且这两行代码顺序的变化缩短了大多数对象的生命周期,从而提高性能。privateListfilterTest(){Listlist=getSomeList();Listres=list.stream().filter(x->filter1(x))//filter1需要调用外部接口进行过滤判断,性能低,过滤率低.filter(x->filter2(x)).filter(x->filter3(x))//filter3本地数值验证,独立于外部,效率高,过滤比高。收集(收集器。toList());}上述代码中,filter1性能低但过滤比低,filter3正好相反。往往filter1没有过滤掉的,filter3会过滤掉,做很多无用功。这里只需要更换filter1和filter3即可。除了减少无用功之外,List中大部分对象的生命周期也会缩短。其实还有一个更好的编程习惯,也可以减少对象的存活时间。其实我大概在这个系列的第一篇文章里提到过,就是缩小变量的作用域。能用局部变量就用局部变量,能放到if或者for里面就可以放到里面,因为编程语言作用域的实现是用栈,作用域越小,更快地弹出堆栈,并且更快地判断其中使用的对象是否为死对象。除了上面三种优化GC的方式外,其实还有一种trick操作,不过我个人不推荐使用,那就是-off-heapmemory和off-heapmemory在Java中,只有堆中的内存将由GC收集器管理。所以如果不想被GC影响,最直接的方法就是使用堆外内存。Java还提供了使用堆外内存的API。然而,堆外内存也是一把双刃剑。如果要使用它,必须采取良好的管理措施。否则内存泄漏会导致OOM和GG,所以不建议直接使用。然而,凡事总有but。有一些优秀的开源代码,比如缓存框架ehcache)可以让你安心享受堆外内存带来的好处。具体使用方法可以参考官网,这里不再赘述。好了,今天的分享到此结束。看完之后你可能会发现今天的内容和上一讲有一些重复的内容(二)数据特征的巧妙运用。是的,我明白性能优化的底层是同一个方法论,很多新方法只是从不同领域的不同角度衍生出来的。最后感谢大家的支持,希望大家看完文章有所收获。另外,如果你有兴趣,也可以关注本系列的前两篇文章。如何写出高性能代码系列文章(一)善用算法和数据结构(二)巧妙利用数据特征(三)优化内存回收(GC)