JVM之垃圾收集器 (GC) 与内存分配策略

1.为什么要学习GC?

GC (Garbage Collection)早于java出现,60年代出现的Lisp中最早使用了GC。

当需要排查各种内存溢出、内存漏斗问题时,当垃圾回收成为系统达到更高并发量的瓶颈时,就需要用到gc了。

总之,写出高性能的Java程序需要懂GC。

2.GC在JVM的体系结构中的位置

HotSpot JVM体系结构。

和应用性能相关的部分用紫色标出,调优从它们着手!

3.什么是性能?

在对Java应用程序进行调优时,主要关注两点:响应速度和吞吐量。

3.1响应速度

响应速度是应用或系统返回指定数据的速度。举例:

  • 桌面UI相应速度。
  • 网站返回网页速度。
  • 数据库查询速度。

3.2吞吐量

吞吐量关注一段时间内应用完成的工作量。举例:

  • 给定时间段内事务数。
  • 一小时内批量处理的作业量。
  • 一小时内数据库查询量。

4.自动垃圾回收

简而言之,自动垃圾回收的过程就是到堆内存中查看哪个对象还被使用,哪些不被使用了,并删掉不被使用的对象。

被使用的对象在程序某处有引用指向它,不被使用的对象不再被引用。在C中,分配和回收内存是手动的。在Java中垃圾回收由GC完成。

4.1 垃圾回收的基本过程

第一步:标记

标记被引用和不再被引用的对象。

如果系统中的所有对象都被扫描,这个过程会很费时。(伏笔)

第二步 (一种选择):正常清除。

删除所有不被引用的对象,保留被引用对象,并将指针指向空闲空间。

内存分配器持有指向空闲空间的指针。

第二步 (另一种选择) : 清除并整理。

为了提高性能,在删除不被引对象之后,我们将所有剩下的被引对象整理在一起,这样空闲空间也能成为连续的大块。这样让新的内存分配更简洁更快速。

4.2大部分对象都是短命的

经过大量的分析,大部分的对象是短命的。如下图所示,Y轴表示存活对象的字节数,X轴表示GC随着时间已分配的字节数。

从图中可看成,大部分对象存活的时间很短。

4.3JVM的分代

上面的伏笔讲到,扫描SVM中所有对象,对其进行标记整理效率很低。对象数量增加的话,GC的耗时也会增加。

在4.2中我们讲到大部分对象都是短命的。所以我们可以将JVM分代回收以提高性能。将堆分成几个部分:新生代、老年代和永久代。

4.3.1新生代

所有新对象被分配到这里。当新生代被占满,会出发minor garbage collection (简称minor GC),回收一个充满“死”对象的新生代会很快。部分幸存对象变老并在年龄达到一定阈值后放到老年代。

Stop the World Event:所有minor GC都是Stop the World Event。就是说会停掉所有的线程直到GC完成。

4.3.2老年代

老年代存放长期存活对象。新生代的对象年龄超过一定阈值时,会进入老年代。对老年代回收叫做major garbage collection (major GC)。

major GC也是Stop the World Event。一般来说major GC比minor GC要慢很多,因为会涉及到所有存活的对象。所以对于响应速度要求高的应用,应该尽可能避免major GC。

另外,major GC的时长也取决于GC使用何种回收策略进行老年代回收。

4.3.3永久代

永久代存放描述类和方法的元数据。

5. 对象已死吗?

如何判断哪些对象还“存活”?哪些已经“死去”呢?一般用引用计数算法和可达性分析算法。

5.1 引用计数算法

是什么?给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

这种算法实现简单,判定效率高。Python就是用了这种算法管理内存。但是主流Java虚拟机里面没有选用引用计数算法来管理内存。主要原因是因为它很难解决对象之间的相互循环引用的问题。

 1 /**
 2  * testGC()方法执行后,objA和objB会不会被GC呢? 
 3  * @author zzm
 4  */
 5 public class ReferenceCountingGC {
 6     public Object instance = null;
 7     private static final int _1MB = 1024 * 1024;
 8     /**
 9      * 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过
10      */
11     private byte[] bigSize = new byte[2 * _1MB];
12     public static void testGC() {
13         ReferenceCountingGC objA = new ReferenceCountingGC();
14         ReferenceCountingGC objB = new ReferenceCountingGC();
15         objA.instance = objB;
16         objB.instance = objA;
17         objA = null;
18         objB = null;
19         // 假设在这行发生GC,objA和objB是否能被回收?
20         System.gc();
21     }
22 }

5.2 可达性分析算法

这个算法的基本思想就是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时 (用图论中的话来说,就是GC Root到这个对象不可达),则证明此对象是不可用的。如下图。

Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈 (栈帧中的本地变量表) 中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI (即一般说的Native方法) 引用的对象。 

6. GC过程

上面阐述了堆为何要分成不同的回收代,下面看不同回收代之间如何协作已完成GC过程。以下图片讲述对象分配和在回收代之间的流转。

第一步,所有新对象都分配在eden空间,两个surviror空间刚开始为空。如下图。

第二步,eden空间被占满后,触发minor GC。如下图。

第三步,被引用对象被移到S0空间,未被引用对象在清除eden时被删除。如下图。

第四步,在下一次minor GC时,eden操作与第三步相同,即未被引用对象被删除,被引对象移到survivor空间,但是是移到S1。

另外,在S0空间中在上次minor GC存活的对象年龄增长并移到S1。当所有的存活对象移到S1后,将S0和eden清除。如下图。

第五步,在接下来的minor GC中,重复和第四步同样的过程,只不过survivor空间互换。将存活对象移到S0,存活对象年龄增长,Eden和S1被清除。如下图。

第六步,在每次minor GC时对象年龄都增长,当对象年龄超过某个阈值时 (假设为8) ,那么这些对象从新生代移到老年代。如下图。

第七步,随着minor GC不断发生,不断有对象从新生代移到老年代。如下图。

第八步,从上面看出,GC的大部分过程是在新生代进行的。直到触发major GC,老年代被清除并整理。如下图。

7. 垃圾收集器

7.1Serial 收集器

Serial GC一般不用于服务器端。但是虚拟机运行在Client模式下的默认新生代收集器,因为它简单高效 (与其他收集器的单线程相比)。

7.2ParNew 收集器

7.3 Parallel Scavenge收集器

7.4 Serial Old收集器

7.5 Parallel Old收集器

7.6 CMS 收集器

7.7 G1收集器

8. 内存分配和回收策略

8.1 对象优先在Eden分配

private static final int _1MB = 1024 * 1024;
/**
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
  */
public static void testAllocation() {
     byte[] allocation1, allocation2, allocation3, allocation4;
     allocation1 = new byte[2 * _1MB];
     allocation2 = new byte[2 * _1MB];
     allocation3 = new byte[2 * _1MB];
     allocation4 = new byte[4 * _1MB];  // 出现一次Minor GC
 }

8.2 大对象直接进入老年代

private static final int _1MB = 1024 * 1024;
/**
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 * -XX:PretenureSizeThreshold=3145728
 */
public static void testPretenureSizeThreshold() {
    byte[] allocation;
    allocation = new byte[4 * _1MB];  //直接分配在老年代中
}

8.3长期存活的对象将进入老年代

8.4动态对象年龄判定

8.5空间分配担保

9. 总结

本文介绍垃圾收集作用、过程、垃圾收集算法、垃圾收集器和各种原理。GC在很多时候是影响系统性能、并发能力的主要因素之一。总之,想要写出高性能JAVA程序,懂GC是必要的!

 

参考资料:

深入理解JVM 周志明

http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

http://www.cubrid.org/blog/dev-platform/understanding-java-garbage-collection/

 

posted on 2016-03-16 10:01  lima  阅读(260)  评论(0编辑  收藏  举报

导航