保持GC低开销的窍门有哪些?

随着一再拖延而即将发布的 Java9,G1(“Garbage First”)垃圾回收器将被成为 HotSpot 虚拟机默认的垃圾回收器。从 serial 垃圾回收器到CMS 收集器, JVM 见证了许多 GC 实现,而 G1 将成为其下一代垃圾回收器。

随着垃圾收集器的发展,每一代 GC 与其上一代相比,都带来了巨大的进步和改善。parallel GC 与 serial GC 相比,它让垃圾收集器以多线程的方式工作,充分利用了多核计算机的计算能力。CMS(“Concurrent Mark-Sweep”)收集器与 parallel GC 相比,它将回收过程分成了多个阶段,使得应用线程正在运行的时候,收集工作可以并发地完成,大大改善了频繁执行 “stop-the-world” 的情况。G1 对于拥有大量堆内存的 JVM 表现出更好的性能,并且具有更好的可预测和统一的暂停过程。

Tip #1: 预测集合的容量

所有标准的 Java 集合,包括定制和扩展的实现(比如 Trove 和 Google 的 Guava),底层都使用了数组(原生数据类型或者基于对象的类型)。因为数组一旦被分配,其大小就不可变,因此添加元素到集合时,大多数情况下都会导致需要重新申请一个新的大容量数组替换老的数组(指集合底层实现使用的数组)。

即使没有提供集合初始化的大小,大多数集合的实现都尽量优化重新分配数组的处理并且将其开销平摊到最低。不过,在构造集合的时候就提供大小可以得到最佳的效果。

让我们将下面的代码作为一个简单的例子分析一下:

public static List reverse(List & lt; ? extends T & gt; list) {

    List result = new ArrayList();

    for (int i = list.size() - 1; i & gt; = 0; i--) {
        result.add(list.get(i));
    }

    return result;
}

This method allocates a new array, then fills it up with items from another list, only in reverse order. 这个方法分配了一个新的数组,然后用另一个 list 中元素对该数组进行填充,只是元素的数序发生了变化。

这个处理方式可能会付出惨重的性能代价,其优化的点在添加元素到新的 list 中这行代码。 随着每一次添加元素,list 都需要确保其底层数组拥有足够的位置来容纳新的元素。如果有空闲的位置,那么只是简单地将新元素存储到下一个空闲的槽位。如果没有的话,将分配一个新的底层数组,拷贝旧的数组内容到新的数组中,然后添加新的元素。这将导致多次分配数组,那些剩余的旧数组最终被 GC 所回收。

我们可以通过在构造集合时让其底层的数组知道它将存储多少元素,从而避免这些多余的分配

public static List reverse(List & lt; ? extends T & gt; list) {

    List result = new ArrayList(list.size());

    for (int i = list.size() - 1; i & gt; = 0; i--) {
        result.add(list.get(i));
    }

    return result;

}

上面的代码通过 ArrayList 的构造器指定足够大的空间来存储 list.size() 个元素,在初始化时完成分配的执行,这意味着 List 在迭代的过程中无需再次分配内存。

Guava 的集合类则更进一步,允许初始化集合时明确指定期望元素的个数或者指定一个预测值。

List result = Lists.newArrayListWithCapacity(list.size());
List result = Lists.newArrayListWithExpectedSize(list.size());

上面的代码中,前者用于我们已经准确地知道集合将要存储多少元素,而后者的分配方式考虑了错误预估的情况。

Tip #2:直接处理数据流

当处理数据流时,比如从一个文件读取数据或者从网络中下载数据,下面的代码是非常常见的:

byte[] fileData = readFileToByteArray(new File("myfile.txt"));

所产生的字节数组可能被解析 XML 文档、JSON 对象或者协议缓冲消息,以及一些常见的可选项。

当处理大文件或者文件的大小无法预测时,上面的做法很是不明智的,因为当 JVM 无法分配一个缓冲区来处理真正文件时,就会导致OutOfMemeoryErrors。

即使数据的大小是可管理的,当到垃圾回收时,使用上面的模式依然会造成巨大的开销,因为它在堆中分配了一块非常大的区域来存储文件数据。

一种更加好的处理方式是使用合适的 InputStream (比如在这个例子中使用 FileInputStream)直接传递给解析器,不再一次性将整个文件读取到一个字节数组中。所有主流的开源库都提供相应的 API 来直接接受一个输入流进行处理,比如:

FileInputStream fis = new FileInputStream(fileName);
MyProtoBufMessage msg = MyProtoBufMessage.parseFrom(fis);

Tip #3: 使用不可变的对象

不变性有太多的好处。甚至不用我赘述什么。然而,有一个优点会对垃圾回收产生影响,应该关注一下。

一个不可变对象的属性在对象被创建后就不能被修改(在这里的例子使用的是引用数据类型的属性),比如:

public class ObjectPair {

    private final Object first;
    private final Object second;

    public ObjectPair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst() {
        return first;
    }

    public Object getSecond() {
        return second;
    }

}

将上面的类实例化后会产生一个不可变对象—它的所有属性用 final 修饰,构造完成后就不能改变了。

不可变性意味着所有被一个不可变容器所引用的对象,在容器构造完成前对象就已经被创建。就 GC 而言:这个容器年轻程度至少和其所持有的最年轻的引用一样。这意味着当在年轻代执行垃圾回收的过程中,GC 因为不可变对象处于老年代而跳过它们,直到确定这些不可变对象在老年代中不被任何对象所引用时,才完成对它们的回收。

更少的扫描对象意味着对内存页更少的扫描,越少的扫描内存页就意味着更短的 GC 生命周期,也意味着更短的 GC 暂停和更好的总吞吐量。

Tip #4: 小心字符串拼接

字符串可能是在所有基于 JVM 应用程序中最常用的非原生数据结构。然而,由于其隐式地开销负担和简便的使用,非常容易成为占用大量内存的罪归祸首。

这个问题很明显不在于字符串字面值,而是在运行时分配内存初始化产生的。让我们快速看一下动态构建字符串的例子:

public static String toString(T[] array) {

    String result = "[";

    for (int i = 0; i & lt; array.length; i++) {
        result += (array[i] == array ? "this" : array[i]);
        if (i & lt; array.length - 1) {
            result += ", ";
        }
    }

    result += "]";

    return result;
}

这是个看似不错的方法,接收一个字符数组然后返回一个字符串。但是这对于对象内存分配却是灾难性的。

很难看清这语法糖的背后,但是幕后的实际情况是这样的:

public static String toString(T[] array) {

    String result = "[";

    for (int i = 0; i & lt; array.length; i++) {

        StringBuilder sb1 = new StringBuilder(result);
        sb1.append(array[i] == array ? "this" : array[i]);
        result = sb1.toString();

        if (i & lt; array.length - 1) {
            StringBuilder sb2 = new StringBuilder(result);
            sb2.append(", ");
            result = sb2.toString();
        }
    }

    StringBuilder sb3 = new StringBuilder(result);
    sb3.append("]");
    result = sb3.toString();

    return result;
}

字符串是不可变的,这意味着每发生一次拼接时,它们本身不会被修改,而是依次分配新的字符串。此外,编译器使用了标准的 StringBuilder 类来执行这些拼接操作。这就会有问题了,因为每一次迭代,既隐式地分配了一个临时字符串,又隐式分配了一个临时的 StringBuilder 对象来帮助构建最终的结果。

最佳的方式是避免上面的情况,使用 StringBuilder 和直接的追加,以取代本地拼接操作符(“+”)。下面是一个例子:

public static String toString(T[] array) {

    StringBuilder sb = new StringBuilder("[");

    for (int i = 0; i & lt; array.length; i++) {
        sb.append(array[i] == array ? "this" : array[i]);
        if (i & lt; array.length - 1) {
            sb.append(", ");
        }
    }

    sb.append("]");
    return sb.toString();
}

这里,我们只在方法开始的时候分配了唯一的一个 StringBuilder。至此,所有的字符串和 list 中的元素都被追加到单独的一个StringBuilder中。最终使用 toString() 方法一次性将其转成成字符串返回。

Tip #5: 使用特定的原生类型的集合

Java 标准的集合库简单且支持泛型,允许在使用集合时对类型进行半静态地绑定。比如想要创建一个只存放字符串的 Set 或者存储 Map<Pair, List>这样的 map,这种处理方式是非常棒的。

真正的问题源于当我们想要使用一个 list 存储 int 类型,或者一个 map 存储 double 类型作为 value。因为泛型不支持原生数据类型,因此另外的一种选择是使用包装类型来进行替换,这里我们使用 List 。

这种处理方式是非常浪费的,因为一个 Integer 是一个完全的对象,一个对象的头部占用12个字节以及其内部的所维护的 int 属性,每个Integer 对象总共占用16个字节。这比起存储相同个数的 int 类型的 list 而言,其消耗的空间是它的四倍!比这个更加严重的问题在于,事实上因为 Integer 是真正的对象实例,因此它需要垃圾收集阶段被垃圾收集器所考虑是否要回收。

为了处理这个问题,我们在 Takipi 中使用非常棒的 Trove 集合库。Trove 摒弃了部分泛型的特定来支持特定的使用内存更高效的原生类型的集合。比如,我们使用非常消耗性能的 Map<Integer, Double>,在 Trove 中有另一种特别的选择方案,其形式为 TIntDoubleMap

TIntDoubleMap map = new TIntDoubleHashMap();
map.put(5, 7.0);
map.put(-1, 9.999);
...

Trove 的底层实现使用了原生类型的数组,所以当操作集合的时候不会发生元素的装箱(int->Integer)或者拆箱(Integer->int), 没有存储对象,因为底层使用原生数据类型存储。

最后

随着垃圾收集器持续的改进,以及运行时的优化和 JIT 编译器也变得越来越智能。我们作为开发者将会发现越来越少地考虑如何编写 GC 友好的代码。然而,就目前阶段,不论 G1 如何改进,我们仍然有很多可以做的事来帮 JVM 提升性能。

降低Java垃圾回收开销的5条建议的更多相关文章

  1. Java垃圾回收略略观

    本文主要介绍Java垃圾回收(Garbage Collection),90%干货,文字颇多,需要耐心一点看. [对象判断状态算法] ------引用计数法 在创建对象时,为对象创建一个伴生的引用计数器 ...

  2. Java垃圾回收机制详解

    前言 Java 相比 C/C++ 最显著的特点便是引入了自动垃圾回收 (下文统一用 GC 指代自动垃圾回收),它解决了 C/C++ 最令人头疼的内存管理问题,让程序员专注于程序本身,不用关心内存回收这 ...

  3. 转 Java虚拟机5:Java垃圾回收(GC)机制详解

    转 Java虚拟机5:Java垃圾回收(GC)机制详解 Java虚拟机5:Java垃圾回收(GC)机制详解 哪些内存需要回收? 哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无 ...

  4. Java垃圾回收机制(转)

    原文链接:Java垃圾回收机制 1. 垃圾回收的意义 在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象:而在Java中,当没有对象引用指向原先分配给某个对象的内 ...

  5. Java垃圾回收精粹 — Part4

    Java垃圾回收精粹分4个部分,本篇是第4部分.在第4部分里介绍了G1收集器.其他并发收集器以及垃圾收集监控和调优. Garbage First (G1) 收集器 G1 (-XX:+UseG1GC)收 ...

  6. Java垃圾回收精粹 — Part3

    Java垃圾回收精粹分4个部分,本篇是第3部分.在第3部分里介绍了串行收集器.并行收集器以及并发标记清理收集器(CMS). 串行收集器(Serial Collector) 串行收集器是最简单的收集器, ...

  7. java垃圾回收机制整理

    一.垃圾回收器和finalize() java垃圾回收器只负责回收无用对象占据的内存资源.但是如果你的对象不是通过 new 创建的(所有的new 对象都往堆中开辟资源,在一个地方,方便清理/管理资源) ...

  8. [转载]深入理解Java垃圾回收机制

    深入理解Java垃圾回收机制 2016-07-28 20:07:49 湖冰2019 阅读数 14607更多 分类专栏: JAVA基础   原文:http://www.linuxidc.com/Linu ...

  9. java垃圾回收机制学习总结

    最近学习了一下java垃圾回收机制,将其主要内容大致总结一下: 1.什么是垃圾回收机制 java GC机制(garbage collection,垃圾收集,垃圾回收),是java特有的机制,作为jav ...

随机推荐

  1. Spring Data JPA教程, 第三部分: Custom Queries with Query Methods(翻译)

    在本人的Spring Data JPA教程的第二部分描述了如何用Spring Data JPA创建一个简单的CRUD应用,本博文将描述如何在Spring Data JPA中使用query方法创建自定义 ...

  2. org.json.JSONObject与com.google.gson.Gson

    org.json库为JSON创始人编写的解析JSON的java库,Gson为Google为我们提供的解析JSON格式数据的库. Gson里最重要的对象有2个Gson 和GsonBuilder. Gso ...

  3. GridView官方教程及示例

    Grid View GridView is a ViewGroup that displays items in a two-dimensional, scrollable grid. The gri ...

  4. android截屏:保存一个view的内容为图片并存放到SD卡

    项目中偶尔会用到截屏分享,于是就有了下面这个截屏的方法~ 下面得saveImage()方法就是保存当前Activity对应的屏幕所有内容的截屏保存. private void saveImage() ...

  5. bzoj1913

    这是一道好题,要求每个三点圆覆盖的点数和 我们可以算四边形的贡献,四边形显然分成两种:凸四边形和凹四边形 显然,凹四边形的覆盖只可能是三个点组成三角形包含另一个点,所以贡献是1 凸四边形,其最小圆覆盖 ...

  6. bzoj1266: [AHOI2006]上学路线route

    最短路+最小割 首先如何使最短路变长?就是要每一条最短路都割一条边. 我们求出每个点到点1和点n的距离,就可以知道哪些边在最短路上(一开始没有想到求到0和n的距离,想用floyd,但是n=500,怕超 ...

  7. 对象不支持“attachEvent”属性或方法的解决办法

    有些脚本在IE11下执行会报错误: 对象不支持“attachEvent”属性或方法 解决办法 解决办法:把attachEvent改为addEventListener即可

  8. UVa 1153 Keep the Customer Satisfied 【贪心 优先队列】

    题意:给出n个工作,已知每个工作需要的时间last,以及截止时间end,(必须在截止时间之前完成)问最多能够完成多少个工作 首先预处理,将这n件任务按照截止时间从小到大排序 然后用一个cur记录当前做 ...

  9. eclipse无法与手机连上的解决方案

    在eclipse上开发android应用,有时候会遇到eclipse无法识别手机的问题,就算把数据线拔了又插,插了又拔,哪怕是重启eclipse 甚至是重启电脑,这个问题也依然是解决不了.这时候就非常 ...

  10. poj 1236 Network of Schools

    题目描述:有一些学校连接到一个计算机网络.这些学校之间达成了一个协议:每个学校维护着一个学校列表,它向学校列表中的学校发布软件.注意,如果学校B在学校A的列表中,则A不一定在B的列表中.任务A:计算为 ...