乐趣区

关于java:如何写出高性能代码之优化内存回收GC

导语

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