java GC垃圾回收机制

垃圾收集器(collector)主要关注两个方面:

1. 找到所有存活的对象

2. 清除掉不可用对象

在所有收集器中,都是通过标记(Marking的方法找到存活对象的。

 

一、标记可访问对象

现在JVM中所有的GC算法都是从找出存活对象开始的。下图形象的展示了JVM中各对象之间的引用关系:    

  首先,GC定义了一些GC Roots对象。一般来说,GC Root是由以下对象组成:

本地变量以及当前正在执行的方法中的输入参数对象

存活的线程

加载的类中的静态属性

JNI引用

 

  然后,GC会在内存中分析所有对象组成的引用图,从GC Roots开始,找到所有GC Roots对象直接或间接引用的对象,所有这些对象都被标记为存活对象。

 

  存活对象即上图中的蓝色小圆圈。当标记阶段结束后,每一个存活对象就已经被标记过了。那些没有被标记到的对象,即上图中的灰色圆圈,就被认为是从GC Roots不可访问的对象。这些对象会被认为是需要清理的垃圾,这些垃圾对象会在接下来的步骤中进行清除。

 

  在标记过程中,我们需要注意以下几点:

1.标记过程需要暂停整个应用。因为如果在标记的同时,应用程序也在运行,对象的引用状况随时可能会发生变化,无法进行准确的标记。可以安全暂停整个应用线程(Stop The World)的地方成为安全点(safe point)。触发安全点的原因有很多种,但是到目前为止,垃圾回收导致的安全点别触发是最主要的原因。

2.标记过程中的暂停时间长短不是由堆中的对象总数或者堆大小来决定的,而是由存活对象的多少来确定的。所以增加堆的大小并不会直接导致标记阶段暂停时间的增加。

3.当标记阶段完成后,GC就会在下一阶段中将标记为不可访问的对象清除掉。

 

 

二、清除不再使用对象

在不同的GC算法中,清除不再使用的对象的方法是不相同的。不同GC算法清除垃圾对象的过程大致可以分为以下几种:

清除(Sweeping)

整理(Compacting)

复制(Copying)

 

1、清除(Sweep)

  标记清除(Mark and Sweep)类算法在清除不再使用对象时的策略非常简单,直接忽略掉这些对象即可。即这些算法在标记阶段结束后,那些不可访问的对象所占的内存空间就被认为是空闲的,当有新的对象需要分配空间时,直接覆盖这部分的内存。

这种策略需要维持一个空闲列表(free-list)来记录每一个空闲区域以及该区域的大小。在为新对象分配空间时,就会记录和维护这个列表。这种策略的一个最显著问题就是这些空闲内存不连续,如果空闲内存片段中最大的区域也无法为新生成的对象分配空间时,也会报出无法分配内存的异常。

 

2、整理(Compact)
  
3、复制(Copying)

新生代GC算法

老年代GC算法

JVM参数

Incremental

Incremental

-Xincgc

Serial

Serial

-XX:+UseSerialGC

Parallel Scavenge

Serial

-XX:+UseParallelGC -XX:+UseParallelOldGC

Parallel New

Serial

N/A

Serial

Parallel Old

N/A

Parallel Scavenge

Parallel Old

-XX:+UseParallelGC -XX:+UseParallelOldGC

Parallel New

Parallel Old

N/A

Serial

CMS

-XX:+UseParNewGC -XX:+UseConcMarkSweepGC

Parallel Scavenge

CMS

N/A

Parallel New

CMS

-XX:+UseParNewGC -XX:+UseConcMarkSweepGC

G1

 

-XX:+UseG1GC

1Minor GC
 对照上面的不同字段进行说明, 
(1)2015-05-26T14:45:37.987-0200,发生本次GC动作的时间 
(2)151.126,GC事件发生时距离该JVM启动的时间,单位为秒 
(3)GC,用于区分是Minor GC还是Full GC。这里表示本次是Minor GC 
(4)Allocation Failure,导致本次进行GC的原因。在这里,本次GC是由于无法为新的数据结构在新生代中分配内存空间导致的。 
(5)DefNew,垃圾收集器的名称。这个名称表示的是在新生代中进行的单线程,标记-复制,全应用暂停的垃圾收集器 
(6)629119K->69888K,表示新生代内存空间在GC前后的大小。 
(7)629120K,括号里的值,表示新生代的总大小 
(8)1619346K->1273247K,堆内存在GC前后的大小 
(9)2027264K,括号里的值,堆内存中可用大小 
(10)0.0585007 secs,GC动作的时间,单位为秒 
(11)Times: user=0.06 sys=0.00, real=0.06 secs,GC动作的时间,其中:
(2)Tenured老年代垃圾收集器的名称。Tenured表示一个单线程,暂停整个应用线程的标记清除整理的垃圾收集过程。 
  并行垃圾收集器一般用在多核服务器上,在多核服务器上使用并行GC,能重复利用硬件资源,提高应用的吞吐量, 
- 在垃圾收集过程中,会利用所有的核并行进行垃圾回收动作,降低应用暂停时间 
- 在垃圾回收间歇期,垃圾收集器不工作,不会消耗系统资源 
   
  另一方面,并行GC的所有阶段都不能被中断,所以这些垃圾收集器仍然有可能在所有应用线程停止时陷入长时间的暂停中。所以,如果要求系统低延迟,那么不建议使用这种垃圾收集器。    
  接下来,我们看一下并行GC时的日志信息。如下所示,
(4)Ergonomics,本次GC的触发原因。这里是由于JVM认为此刻是一次适合进行垃圾回收的时间 
(5)[PSYoungGen: 1305132K->0K(2796544K)],垃圾收集器的名称。PSYoungGen表示这是一次新生代中进行的标记复制,暂停全部应用的新生代GC。新生代的内存空间使用量从1305132K降低到0。一般来说,进行了一次Full GC后,新生代的内存空间将会被全部清理。 
(6)ParOldGen,老年代中的垃圾收集器类型。在这里ParOldGen表示在老年代中使用的标记清除整理,暂停全部应用的老年代垃圾收集器。
  这一步是CMS垃圾收集过程中两次stop-the-world的其中一次。目的是找到垃圾收集器的roots对象。 
  这个阶段垃圾收集器将会遍历老年代中的所有对象,然后标记其中哪些对象仍然存活。判断对象是否存活将参考当前对象是否直接或间接的被上一阶段中找出的Roots对象所引用。这一阶段的动作将与应用线程并行进行。
  这一阶段也是一个并发阶段,与应用线程并发运行,不会将应用线程暂停。当阶段二在并发标记时,随着应用的运行,也可能会改变某些对象的引用状况。比如标记为存活的对象,或许已经被遗弃,比如被遗弃的对象又被重新使用等。这些引用状况发生变化的对象将被JVM标记为drity(即所谓的Card Marking)。在并发预清理阶段,这些对象将被标记为存活,即使可能会出现将垃圾对象也标记为存活。此外, 本阶段也会执行一些必要的细节处理,并为Final Remark 阶段做一些准备工作。
  这一阶段同样不会暂停应用线程,主要用于尽可能少的减少Final Remark阶段的工作量,以减少stop-the-world的时间。这一阶段会循环重复相同的动作,直到循环次数,有用工作量,消耗的系统时间达到预定值为止。
  这是CMS垃圾收集器各阶段中第二个也是最后一个stop-the-world阶段。这一阶段的目的是确定老年代中最终具体存活有哪些对象。这意味着在这一阶段需要遍历老年代区域中(包括阶段三中的dirty对象)所有能直接或间接被GC Roots对象所引用的对象。    
  通常CMS垃圾收集器会在新生代有足够空间的情况下尝试执行最终标记阶段。这主要是为了防止与新生代GC导致的stop-the-world事件连续发生。即尽量避开与新生代GC连续执行。 
(3)YG occupancy: 387920 K(613440 K),新生代的内存使用量,以及新生代总内存量 
(4)[Rescan (parallel), 0.0085125 secs],这一步将在应用暂停时并发标记出所有存活对象。在这里耗时为0.0085125秒 
(5)[weak refs processing,0.0000243 secs]65.559,这里处理弱引用对象,耗时为0.0000243秒,发生在JVM启动后的第65.559秒 
(6)[class unloading, 0.0013120 secs]65.560,清理未使用的类信息,并记录好耗时和发生时间 
(7)scrub string table, 0.0001759 secs,清理持有class级别元数据的symbol和string表,总耗时为0.0001579秒 
   
阶段六:并发清除(Concurrent Sweep) 
  与应用线程一起并发进行,不需要stop-the-world。这一阶段的目的是清除不再使用的对象,回收这些对象占用的内存空间。
  并发执行阶段,重置CMS算法内部使用的数据结构,为下一次垃圾回收作准备。
  与其他垃圾收集器相比,G1有一些独特的设计。首先,堆空间不再被划分为连续新生代和老年代,而是将堆空间划分为一定数量(一般为2048)个小的堆区域(heap regions)。在这些堆区域中可以存储对象。每一个区域可能都是一个Eden区,Survivor区,或者是Old区。在逻辑上,G1的小堆区中所有的Eden区和Survivor区可以统称为新生代,所有Old区可以统称为老年代。如下图所示:    
 
 
   
  G1的另一点不同之处是,在GC的并发阶段,G1收集器会预估一下每个region中可能存活的对象有多少。那些被预估出可能包含很多垃圾的Region会被归入回收集中,优先进行收集。这也是G1收集器所谓的垃圾优先(garbage-first)的由来。 
   
  如果想要设置JVM使用G1收集器的话,使用如下参数

标记清除整理(Mark-Sweep-Compact)类算法则解决了上面标记清除算法的缺陷。在对不再使用的对象进行标记和清除之后,接下来会把存活的对象在内存区域中进行移动,按顺序分配在连续的内存区域中。由于需要将所有对象复制到一个新的区域并且更新指向该对象的引用,所以该策略在GC暂停上的时间消耗会更长。但是这种算法的显著优势是在后续新对象的内存分配上更加简单和高效,直接在已用内存尾部为新生成对象分配一片内存区域即可。


标记复制(Mark and Copy)类算法和标记整理算法非常类似,这两种算法都会为存活对象重新分配内存区域。两者的不同之处在于,标记复制算法为存活对象重新分配的内存区域是另一块存活区。标记复制策略的优点是,复制过程可以和标记过程同时进行,一个对象被标记为存活的同时就可以将其复制到另一片内存区域中。标记复制策略的缺点是,需要准备另一块足够容纳所有存活对象的内存区域。 

  

 

 

 

GC算法实现

对大多数JVM来说,一般需要选择两种GC算法,一种用于回收新生代内存区,另一种用于回收老年代内存区域。

新生代和老年代GC算法的可能组合如下表所示,如果不指定的话,将会在新生代和老年代中选择默认的GC算法。下表中的GC算法组合是基于Java 8的,在其他Java版本中可能会有所不同。 

一般常用的是上面加粗的四种组合。剩下的组合一般是已经不用了,或者是不再支持,或者在实际中基本不使用。所以,在接下来的文章中,只介绍上面这四种组合:

1新生代和老年代的串行GC(Serial GC)

2新生代和老年代的并行GC(Parallel GC)

3新生代并行GC(Parallel GC) + 老年代CMS

4部分新生代老年代的G1

一、串行GC(Serial GC)

 串行GC对于新生代使用标记复制(mark-copy)策略,对老年代使用标记清除整理(mark-sweep-compact)策略进行垃圾回收。这些收集器是单线程的,不能并发的对垃圾进行回收。并且在垃圾回收动作时会暂停整个应用线程(stop-the-world)

  这种GC算法无法充分利用硬件资源,即使有多个核,在GC时也只用其中一个。在新生代和老年代启动串行GC的命令如下:

java -XX:+UseSerialGC com.mypackages.MyExecutableClass

  

这种GC算法一般并不常用,只有在堆内存为几百MB,并且应用运行在单核CPU上时才使用。一般应用都部署在多核的服务器上,如果使用串行GC会在GC时无法充分利用资源,造成性能瓶颈,提高应用延迟和降低吞吐量。

  接下来我们看一个串行GC的垃圾收集日志信息,使用如下命令使应用打印出GC日志:

-XX:+PringGCDetails -XX:+PringGCDateStamps-XX:+PringGCTimeStamps

 

输出日志如下:

  清理新生代内存的GC事件日志如下,

2015-05-26T14:45:37.987-0200 :151.1262 :

[ GC (AllocationFailure 151.126:

[DefNew :629119K->69888K (629120K)> , 0.0584157 secs]

1619346K-1273247K (2027264K), 0.0585007> secs]

[Times: user=0.06 sys=0.00, real=0.06 secs]

   

user- 表示在此次垃圾回收过程中,所有GC线程消耗的CPU时间之和

sys - 表示GC过程中操作系统调用和系统等等事件所消耗的时间

real - 应用暂停的总时间。由于串行GC是单线程的,所以暂停总时间等于user+sys

 

经过上面这些分析后,我们可以更加清楚的从GC日志中获取到当时的详细信息。在GC前,总共使用了1619346K堆内存,其中新生代使用了629119K。通过计算就可以得到老年代使用了990227K。

  GC后,新生代释放出了559231K内存空间,但是堆的总内存仅仅释放了346099K。也就是说,在本次GC时,有213132K的对象从新生代升级到了老年代区域。

 

下图形象的表明了本次GC前后内存的变化情况。 

 

2Full GC

2015-05-26T14:45:59.690-0200 : 172.829 :

[ GC (Allocation Failure

172.829:[ DefNew:629120K->629120K(629120K), 0.0000372 secs]

172.829:[Tenured:1203359K->755802K (1398144K), 0.1855567 secs ]

1832479K->755802K (2027264K),

[Metaspace:6741K->6741K(1056768K)]

[Times: user=0.18 sys=0.00, real=0.18 secs]

 

(1)[DefNew:629120K->629120K(629120K), 0.0000372 secs,由于分配内存不足导致的一次新生代GC。在本次GC时,首先进行的是新生代的DefNew类型GC,将新生代的内存使用从629120K降低到0。注意在这里,JVM的显示有问题,误认为年轻代内存使用完了。本次GC耗时0.0000372秒 

(3)[Metaspace:6741K->6741K(1056768K)],元数据区在垃圾回收前后的内存使用情况,从这里可以看出,本次GC时并没有对元数据区的内存进行回收 

本次Full GC与上面的Minor GC区别十分明显,Full GC是会对老年代和元数据区进行垃圾回收的。本次垃圾回收的过程如下图所示:

 

 

 

二、并行GC(Parallel GC)

在这种GC模式下,新生代使用标记复制策略,老年代使用标记清除整理策略。新生代和老年代的GC事件都会导致所有应用线程暂停。新生代和老年代在复制(copy)或整理(compact)阶段都使用多线程,这也是并行GC名称的来由。使用这种GC算法,可以降低垃圾回收的时间消耗。 

 

在垃圾回收时的并行线程数,可以由参数-XX:+ParallelGCThreads=NNN来设置。该参数的默认值是服务器的核数。 

  使用并行GC,可以用以下三种命令模式:

java -XX:+UseParallelGC com.mypackages.MyExecutableClass

java -XX:+UseParallelOldGC com.mypackages.MyExecutableClass

java -XX:+UseParallelGC -XX:+UseParallelOldGC com.mypackages.MyExecutableClass

   

1、Minor GC

2015-05-26T14:27:40.915-0200:

116.115: [GC (Allocation Failure)

[PSYoungGen:2694440K->1305132K(2796544K)] 9556775K->8438926K(11185152K),

0.2406675 secs]

[Times: user=1.77sys=0.01, real=0.24 secs]

(1)Allocation Failure,导致本次GC的原因。是由于新生代中无法为新对象分配内存 

(2)PSYoungGen,垃圾收集器的名称,这里表示这是一个并行标记复制,暂停全部应用的新生代垃圾收集器 

(3)real - 应用暂停的总时间。在并行GC中,这个数值应该接近于(user + sys) / GC线程数即单个核上平均的暂停时间,在这里线程数为8由于某些过程是不能并行执行的,所以这个值会比刚才求的均值略高。

 

2、Full GC

2015-05-26T14:27:41.155-0200:

116.356: [Full GC(Ergonomics)

[PSYoungGen:1305132K->0K(2796544K)]

[ParOldGen:7133794K->6597672K(8388608K)] 8438926K->6597672K(11185152K),

[Metaspace:6745K->6745K(1056768K)], 0.9158801 secs]

[Times: user=4.49 sys=0.64,real=0.92 secs]

(3)Full GC,表示本次是一次Full GC,将会对新生代和老年代的内存空间进行回收 

 

 

 

三、并发标记清除CMS(ConcurrentMark and Sweep)

CMS垃圾收集器在新生代使用stop-the-world的并行标记复制算法,在老年代使用并发的标记清除算法。

这种收集器可以避免在回收老年代空间时出现的长时间暂停。这主要是由于:(1)不对老年代的空间进行整理,而是使用一个空闲列表(free-lists)来管理这些被回收的空间。(2)标记清除阶段与应用并行进行。这意味着CMS垃圾收集器在回收老年代空间时完全不会停止应用线程,并且使用多线程来完成这些操作。默认情况下,并发线程数为当前机器物理核的1/4。 

 

要使用CMS垃圾收集器的话,可以使用如下参数

java -XX:+UseConcMarkSweepGC com.mypackages.MyExecutableClass

 

      如果应用运行在多核机器上,并且对系统的延迟性能要求比较高,那么就很适合使用这种垃圾收集器。但是,由于CMS垃圾收集器在大部分时候总会有一些CPU资源正在进行GC操作,所以势必会降低系统的吞吐量。 

 

接下来我们看一些CMS在垃圾回收时生成的日志信息:

1、Minor GC

2015-05-26T16:23:07.219-0200: 64.322:

[GC (Allocation Failure) 64.322:

[ParNew: 613404K->68068K(613440K),0.1020465 secs]

10885349K->10880154K(12514816K), 0.1021309 secs]

[Times: user=0.78 sys=0.01, real=0.11 secs]

(5)ParNew,垃圾收集器的名称。在这里表示这是一次在新生代中进行的并行标记复制,stop-the-world的垃圾回收。一般与老年代中的并发标记清除垃圾收集器配合使用。

(6)real - 应用暂停的总时间。在并行GC中,这个数值应该接近于(user + sys) / GC线程数。

 

2、Full GC

阶段一:初始标记(Initial Mark) 

CMS InitialMark,标识本次垃圾收集的阶段。initial mark阶段的目的是找到垃圾收集器的roots对象。 

 

阶段二:并发标记(Concurrent Mark) 

CMS-concurrent-mark,CMS垃圾收集器的并发标记阶段。遍历老年代中所有对象,标记其中存活的对象,这一阶段并发执行,并且不会stop-the-world 

 

阶段三:并发预清理(ConcurrentPreclean) 

CMS-concurrent-preclean,表示此次为并发预清理阶段,找出在阶段二中标记过之后引用状态发生变化的对象 

 

阶段四:并发可取消预清理(ConcurrentAboartable Preclean) 

CMS-concurrent-abortable-preclean,表示这是并发可取消的预清理步骤 

 

阶段五:最终标记(Final Remark) 

(2)CMS Final Remark,标记本次是FinalRemark阶段。这一阶段会stop-the-world,用于标记出老年代中所有存活对象 

 

经过以上五步标记阶段后,老年代中所有存活对象都已经被标记过,接下来垃圾收集器将回收其中不再使用的对象,并清理老年代的内存空间。 

CMS-concurrent-sweep,标记本次是并发清除阶段,清除不再使用的对象,回收这些对象占用的空间 

 

阶段七:并发重置(ConcurrentReset) 

CMS-concurrent-reset,标识并发重置阶段。这一阶段主要用于重置CMS算法内部使用的数据结构,为下一次垃圾回收周期做准备 

 

总结一下,CMS垃圾收集器尽量将大部分工作移到必须stop-the-world阶段之外并发进行,大大降低了应用暂停时间。但是CMS垃圾收集器的缺点也是很明显的,垃圾收集后在老年代中会有很多内存碎片。并且CMS垃圾收集器可能会造成不可预期的暂停,尤其是在堆内存比较大的时候。

 

 

四、G1(Garbage First)

实现G1(Garbage First,垃圾优先) 垃圾收集器的主要目的是想将垃圾回收过程中导致的Stop-The-World维持在一个可控范围内,对暂停时间可预期,可配置。可以为G1收集器设置一个性能调优目标,可以要求垃圾收集器在给定的y毫秒时间内stop-the-world的暂停时间不超过x毫秒。例如,在给定1秒时间内,由于垃圾收集导致的应用暂停时间不能超过5毫秒。G1收集器会尽可能的去达到设定的调优目标,但是并不保证完全实现设定值。所以,可以称G1收集器为软实时(soft real-time)。   

这种设计可以避免GC时需要同时回收整个堆空间,在每次GC时,只需要回收所有Region中的一部分小堆区即可,即所谓的回收集(collectionset)。所有Young区会在每次暂停时进行回收,部分Old区也有可能在暂停时被回收, 

java -XX:+UseG1GC com.mypackages.MyExecutableClass

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值