深入理解JVM之垃圾收集器与内存分配策略

概述

思考GC需要完成的3件事:

* 哪些内存需要回收;
* 什么时间回收;
* 以什么方式回收;

回顾第二章Java内存运行时各个区域的划分:

* 程序计数器、虚拟机栈、本地方法栈中的内存区域是私有的,栈帧随方法的运行而进栈出栈,每一个栈帧所需分配的内存在类结构确定时就是已知的,因此这几个区域不需要考虑内存的回收;
* 对Java的堆和方法区,因为是共用的内存,只有在程序运行期间才知道创建哪些对象,内存的分配和回收都是动态的,垃圾收集器关注的往往是这部分的内存回收;

对象已死吗?

判断对象是否存活的方法有以下两种:

引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时计数器加一,引用失效就减一,当计数器值为0时,说明此对象不再被任何地方引用,可以被回收。
判断失效简单、效率高,但是不被主流虚拟机使用,主要原因是无法解决对象间的循环引用问题;

可达性分析法

通过一系列的称为“GC Roots”的对象开始向下搜索,走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链,即不可达GC Roots时,则判定这个对象不可用;
可达性分析图

可作为GC Roots的对象有如下几类:

* Java虚拟机栈中(栈帧中的本地变量表)引用的对象;
* 本地方法栈JNI中(Native方法)引用的对象;
* 方法区中引用静态属性的对象;
* 方法区中引用常量属性的对象;

再谈引用

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为了`强引用、软引用、弱引用、虚引用`,这四种引用强度由强到弱。
* 强引用:类似Object obj = new Object();这类的引用,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象;
* 软引用:代表一类有用但非必要的对象,对于软引用所关联的对象,在系统即将抛出内存溢出异常之前这类对象会被列入回收范围内进行二次回收。如果回收之后还是没有足够的内存,那么系统将抛出内存溢出异常。JDK1.2之后提供了SoftReference类来实现软引用;
* 弱引用:代表一类非必需的对象,强度比软引用还弱。这类引用所关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论当前内存是否充足,都会回收掉弱引用关联的对象。JDK1.2之后提供了WeakReference类来实现弱引用;
* 虚引用:又称为幽灵引用或幻影引用,是最弱的引用关系。一个对象是否有虚引用,对于对象的生存周期没有任何影响,也无法通过虚引用来获取对象实例。虚引用的存在只是为了对象在垃圾回收时收到一个系统通知。在JDK1.2之后提供了PhantomReference来实现虚引用;

生存还是死亡

要真正宣告一个对象的死亡,需要经历`两次标记`。如果对象进行可达性分析后发现对象到GC Roots没有引用链,此时进行第一次标记。标记之后需要进行筛选,筛选的条件是该对象是否有必要执行finalize()方法,有两种情况会被判断为没有必要执行finalize()方法:
    1. 该对象没有覆盖自带的finalize()方法;
    2. 该对象已经执行过finalize()方法;
筛选后如果对象有必要执行finalize()方法,则会把该对象放到一个F-Queue的队列中,由一个虚拟机自动创建的、优先级低的线程去执行。虚拟机不会承诺等待finalize()方法执行完成,稍后GC会对F-Queue中的对象进行第二次小规模标记。若对象没有在此之前在finalize()重新与GC Roots相关联,则该对象几乎已经是死亡的状态。
对象的finalize()方法不建议被调用,因为它的运行代价高、不确定性高、无法保证各个对象的调用顺序。

回收方法区

永久代的垃圾回收包括两部分:废弃常量和无用的类。
1. 废弃常量的判定条件:当前系统没有任何地方引用这个常量。即没有任何地方引用常量池中的常量,也没有其他地方引用该常量的字面量。
2. 无用的类的判定条件:
    1. 该类所有的实例被回收,即堆中没有该类的实例;
    2. 加载该类的ClassLoader已经被回收;
    3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问类中方法;
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成Jsp及OSGI这类频繁自定义ClassLoader的场景要求虚拟机具备类卸载功能,已保证永久代不会内存溢出。

垃圾收集算法

* 标记-清除算法(Mark-Sweep):首先标记出需要回收的对象(两阶段标记),然后对标记过的对象进行垃圾收集。缺点是效率不高并且清理出的内存有大量的内存碎片,致使在对象分配时只能进行“空闲列表分配”;
* 复制算法(Cppying):将可用内存分为两块,每次只使用一块,当这一块用完了,就将这块内存上还存活的对象复制到另一块内存上,然后回收掉当前的内存。优点是实现简单、运行高效,这样就可以使用“指针碰撞“来分配内存;缺点是代价太大,每次要牺牲一半的空间。在HotSpot虚拟机中,考虑到大部分对象的生存时间都很短适合在`新生代`中使用,所以将内存分为了一块Eden和两块Survivor,默认比例是8:1:1,每次只使用Eden和一块Survivor,这样每次只有10%的空间被浪费,当另一块Survivor内存没有空间存放上一次新生代收集下来的存活对象时,需要通过`分配担保机制`进入老年代;
* 标记-整理算法(Mark-Compact):标记过程和Mark-Sweep的过程一致,标记之后,让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存,适用在`老年代`;
* 分代收集算法:根据内存的不同的区域划分使用不同的收集算法。 在新生代使用复制算法,在老年代使用标记-清除或标记-整理算法,是现代虚拟机通常采用的算法;

HotSpot的算法实现

枚举根节点

* 由于要确保在一致性的快照中进行可达性分析,从而导致必须要停止所有的Java执行线程(”Stop The World”);
* 在HotSpot虚拟机中通过一组OopMap的数据结构知道在哪些位置存放着对象引用;

安全点

* HotSpot没有必要为每条指令都生成一个OopMap,会在特定的位置记录这些信息,这些位置称为安全点;
* 执行线程并非在所有地方都能停下开始GC,只有到达安全点才可以暂停;
* 安全点的选定是以”是否能让程序长时间运行“为条件选定的,如方法跳转、异常处理、循环结构等;
* 还需要考虑如何在GC时让所有的线程都能停在安全点:分为抢占式中断和主动式中断两种;
    1. 抢占式中断(Preemptive Suspension):GC发生时,所有线程停顿,如果发现有线程不在安全点上,就恢复线程,让它运行到安全点。几乎被弃用;
    2. 主动式中断(Voluntary Suspension):设置一个标志,各个线程主动轮询这个标志,为true时就将线程挂起。轮询标志的地方是和安全点重合的,另外再加上为创建对象需要分配内存的地方;

安全区域

* 如果程序没有为CPU分配时间(线程处于Sleep或Blocked),此时就需要安全区(Safe Region)。安全区指在一段代码片段中,对象的引用关系不会发生变化,在这个区域的任何位置开始GC都是安全的,Safe Region是Safe Point的扩展;
* 当线程执行到Safe Region的代码片段时,需要标识自己进入了Safe Region,离开Safe Region时,要检查是否完成了根节点的枚举或者整个GC过程,如果完成了,那么线程就继续执行,否则就必须等到收到可以安全离开Safe Region的信号;

垃圾收集器

* 垃圾收集算法是内存回收的方法论,垃圾收集器是具体实现;
* 基于JDK1.7Update14之后版本的HotSpot虚拟机包含的收集器如下(两两连线代表可以搭配使用):
常见的垃圾收集器

Serial收集器

Serial收集器
* 最基本、发展历史悠久的收集器,曾经是JDK1.3.1之前的虚拟机新生代收集的唯一选择;
* 是一个`单线程`(并非指的是一个收集线程,而是会暂停索引工作线程)收集器;
* 简单高效,没有线程切换的额外开销,即使是现在依然是虚拟机运行在`Client模式`下的默认新生代收集器;

ParNew收集器

ParNew收集器
* 是Serial收集器的`多线程`版本,其余行为(控制参数、收集算法、对象分配规则、回收策略等)都与Serial收集器一致,二者共用了大量的代码;
* ParNew收集器在单CPU的条件下绝对不会有比Serial收集器更好的效果,ParNew收集器需要额外的线程交互开销,即使是使用两个CPU都不能百分之百的超越Serial收集器;
* 运行在`Server模式`下首选的`新生代`收集器;
* `并发`(Concurrent):指用户线程与垃圾收集线程同时执行,用户线程在继续执行而垃圾收集程序运行在另外一个CPU上;
* `并行`(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态;

Parallel Scavange收集器

* 使用复制算法的`多线程`新生代收集器,几乎和ParNew收集器一样;
* `关注点`不一致,Parallel Scavenge收集器更加关注的系统到达一个可控制的吞吐量,而其他收集器(如CMS)关注的是如何缩短垃圾收集时用户线程的停顿时间;
* 高吞吐量(吞吐量=代码运行时间/(代码运行时间+垃圾收集时间))可以高效率的利用CPU时间,适合在`后台运算而不需要太多交互的任务`;
* 提供了两个参数用于精确控制吞吐量:-XX:MaxGCPauseMillis设置最大垃圾收集停顿时间,-XX:GCTimeRatio设置吞吐量大小;
* 最大停顿时间的缩短是以降低吞吐量和减小新生代内存空间为代价的,吞吐量设置的值相当于吞吐量的倒数;
* 参数-XX:UserAdaptiveSizePolicy为开关,打开后不需要设置新生代大小(-Xmn)、Eden与Survivor的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据监控参数动态调节以提供最合适的停顿时间和吞吐量,这种调节方式称为`自适应调节策略`,这种策略也是Parallel Scavenge和ParNew最大的区别之一;

Serial Old收集器

Serial Old收集器
* Serial Old收集器是Serial收集器的老年代版本,`单线程`,使用标记-整理算法;
* 主要意义也是在于给`Client模式`下的虚拟机使用;
* 在`Server模式下有两种用途`:一是JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,二是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用;

Parallel Old收集器

Parellel Old收集器
* 是Parallel Scavenge的老年代收集器版本,使用`多线程`和标记-整理算法,JDK1.6之后开始提供;
* Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代回收,`并非直接使用Serial Old收集器`;
* 老年代的Serial Old收集器在服务端应用性能上的拖累,即使使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,单线程的老年代收集中无法充分利用服务器多CPU的处理能力;
* 在`注重吞吐量以及CPU资源敏感的场合`,都可以优先考虑Parallel Scavenge + Parallel Old收集起的组合;

CMS收集器

CMS收集器
* CMS(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器;
* 目前很大一部分Java应用集中在互联网或者B/S系统的服务端,这类应用尤其`重视服务的响应速度`,CMS很适合这种场景;
* 基于标记-清除算法实现,整个过程分为`4个步骤`:
    1. 初始标记(CMS Initial Marking):需要Stop The World,仅仅是标记一下GC Roots能直接关联到的对象,速度很快;
    2. 并发标记(CMS Concurrent Mark):是从GC Roots开始对堆中对象进行可达性分析,耗时较长,但可同时与用户线程并发执行;
    3. 重新标记(CMS Remark):需要Stop The World,是对并发标记阶段因用户线程执行导致标记发生改变的那部分对象的修正;
    4. 并发清除(CMS Concurrent Sweep):执行垃圾回收;
* `优点`是能并发的和用户线程一起执行,停顿时间短;
* 3个明显的`缺点`:
    1. `对CPU资源比较敏感`。由于CMS收集器占用了一部分CPU资源而导致应用程序变慢,总吞吐量会降低。CMS默认开启的线程数是(CPU数量+3)/4,随着CPU数量的下降对程序的影响就越明显,为了减小这种影响产生了产生了“增量式并发收集器”的CMS收集器变种,主要目的就是在“Stop The World”期间,让GC线程和用户线程交替运行,避免GC线程独占资源是时间,从而使下降速度不那么明显,效果一般;
    2. `无法处理浮动垃圾`。因为在CMS收集器执行的过程中,不断有用户线程运行,CMS在当次无法回收掉,只能留在下次,即CMS在运行期间需要有内存空间支持用户线程继续运行。可以通过调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比从而降低内存回收次数,获得更好的性能。在CMS运行期间没有足够的内存满足程序需要,就会出现Concurrent Mode Failure,此时会启动`后备预案`,临时启动Serial Old收集器重新进行老年代的垃圾收集,停顿时间更加漫长。所以参数-XX:CMSInitiatingOccupancyFraction设置的太高容易造成Concurrent Mode Failure,性能反而降低;
    3. `产生大量内存碎片`。因为CMS是基于标记-清除实现的,所以无可避免的会在收集之后产生大量的内存碎片,碎片过多时,将会给大对象的分配造成很大的问题。CMS提供了一个参数-XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用于在CMS将要FullGC时开启内存碎片的合并整理过程,该过程无法并发,停顿时间较长。另外一个参数-XX:CMSFullGCsBeforeCompaction用于设置执行过多少次FullGC后进行一次压缩整理(默认值为0);

G1收集器

G1收集器
* 一款面向`服务端`应用的垃圾收集器,使命是替换掉CMS;
* `特点`:
    1. 并行与并发(可以充分利用多个CPU来降低Stop The World的停顿时间);
    2. 分代收集(可独立管理整个堆,但对不同年龄的对象使用不同的策略);
    3. 空间整合(整体上采用了标记-整理算法,局部采用了复制算法);
    4. 可预测的停顿(将堆分为大小相等的独立区域,避免全区域的垃圾收集);
* 关于`Region`:G1收集器中,虽然新生代和老年代的概念还在,但新生代和老年代不再是物理隔离的了,他们都是部分Region的集合。G1跟踪各个Region的垃圾堆积的价值大小(回收所获得空间的大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region;
* 不同Region之间的对象引用或新生代和老年代对象的相互引用,虚拟机都是采用`Remembered Set`来避免全堆扫描的。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断操作,检查Reference引用的对象是否处于不同的Region(或新生代引用了老年代的对象),如果是,便通过CardTable把相关信息写入到被引用对象所属Region中的Remembered Set中。进行GC时,把Remembered Set加入GC Roots枚举从而避免全堆扫描;
* 分为`4个步骤`:
    1. 初始标记(Initial Marking):仅仅是标记GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段的用户线程运行时能正常的创建对象,这阶段的线程停顿时间很短;
    2. 并发标记(Concurrent Mark):是从GC Roots开始向下进行可达性分析,找出存活的对象,耗时较长,但这个阶段用户线程可与垃圾收集线程并发执行;
    3. 最终标记(Final Marking):这个阶段是为了修正上一个阶段由于用户线程运行导致对象标记发生变化的那部分标记记录,虚拟机将这段时间对象的变化记录在线程Remembered Set Logs中,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这个阶段需要停止线程,但可并行执行;
    4. 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划,这个阶段也可以做到和用户线程一起并发执行,因为只回收一部分Region,时间是用户可控制的,停顿用户线程将大幅提高收集效率;

理解GC日志

GC日志示例
* 最前面的数字代表GC发生的时间(虚拟机启动以来经过的秒数);
* “[GC”和“[Full GC”说明停顿类型,有Full代表的是Stop-The-World的;
* “[DefNew”、“[Tenured”和“[Perm”表示GC发生的区域;
* 方括号内部的“3324K -> 152K(3712K)” 含义是 “GC前该内存已使用容量 -> GC后该内存区域已使用容量(该区域总容量)”;
* 方括号之外的“3324K -> 152K(11904)” 含义是 “GC前Java堆已使用容量 -> GC后Java堆已使用容量(Java堆总容量)”;
* 再往后“0.0025925 secs”表示该内存区域GC所占用的时间;

垃圾收集器参数总结

常用收集参数1

常用手机参数2

内存分配与回收策略

自动内存管理最终可以归结为自动化地解决了两个问题:对象的内存分配和回收为对象分配的内存。对象的分配`主要在堆上`进行,但也可能是经过JIT编译后被拆散为标量类型`间接地在栈上`分配。
* 对象`优先在新生代的Eden分配`:当Eden区没有足够的空间进行分配时,虚拟机会发生一次Minor GC,若对象都还存活且Survivor区没有足够的空间时则只能通过分配担保机制提前将对象转移到老年区;
    1. Minor GC(新生代GC):发生在新生代的垃圾收集动作,Minor GC发生十分频繁,回收速度也比较快;
    2. Major GC/Full GC(老年代GC):发生在老年代的垃圾收集动作,Major GC的出现经常会伴随着至少一次的Minor GC,但非绝对。Major GC的速度一般会比Minor GC慢十倍以上;
* `大对象直接进入老年代`(典型的大对象是很长的字符串或数组):code中应尽量避免生命周期很短的大对象,经常出现大对象容易导致内存还有很多空间是就提前触发垃圾收集以获取足够的连续空间来分配他们。参数-XX:PretenureSizeThreshold使大于这个值的对象直接进入老年代分配,避免在Eden和Survivor频繁进行复制,这个参数只对Serial收集器和ParNew收集器有效,Parallel Scavenge收集器并不需要设置,如果必须有场景要使用这个参数,可以考虑ParNew + CMS的组合;
* `长期存活的对象将进入老年代`:虚拟机为每一个对象定义了一个对象年龄计数器,从Eden出生经过Minor GC后仍存活,并能被Survivor容纳移动到Survivor空间中,年龄+1;可通过参数-XX:MaxTenuringThreshold设置对象晋升到老年代的年龄阈值;
* `动态对象年龄判定`:如果在Survivor区的所有同一年龄的对象所占的空间达到了Survivor区的一半时,大于等于该年龄的对象可以直接进入老年代;
* `空间分配担保`:在发生Minor GC之前,虚拟机会首先检查老年代的最大可用连续空间是否大于新生代中所有对象的总空间。若大于,则此次Minor GC是安全的,否则虚拟机会检查HandlePromotionFailure设置值是否允许担保失败,如果允许则继续检查老年代的最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试进行一次Minor GC,这时的Minor GC是有风险的;如果小于或者HandlePromotionFailure设置值不允许担保失败,则老年代也会进行一次Full GC,JDK1.6 Update 24之后的规则变为只要老年代的连续空间大于新生代所有的对象的总大小或者历次晋升到老年代的对象的平均大小就会进行Minor GC,否则执行Full GC;

总结

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