深入Java虚拟机笔记

GC

GC需要处理的是:哪些对象没用了需要清理、什么时候回收、如果清理

哪些对象需要清理?

判断一个对象是否需要回收有两种方式:引用计数方式、可达性分析算法

引用计数方式

对对象的引用进行计数,计数为0则是需要清理的对象,>=1则是需要持有的。无法处理的问题是在循环引用的时候是无法处理。这只是一个描述GC理论说法,其实没有真正的JVM是中这种算法实现的。

可达性分析算法

用堆上对象或者方法区的常量作为「根对象」,从「根对象」出发所有能到达的对象都是认为是活的,没有到达的都是需要被清理的。
根对象包括:

  • 虚拟机栈中引用的对象。
  • 本地方法栈中JNI引用的对象。
  • 方法区中类静态属性实体引用的对象。
  • 方法区中常量引用的对象。
  • 活着的线程。
  • Monitor Used:同步用的监控器。
  • Held by JVM: JVM自己持有的对象,比如系统类加载器,一些异常等。

为什么选择这些对象,是如何选择的?
可以想象,程序的执行的对象是一个对象树,当一个方法被执行,其实是执行了一段逻辑,这段逻辑以内存上的对象树为目标,进行一定的操作。这样想,就可以理解到,运行程序的时候,程序必须首先拿到这个对象树的若干个root,然后再从这个root出发,运行程序逻辑,然后按照程序逻辑的需要去访问树上的不同节点。那么这些root就是可达性分析需要的GC root。
如果上面的思路是正确的,那么寻找GC root应该是这么一个思路:一个方法的运行时可访问到的对象包括当前类实例(或静态类)的类属性、当前执行方法的局部变量、以及一些全局能引用到的对象(包括常量池、方法区的类信息等)等。上述对象以外的对象要么是间接被上述对象引用,如果不是,那么就是永远不会被用到的对象。所以最终就可以以上述3种描述的对象作为GC root去做内存区的可达性分析。

如何清理(垃圾回收算法)

标记清除算法

如字面意思,在运行的时候先标记无用的对象让后再清除。缺点是标记和清除消耗都比较大。另外在清理了以后,会留下内存碎片,碎片的处理也是一个消耗。

复制算法

将内存分成两块,GC时将使用的那块内存的有用对象复制到另外一个内存块上。好处是处理简单在迁移到新内存块的时候直接在空的内存块上按序排列对象就行了,不会出现内存碎片(相对于标记算法)。缺点是需要内存较多。
大部分商业虚拟机的新生代使用复制算法。内存区分成Eden、Survivor1、Survivor2三个区域。GC的时候将Eden、Survivor1两个部分的对象用复制算法迁移到Survivor2上,然后当下一次GC时就Eden、Survivor2 -> Survivor1。
因为新生代的内存都是一初始化马上又很快会被GC掉,故而内存的比例是这样的:

Eden : Survivor1 + Survivor2 = 8 : 1

即 Eden 很大,Survivor1、Survivor2很小。这样新生代的内存会慢慢轮流积蓄在Survivor1、Survivo2上(他们轮流持有)。当他们的内存不够时就迁移到老年代去。

标记整理算法

和标记清除算法类似,区别是才用「整理」而非「清楚」。整理的做法是先把有用对象向一端归并在一起,然后一次性清理掉有用对象一端以外的所有无用对象。

对象分待收集处理

虚拟机都采用分代处理方式处理堆上的对象。老生代上是长期持有的对象,新生代上是刚建立的新鲜的对象。Eden是是「伊甸园」洋房夏娃初生的地方,代表了新创建的对象的意思。Survivor是存活的意思,意指在过了段时间在「伊甸园」中仍然存活的对象。在存活区(Survivor)中慢慢积累的对象会具有进入老生代的资格。
新生代因为大部分对象都会被丢弃,因为一般情况下的Java程序,会建立大量的零时对象,而长期持有的对象相对少,所以新生代适合复制算法。而老生代则不同,老生带适合标记处理算法,老生带普遍比较大,复制算法不适合,因为复制算法需要更多的内存支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
   S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.00 99.19 93.29 41.11 98.22 96.73 11 0.504 3 0.433 0.937
0.00 99.19 93.63 41.11 98.22 96.73 11 0.504 3 0.433 0.937
0.00 99.19 94.96 41.11 98.22 96.73 11 0.504 3 0.433 0.937
0.00 99.19 96.29 41.11 98.22 96.73 11 0.504 3 0.433 0.937
0.00 99.19 97.63 41.11 98.22 96.73 11 0.504 3 0.433 0.937
0.00 99.19 98.30 41.11 98.22 96.73 11 0.504 3 0.433 0.937
0.00 99.19 99.30 41.11 98.22 96.73 11 0.504 3 0.433 0.937
21.22 0.00 0.49 54.56 98.19 95.50 12 0.653 3 0.433 1.087
21.22 0.00 6.72 54.56 98.19 95.50 12 0.653 3 0.433 1.087
21.22 0.00 8.90 54.56 98.19 95.50 12 0.653 3 0.433 1.087
21.22 0.00 9.43 54.56 98.19 95.50 12 0.653 3 0.433 1.087
21.22 0.00 9.92 54.56 98.19 95.50 12 0.653 3 0.433 1.087

枚举根节点

在开始GC之初,先要确定哪些对象需要清理,前面说了用的是「可达性分析算法」,他需要从根节点开始一个一个链路的去遍历对象,以找出所有有用的对象。因为是全量的去遍历,所以这消耗是很大的。另外当进行「可达性分析」时,需要内存有一个「一致性快照」即程序停止运行。
备注:具体实现中,虚拟机用了一个引用表(OopMap),维护了对象应用关系。直接在这个表上运行「可达性分析」的时候将会非常快。

安全点和安全区域

什么时候去维护OopMap呢?很多字节码指令都能让对象引用关系变化,也就是OopMap的变化,这是不可取的,OopMap会需要更多的内存。
那些能长时间执行的指令,如方法调用、异常跳转等就能产生安全点。线程执行到该点时,会维护OopsMap,并做根节点枚举(可达性遍历),然后就可以判断是否需要GC了。安全点的意义是在指令序列中设立几个点去处理GC的初始步骤(维护OopMap、可达性遍历),而不是每个指令都去处理这些步骤,不然会有非常大的消耗。
还有一个问题,当GC发生时,怎么让所有线程进入安全点停下来开始GC呢?两种方式:

  1. 强行中断所有线程,没有进入安全区的线程让他运行到安全点(现在几乎没有虚拟机这么实现)
  2. 设置一个标识,各个线程在到达安全点的时候探测这个标识,然后开始GC。

接着说安全区域。安全区域解决线程不运行的情况(线程未获得CPU时间,比如被sleep或者blocked状态)。安全区域是一段引用关系不会变化的指令区段,在这段区域任何地方执行GC都安全。
当线程被挂起,就不能检测安全点,都不运行了还怎么达到安全点。这个时候就靠安全区域了,进入了安全区域的线程会给自己设置一个「Safe Regin」状态,在安全区域中如果被中断,这个时候可能GC刚开始,或者GC正在进行。如果继续中断那也不影响GC,如果从中断恢复过来,那么在出安全区域的时候,会等待GC线程完成GC并把自己的「Safe Regin」状态取消掉才继续执行,不然不会继续执行。

Parallel Scavenge

Parallel Scavenge 是一个更多关注吞吐量的垃圾收集器,所谓吞吐量就是执行用户代码的时间除以(执行用户代码的时间 + 垃圾回收时间)。这个收集器更适合后台运行的程序,如果是需要快速响应请求的则不适合。

CMS(Concurrent Mark Sweep)

这个算法的执行分4个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 标记清除(CMS concurrent sweep)

处理流程如图,其中左边的是单线程的标记清除算法:

图中可以看出,「初始标记」「并发标记」是会stop the world的。他的「并发标记」和「并发清除」线程是可以和用户线程一起运行的,所以只有较少的时间是会阻断用户线程的。而且「并发标记」和「并发清除」的运行时间相对更长。
他也有缺点:

  • 标记和清理线程和用户线程一起运行,造成的问题是会多少影响用户线程的运行,降低其吞吐。即使JVM是控制了标记和清理线程数。
  • 「并发清理」阶段,用户线程还是在运行的,还是会在产生垃圾。所以CMS收集器不能等到内存用完的时候再开始,因为他在还没有完成清理的时候,用户线程已经开始产生新的垃圾。如果CMS开始前是满的,那么用户线程就拿不到内存去新建对象了。解决办法是CMS开始的时候预留一部分内存。但是如果这部分预留内存不够用怎么办?当预留内存不顾的时候就会启动一个Serial Old收集器,相当于 stop the world以后再进行清理,又因为是单线程,所以就很慢了。这也是只有CMS收集器才有的坑。
  • 使用标记-清除算法的CMS还有个问题是清理完以后,堆上的内存是有很多碎片的。这个时候就需要去整理内存。整理也是一个有一定消耗的过程,而且无法并发处理。

G1(Garbage First)

TODO

Author

张阿力

Posted on

2017-03-15

Updated on

2018-02-13

Licensed under