《深入理解java虚拟机》- 02 GC

1、对象是否存活

1.1 可达性分析算法

可达性分析

(1) java是通过可达性分析来判定对象是否存活:通过GC Roots的对象作为起始点向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明该对象不可用。如上图所示,Object5和Object6到GC Roots没有引用链,它们将被判定为可回收对象

(2) 可作为GC Roots的对象包括:

虚拟机栈(栈帧中的本地变量表)中引用的对象

方法区中静态属性引用的对象

方法区中常量引用的对象

本地方法栈中JNI(即一般说的Native方法)引用的对象

1.2 引用概念的扩充

(1) 强引用:类似Object obj = new Object()的引用,只要强引用还在,永远不会被GC回收

(2) 软引用:通过SoftReference实现软引用,虚拟机将要抛出OutOfMemoryError异常之前,将会发起一次GC动作,回收掉所有非强引用的对象

(3) 弱引用:通过WeakReference实现弱引用,比软引用更弱,无论当前内存是否足够,都会回收掉被弱引用关联的对象

(4) 虚引用:通过PhantomReference实现虚引用,最弱的一种引用,唯一作用是该对象回收时收到一个通知

1.3 对象的自我拯救 - finalize方法

被虚拟机判定为不可达的对象,还有一次拯救自己的机会。如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,将会被第一次标记,并进行一次筛选,如果该对象没有重写finalize()方法,或者finalize()方法被虚拟机调用过,将视为不必执行。

如果被判定为有必要执行finalize()方法,该对象将会放入F-Queue队列,并在稍后由虚拟机自动创建的、低优先级的Finalizer线程去执行,finalize()方法是对象逃脱死亡的最后一次机会,如果对象在finalize()方法中重新和引用链建立连接,就可避免被回收。finalize()方法只会被系统自动调用一次

1.4 方法区(永久代)的回收

(1) gc主要回收两部分内容:废弃常量和无用的类。回收废弃常量和回收堆中的对象类似。判定一个类是否无用,需同时满足3个条件:一是该类的所有实例都被回收,二是加载该类的ClassLoader已被回收,三是该类对应的java.lang.Class对象没有在任何地方被引用

(2) HotSpot虚拟机通过 -Xnoclassgc 参数进行控制是否启用类卸载功能。在大量使用反射、动态代理、CGLib等框架,需要虚拟机具备类卸载功能,避免方法区发生内存溢出

2、垃圾回收算法

2.1 标记-清除(Mark-Sweep)算法

(1) 算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,然后统一回收有标记的对象

(2) 两个不足:一是效率不高,二是会产生大量不连续的内存碎片

2.2 复制(Copying)算法

(1) 新生代分为一个Eden,两个Survival空间,默认比例是8:1。回收时,将Eden和一个Survival的存活对象全部放入到另一个Survival空间中,最后清理掉刚刚的Eden和Survival空间

(2) 当Survival空间不够时,由老年代进行内存分配担保

2.3 标记-整理(Mark-Compact)算法

标记过程和“标记-清除”算法类似,不同之处是让所有存活的对象向一端移动,然后直接清理掉边界以外的对象

2.4 分代收集(Generational Collection)算法

将堆分为新生代和老年代,新生代对象存活率低,选用复制算法,老年代存活率高,选用标记-清除或标记-整理算法

3、hotspot的算法实现

3.1 枚举根节点

(1) 可达性分析时,必须停顿所有java执行线程(Stop The World)

(2) HotSpot通过OopMap记录对象引用存放的地址,类加载完后,HotSpot把偏移量对应的类型数据计算出来,JIT编译过程中,会在特定位置记录下栈和寄存器中哪些位置是引用

3.2 安全点

(1) 程序执行时,只有到达安全点才能停顿下来开始GC

(2) 安全点的选定以“是否让程序长时间执行”为标准,“长时间执行”最明显的特征是指令序列复用,如方法调用、循环跳转、异常跳转等

(3) 采用主动中断的方式让所有线程都跑到最近的安全点上停顿下来。设置一个标志,各个程序执行的时候轮询这个标志,发现中断标志为真时自己就中断挂起

3.3 安全区域

安全区域是指在代码片段中,引用关系不会发生变化。在这块区域任何地方GC都是安全的,可以把Safe Region看做是Safe Point的扩展

4、垃圾回收器

垃圾回收器

上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,说明它们可以搭配使用,所处区域表示收集器属于新生代还是老年代。

4.1 Serial收集器

       Serial收集器是一款年轻代的垃圾收集器,使用标记-复制算法。它是一款历史最悠久的垃圾收集器。Serial收集器只能使用一条线程进行垃圾收集工作,并且在进行垃圾收集的时候,所有的工作线程都需要停止工作,等待垃圾收集线程完成以后,其他线程才可以继续工作。工作过程可以简单的用下图来表示:

4.2 ParNew收集器

(1) ParNew收集器是Serial收集器的多线程版本,除了多线程外,与Serial相比没有太多创新之处

(2) 多线程版本的年轻代收集器中,只有它可以和CMS一起搭配搭配使用

(3) ParNew收集器在单核环境性能不如Serial收集器(线程交互的开销),默认开启的收集线程数和CPU数量相同,可以使用 -XX:ParallelGCThreads 限定垃圾回收的线程数

4.3 Parallel Scavenge收集器

(1) 目标是达到一个可控的吞吐量(Throughput),吞吐量=运行用户代码时间/(运行用户代码时间+GC时间)

(2) 控制吞吐量的两个参数:-XX:MaxGCPauseMillis(最大GC停顿时间)和-XX:GCTimeRatio(设置吞吐量大小)

(3) -XX:MaxGCPauseMillis允许设置一个大于0的毫秒数,GC停顿时间短是以牺牲吞吐量和新生代空间换取的

(4) -XX:GCTimeRatio是一个大于0且小于100的整数,默认值是99,即允许的最大GC时间1%(即1/(1+99)

(5) -XX:UseAdaptiveSizePolicy,打开后虚拟机会根据当前系统的允许情况收集监控信息,动态调整以提供最适合的停顿时间或最大的吞吐量,该方式称为GC自适应的调节

4.4 Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法,主要用于Client模式下老年代的垃圾收集器。在Server模式下,主要是在JDK1.5版本之前和Parallel Scavenge年轻代收集器配合使用,或者作为CMS收集器的后备收集器

4.5 Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理"算法,主要是和Parallel Scavenge收集器一起配合(注重吞吐量及CPU资源敏感的场景),可以实现对Java堆内存的吞吐量优先的垃圾收集策略

4.6 CMS收集器

(1) CMS(Concurrent Mark Sweep)收集器是以最短GC停顿时间为目标的收集器,符合重视服务响应时间的应用

(2) CMS收集器的工作过程可以分为4个阶段:

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

  从图中可以看出,在这4个阶段中,初始标记和重新标记这两个阶段都只有GC线程在运行,用户线程会被停止,所以这两个阶段会发送STW(Stop The World)。初始标记阶段的工作是标记GC Roots可以直接关联到的对象,速度很快。并发标记阶段,会从GC Roots 出发,标记处所有可达的对象,这个过程可能会花费相对比较长的时间,但是由于在这个阶段,GC线程和用户线程是可以一起运行的,所以即使标记过程比较耗时,也不会影响到系统的运行。重新标记阶段,是对并发标记期间因用户程序运行而导致标记变动的那部分记录进行修正,重新标记阶段耗时一般比初始标记稍长,但是远小于并发标记阶段。最终,会进行并发清理阶段,和并发标记阶段类似,并发清理阶段不会停止系统的运行,所以即使相对耗时,也不会对系统运行产生大的影响。

  由于并发标记和并发清理阶段是和应用系统一起执行的,而初始标记和重新标记相对来说耗时很短,所以可以认为CMS收集器在运行过程中,是和应用程序是并发执行的。由于CMS收集器是一款并发收集和低停顿的垃圾收集器,所以CMS收集器也被称为并发低停顿收集器。

(3) 不足:一是CMS收集器对CPU资源非常敏感;二是CMS收集器在处理垃圾收集的过程中,可能会产生浮动垃圾,由于它无法处理浮动垃圾,所以可能会出现Concurrent Mode Failure问题而导致触发一次Full GC。所谓的浮动垃圾,是由于CMS收集器的并发清理阶段,清理线程是和用户线程一起运行,如果在清理过程中,用户线程产生了垃圾对象,由于过了标记阶段,所以这些垃圾对象就成为了浮动垃圾,CMS无法在当前垃圾收集过程中集中处理这些垃圾对象;三是它在进行垃圾收集时使用的"标记-清除"算法,在进行垃圾清理以后,会出现很多内存碎片,过多的内存碎片会影响大对象的分配,会导致即使老年代内存还有很多空闲

4.7 G1收集器

为解决CMS算法产生空间碎片和其它一系列的问题缺陷,HotSpot提供了另外一种垃圾回收策略,G1(Garbage First)算法,通过参数-XX:+UseG1GC来启用,该算法在JDK 7u4版本被正式推出,官网对此描述如下:

The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with a high probability, while achieving high throughput. The G1 garbage collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collector is designed for applications that:

1.Can operate concurrently with applications threads like the CMS collector.

2.Compact free space without lengthy GC induced pause times.

3.Need more predictable GC pause durations.

4.Do not want to sacrifice a lot of throughput performance.

5.Do not require a much larger Java heap.

在G1算法中,堆内存被划分为多个大小相等的内存块(Region),每个Region是逻辑连续的一段内存,结构如下:

每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色,其中H是以往算法中没有的,它代表Humongous,表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。

堆内存中一个Region的大小可以通过-XX:G1HeapRegionSize参数指定,大小区间只能是1M、2M、4M、8M、16M和32M,总之是2的幂次方。

GC模式:G1中提供了三种模式垃圾回收模式,young gc、mixed gc 和 full gc,在不同的条件下被触发。

young gc:发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc,这种触发机制和之前的young gc差不多,执行完一次young gc,活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。

参数含义:-XX:MaxGCPauseMillis设置G1收集过程目标时间,默认值200ms;-XX:G1NewSizePercent新生代最小值,默认值5%;-XX:G1MaxNewSizePercent新生代最大值,默认值60%

mixed gc:当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代。

mixed gc中有一个阈值参数-XX:InitiatingHeapOccupancyPercent,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次mixed gc。mixed gc的执行过程有点类似cms,主要分为以下几步:

(1) initial mark: 初始标记过程,整个过程STW,标记了从GC Root可达的对象

(2) concurrent marking: 并发标记过程,整个过程gc collector线程与应用线程可以并行执行,标记出GC Root可达对象衍生出去的存活对象,并收集各个Region的存活对象信息

(3) remark: 最终标记过程,整个过程STW,标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象

(4) clean up: 垃圾清除过程,如果发现一个Region中没有存活对象,则把该Region加入到空闲列表中

full gc:如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc

4.8 理解GC日志

33.125:[GC[DefNew:3324K->152K(3712K),0.0025925secs]3324K->152K(11904K),0.0031680 secs]

100.667:[FullGC[Tenured:0K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]

最前面的数字“33.125:”和“100.667:”代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。

GC日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的,例如下面这段新生代收集器ParNew的日志也会出现“[Full GC”(这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。如果是调用System.gc()方法所触发的收集,那么在这里将显示“[Full GC(System)”。

[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]

接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。

如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。

如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。

后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”。 

而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”。

再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。

有的收集器会给出更具体的时间数据,如“[Times:user=0.01 sys=0.00,real=0.02 secs]”,这里面的user、sys和real与Linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。

CPU时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时。当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以读者看到user或sys时间超过real时间是完全正常的。

5、内存分配和回收策略

5.1 对象优先在Eden区分配

一般情况下,对象在新生代Eden区分配,当Eden区没有足够空间时,将发起一次Minor GC。-XX:PrintGCDetails可以设置虚拟机在发生GC时打印内存回收日志,并在进程退出时输出当前内存各区域分配情况。

5.2 大对象直接进入老年代

大对象是指需要大量连续内存空间的对象,如很长的字符串或数组。通过设置-XX:PretenureSizeThreshold参数的值,大于该值的对象直接在老年代分配

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

虚拟机给每个对象定义了年龄,每经过一次Minor GC且在新生代存活的对象,年龄加1,当达到年龄阀值(默认15,可以通过-XX:MaxTenuringThreshold设置),将会被晋升到老年代中

5.4 对象年龄动态判断

为了更好的适应不同程序的内存情况,并不是完全根据对象的年龄来晋升到老年代,如果Survivor空间中相同年龄所有对象大小总和大于Survivor空间一半,年龄大于或等于该年龄的对象将直接进入老年代

5.5 空间分配担保

在发生Minor GC之前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果成立可以确保Minor GC成功,否则虚拟机会查看HandlePromotionFailure是否设置失败担保,如果允许,会检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管存在风险,如果小于,则进行一次Full GC。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容