导语

  同一份逻辑,不同人的实现的代码性能会呈现数量级的差别; 同一份代码,你可能微调几个字符或者某行代码的程序,就会有数倍的性能晋升;同一份代码,也可能在不同处理器上运行也会有几倍的性能差别;十倍程序员 不是只存在于传说中,可能在咱们的四周也亘古未有。十倍体现在程序员的办法面面,而代码性能却是其中最直观的一面。
  本文是《如何写出高性能代码》系列的第三篇,本文将通知你如何写出GC更优的代码,以达到晋升代码性能的目标

优化内存回收

  垃圾回收GC(Garbage Collection)是当初高级编程语言内存回收的次要伎俩,也是高级语言所必备的个性,比方大家所熟知的Java、python、go都是自带GC的,甚至是连C++ 也开始有了GC的影子。GC能够主动清理掉那些不必的垃圾对象,开释内存空间,这个个性对老手程序猿极其敌对,反观没有GC机制的语言,比方C++,程序猿须要本人去治理和开释内存,很容易呈现内存泄露的bug,这也是C++的上手难度远高于很多语言的起因之一。
  GC的呈现升高了编程语言上手的难度,然而适度依赖于GC也会影响你程序的性能。这里就不得不提到一个臭名远扬的词——STW(stop the world) ,它的含意就是利用过程暂停所有的工作,把工夫都让进去让给GC线程去清理垃圾。别小看这个STW,如果工夫过长,会显著影响到用户体验。像我之前从事的广告业务,有钻研表明广告零碎响应工夫越长,广告点击量越低,也就意味着挣到的钱越少。
  GC还有个要害的性能指标——吞吐率(Throughput),它的定义是运行用户代码的工夫占总CPU运行工夫的比例。举个例子,假如吞吐率是60%,意味着有60%的CPU工夫是运行用户代码的,而剩下的40%的CPU工夫是被GC占用。从其定义来看,当然是吞吐率越高越好,那么如何晋升利用的GC吞吐率呢? 这里我总结了三条。

缩小对象数量

  这个很好了解了,产生的垃圾对象越少,须要的GC次数也就越少。那如何能缩小对象的数量?这就不得不回顾下咱们在上一讲巧用数据个性) 中提到的两个个性——可复用性和非必要性,遗记的同学能够再点开下面的链接回顾下。这里再大略讲下这两个个性是如何缩小对象生成的。

可复用性

  可复用性在这里指的是,大多数的对象都是能够被复用的,这些能够被复用的对象就没必要每次都新建进去,节约内存空间了。 处了巧用数据个性) 中的例子,我这里再个Java中曾经被用到的例子,这个还得从一段奇怪的代码说起。

Integer i1 = Integer.valueOf(111);Integer i2 = Integer.valueOf(111);System.out.println(i1 == i2);Integer i3 = Integer.valueOf(222);Integer i4 = Integer.valueOf(222);System.out.println(i3 == i4);

  
  下面这段代码的输入后果会是啥呢?你认为是true+true,实际上是true+false。 What?? Java中222不等于222,难道是有Bug? 其实这是老手在比拟数值大小时常犯的一个谬误,包装类型间的相等判断应该用equals而不是'==’,'==’只会判断这两个对象是否是同一个对象,而不是对象中包的具体值是否相等。
 
  像1、2、3、4……等一批数字,在任何场景下都是十分罕用的,如果每次应用都新建个对象很是节约,Java的开发者也思考到了这点,所以在Jdk中提取缓存了一批整数的对象(-128到127),这些数字每次都能够间接拿过去用,而不是新建一个对象进去。而在-128到127范畴外的数字,每次都会是新对象,上面是Integer.valueOf()的源码及正文:

/**     * Returns an {@code Integer} instance representing the specified     * {@code int} value.  If a new {@code Integer} instance is not     * required, this method should generally be used in preference to     * the constructor {@link #Integer(int)}, as this method is likely     * to yield significantly better space and time performance by     * caching frequently requested values.     *     * This method will always cache values in the range -128 to 127,     * inclusive, and may cache other values outside of this range.     *      * @param  i an {@code int} value.     * @return an {@code Integer} instance representing {@code i}.     * @since  1.5     */    public static Integer valueOf(int i) {        if (i >= IntegerCache.low && i <= IntegerCache.high)            return IntegerCache.cache[i + (-IntegerCache.low)];        return new Integer(i);    }

  我在Idea中通过Debug看到了i1-i4几个对象,其实111的两个对象的确是同一个,而222的两个对象的确不同,这就解释了下面代码中的诡异景象。

非必要性

  非必要性的意思是有些对象可能没必要生成。这里我举个例子,可能相似上面这种代码,在业务零碎中会很常见。

    private List<UserInfo> getUserInfos(List<String> ids) {        List<UserInfo> res = new ArrayList<>(ids.size());        if (ids == null || res.size() == 0) {            return new Collections.emptyList();        }        List<UserInfo> validUsers = ids.stream()                .filter(id -> isValid(id))                .map(id -> getUserInfos(id))                .filter(Objects::nonNull)                .collect(Collectors.toList());        res.addAll(validUsers);        return res;    }

  下面代码非常简单,就是通过一批用户Id去获取进去残缺的用户信息,获取前要对入参做校验,之后还会对id做合法性校验。 下面代码的问题是 res对象初始化太早了,如果一个UserInfo没查到,res对象就白初始化了。另外,最初间接返回validUsers是不是就行了,没必要再装到res中,这里res就具备了非必要性。
  像上述这种状况,可能在很多业务零碎里随处可见(但不肯定这么直观),提前初始化一些之后没用的对象,除了节约内存和CPU之外,也会给GC减少累赘。

放大对象体积

  放大体积对象也很好了解,如果对象在单位工夫内生成的对象数量固定,但体积减小后,同样大小的内存就能装载更多的对象,更晚才触发GC,GC的频次就会升高,频次低了天然对性能的影响就会变小。
  对于缩小对象体积,这里我给大家举荐一个jar包——eclipse-collections,其中提供了好多原始类型的汇合,比方IntMap、LongMap…… 应用原始类型(int,long,double……)而不是封装类型(Integer,Long,Double……),在一些数值偏多的业务中很有劣势,如下图是我比照了HashSet<Integer>和eclipse-collections中IntSet在不同数据量下的内存占用比照,IntSet的内存占用只有HashSet<Integer>的四分之一。

  另外,咱在写业务代码的时候,写一些DO、BO、DTO的时候没必要的字段就别加进去了。查数据库的时候,不必的字段也就别查出来了。我之前看到过很多业务代码,查数据库的时候把整行都查出来了,比方我要查一个用户的年龄,后果把他的姓名、地址、生日、电话号码…… 全查出来,这些信息放在Java外面须要一个个的对象去存储的,没有用到局部字段首先就是白取了,其实存它还节约内存空间。

缩短对象存活工夫

  为什么缩小对象的存活工夫就能晋升GC的性能?总的垃圾对象并没有缩小啊! 是的 没错,单纯缩短对象的存活工夫并不会缩小垃圾对象的数量,而是会缩小GC的次数。要了解这个就得先晓得GC的触发机制,像Java中当堆空间使用率超过某个阈值后就会触发GC,如果能缩短对象的工夫,那每次GC就能释放出来更多的空间,下次GC也就会来的更迟一些,总体上GC次数就会缩小。
  这里我举个我本人经验的实在案例,咱们之前零碎有个接口,仅仅是调整了两行代码的程序,这个接口的性能就晋升了40%,这个整个服务的CPU使用率升高了10%+,而这两行程序的改变,缩短了大部分对象的生命周期,所以导致了性能晋升。

    private List<Object> filterTest() {        List<Object> list = getSomeList();        List<Object> res = list                .stream()                .filter(x -> filter1(x))  // filter1须要调用内部接口做过滤判断,性能低且过滤比例很少                .filter(x -> filter2(x))                  .filter(x -> filter3(x))  // filter3 本地数值校验,不依赖内部,效率高且过滤比例高                .collect(Collectors.toList());    }

  下面代码中,filter1性能很低但过滤比低,filter3恰恰相反,往往没被filter1过滤的会被filter3过滤,做了很多无用功。这里只须要将filter1和filter3调换下地位,除了缩小无用功之外,List中的大部分对象生命周期也会缩短。
  其实有个比拟好的编程习惯,也能够缩小对象的存活工夫。其实在本系列的第篇中我也大略提到过,那就是放大变量的作用域。能用局部变量就用局部变量,能放if或者for外面就放外面,因为编程语言作用域实现就是用的栈,作用域越小就越快出栈,其中应用到的对象就越快被判断为死对象。


  除了上述三种优化GC的形式话,其实还有种骚操作,然而我自己不举荐应用,那就是——堆外内存

堆外内存

  在Java中,只有堆内内存才会受GC收集器治理,所以你要不被GC影响性能,最间接的形式就是应用堆外内存,Java中也提供了堆外内存应用的API。然而,堆外内存也是把双刃剑,你要用就得做好欠缺的治理措施,否则内存泄露导致OOM就GG了,所以不举荐间接应用。然而,凡事总有然而,有一些优良开源代码,比方缓存框架ehcache)就能够让你平安的享受到堆外内存的益处,具体应用形式能够查阅官网,这里不再赘述。


  好了,明天的分享就到这里了,看完你可能会发现明天的内容和上一讲 (二)巧用数据个性有一些反复的内容,没错,我了解性能优化底层都是同一套方法论,很多新办法只是以不同的视角在不同畛域所衍生进去的。最初感激下大家的反对,心愿你看完文章有所播种。另外有趣味的话也能够关注下本系列的前两篇文章。

如何写出高性能代码系列文章

  • (一)善用算法和数据结构
  • (二)巧用数据个性
  • (三)优化内存回收(GC)