深入了解JVM——垃圾收集器与内存分配策略
Posted 在咖啡里溺水的鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入了解JVM——垃圾收集器与内存分配策略相关的知识,希望对你有一定的参考价值。
本文为 《深入理解Java虚拟机》第三章内容的学习笔记,部分内容经过二次加工。若对相关知识感兴趣,推荐购书深入阅读。若认为文章涉嫌侵权,请联系作者及时删除。
本作品采用 知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议 (CC BY-NC-SA 3.0 CN) 进行许可 。非商业性质转载请注明作者和出处,禁止商业性质转载。
开源创造世界
个人练习代码:https://github.com/dreamerfable/Understanding-the-JVM
如何判断对象已死
引用计数算法
引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;引用失效时减1;任何时刻计数器为0的对象就是不可能再被使用的。
Java虚拟机里没有使用引用计数算法来管理内存,主要的原因是它很难解决对象之间相互循环引用的问题。
即两个对象都相互引用了对方的一个属性,则即使这两个对象都没有其他任何引用,在引用计数算法中也依然会认为这两个对象时存活的。
可达性分析算法
可达性分Reachability Analysis析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链Reference Chain,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。
在Java中,可作为GC Roots的对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
引用
JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用Strong Reference、软引用Soft Reference、弱引用Weak Reference、虚引用Phantom Reference。
- 强引用是指类似
Object obj = new Object()
这类的引用,只要强引用存在,垃圾收集器不会回收强引用对象。 - 软引用用来描述一些还有用但并非必需的对象。系统将要发生内存溢出异常之前,会把软引用对象列进回收范围中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。JDK1.2之后,SoftReference类来实现软引用。
- 弱引用也是用来描述强度比软引用更弱的对象。下一次垃圾回收时,会回收掉弱引用对象。WeakReference类实现软引用
- 虚引用也称为幽灵引用或者幻影引用,是最弱的一种引用关系。一个对象是否有虚引用,不会影响其生存事件,也无法通过虚引用获取一个对象实例。虚引用的作用是,当对象被垃圾回收时,能够受到一个系统通知。PhantomReference用来实现虚引用。
回收方法区
方法区(包括HotSpot虚拟机中的永久代)也是存在垃圾收集的,但Java虚拟机规范中规定不要求虚拟机在方法区实现垃圾回收,并且永久代的垃圾回收相比新生代要效率低的多。
永久代的垃圾收集主要回收废弃常量和无用的类。没有对象引用常量,则在必要的时候这个常量会被垃圾收集掉。
判断一个类是“无用的类”需要同时满足以下三个条件:
- 该类的所有实例都已经被回收,Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
HotSopt虚拟机提供了 -Xnoclassgc
参数控制是否对类进行回收。可以使用 -verbose:class
-XX:+TraceClassLoading
-XX:+TraceClassUnloading
查看类加载和卸载信息。前两个参数在Product版的虚拟机中使用,第三个参数需要FastDebug版的虚拟机支持。
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
垃圾收集算法
标记-清除算法
标记-清除 Mark-sweep 算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
缺点:标记和清除的效率低。标记清除后会产生大量不连续的内存碎片。
复制算法
复制算法 Copying :将肯用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
缺点:将内存缩小为了原来的一半。
现代商业虚拟机都采用这种收集算法回收新生代
Hotspot虚拟机中就将新生代分为 Eden 空间 + 2 * Survivor空间,每次只使用一个Survivor空间,Hotspot虚拟机默认Eden和Survivor的比例是8:1
标记-整理算法
标记-整理 Mark-compact算法:标记出所有需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 适合于老年代的特点。
分代收集
当前商业虚拟机的垃圾收集都采用。
分代收集 Generational Collection 算法:根据对象存活周期的不同将内存划分为几块。再根据新生代和老年代的特点采用最适当的收集算法。新生代采用复制算法,老年代采用标记-清理或标记-整理。
Hotspot的算法实现
枚举根节点
HotSpot的实现中,使用一组称为OopMap的数据结构来保存哪些地方存放着对象的引用,类加载完成时,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。在GC执行可达性分析时,可以直接获得对象引用相关的信息。
在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots枚举。
安全点
HotSpot没有为每条指令都生成OopMap,只在特定位置记录了这些信息,这些位置称为安全点 Safepoint。程序执行时并非在所有地方都能停顿下载开始GC,只有在到达安全点时才能暂停。
在GC发生时让所有线程都跑到最近的安全点上再停顿下来,有两种方案:
- 抢先式中断 Preemptive Suspension 不需要线程的执行代码主动去配合,GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
- 主动式中断 Voluntary Suspension 当GC需要中断线程时,不直接对线程操作,而是设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志位真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外在加上创建对象需要分配内存的地方。
安全区域
对于Sleep状态或Blocked状态的线程,安全点无法解决线程进入GC的问题。
安全区域 Safe Region:在一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。
当线程执行到Safe Region中的代码时,会标识自己已经进入了Safe Region。在这段时间里JVM要发起GC时,就不会去管Safe Region状态的线程了。线程在离开Safe Region时,要检查是否已经完成了GC Root枚举或GC过程。如果完成了,线程继续执行,否则等待。
垃圾收集器
不存在最好的收集器,也没有万能的收集器,不同的应用场景有着各种不同合适的收集器
Serial收集器
Serial收集器是最基本、最悠久的收集器。单线程,GC时需要停止所有工作线程。目前仍然是client模式下JVM的默认新生代收集器。
ParNew收集器
ParNew收集器是Serial收集器的多线程版本,除了使用多个线程进行垃圾收集之外,其他特性与Serial完全相同。
Server模式JVM的首选新生代收集器。
只有Serial和ParNew能够与CMS收集器配合工作。
ParNew收集器是使用-XX:UseConcMarkSweepGC
选项后的默认新生代收集器,也可以使用 -XX:+UseParNewGC
来强制指定。默认开启的收集线程与CPU数量相同,在CPU非常多的情况下,可以使用-XX:ParallelGCThreads
来限制垃圾收集的线程数。
并发 Parallel 指多条垃圾收集线程并行工作,用户线程处于等待状态
并行 Concurrent 指用户线程与垃圾收集线程同时执行,用户程序在继续执行,垃圾收集程序运行在另一个CPU上。
Parallel Scavenge收集器
新生代收集器,使用复制算法,并行的多线程处理器。又称为吞吐量优先收集器。
特点是关注点与其他收集器不同,CMS等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量 Throughput。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
停顿时间短适合与用户交互的程序,良好的相应速度能提升用户体验。高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务
参数
-XX:MaxGCPauseMillis
控制最大垃圾收集停顿时间 允许一个大于0的毫秒数,收集器将尽可能的保证内存回收花费的时间不超过设定值。
-XX:GCTimeRatio
设置吞吐量大小 ,值因当时一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。设置19,允许最大GC时间就占总时间的5% =>> 1/(1+19)
-XX:+UseAdaptiveSizePolicy
开关参数,打开时,不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式成为GC自适应的调节策略 GC Ergonomics。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
Serial Old收集器
Serial收集器的老年代版本,单线程,使用 标记-整理 算法。给Client模式下的JVM使用。Server模式下,主要有两大用途:在JDK 1.5之前与Parallel Scavenge收集器搭配,另一种用途是作为CMS收集器的后备预案,在并发手机发生Concurrent Mode Failure时使用。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和 标记-整理 算法。JDK 1.6之后开始提供。
在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge与Parallel Old收集器
CMS
CMS Concurrent Mark Sweep收集器是一种以获取最短回收停顿时间为目标的收集器。CMS后几期非常适合B/S架构的服务端。又称为 并发低停顿收集器。
基于改良的标记-清除算法,算法分为4个步骤:
- 初始标记 CMS initial mark
- 并发标记 CMS concurrent mark
- 重新标记 CMS remark
- 并发清除 CMS concurrent sweep
初始标记、重新标记需要Stop The World。
缺点
对CPU资源非常敏感。虽然不会导致用户线程停顿,但因为占用了一部分CPU资源所以导致应用程序变慢,总吞吐量会降低。CPU不足4个时,CMS对用户程序的影响会很大。
无法处理 浮动垃圾 Floating Garbage,可能出现 Concurrent Mode Failure 失败而导致另一次Full GC的产生。因为与用户线程并发,所以垃圾收集时又会产生新的垃圾,这部分垃圾只能等下一次GC时再清理,这部分垃圾称为 浮动垃圾。
因为与用户线程并发,所以不能等老年代几乎被填满时再进行GC,需要预留空间提供并发收集时程序运作使用。JDK 1.5默认设置,当老年代使用了68%之后就会激活。可以通过-XX:CMSInitiatingOccupancyFraction
的值提高触发百分比。JDK1.6中阈值已提升至92%。在CMS运行期间预留的内存无法满足程序需要,就会出现一次 Concurrent Mode Failure 失败,这时虚拟机将临时启用Serial Old收集器重新进行老年代的垃圾收集。因此,该参数不宜设置的过高。
因为基于 标记-清除 算法,内存碎片较多。CMS提供了-XX:UseCMSCompactAtFullCollection
开关参数,用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,默认开启。内存整理过程无法并发,空间碎片的问题解决了,但停顿时间将变长。-XX:CMSFullGCsBeforeCompaction
参数可以设置执行多少次不压缩的FullGC后,下一次进行压缩,默认0.
G1收集器
G1 Garbage-First 是一款面向服务端应用的垃圾收集器,其使命在于未来替换掉JDK1.5中的CMS收集器。
特点
- 并行与并发 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短StopTheWorld的停顿事件。
- 分代收集
- 空间整合。整体基于标记-整理算法,局部(两个Region之间)基于复制算法。运行后不会产生碎片,有利于程序长时间运行。
- 可预测的停顿:在降低停顿的基础上,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
G1收集器中Java堆的内存布局与其他收集器差别较大。G1将整个Java堆划分为多个大小相等的独立区域Region,虽然仍保留新生代和老年代的概念,但新生代和老年代不再物理隔离,它们都是一部分Region的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需事件的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remebered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Rememebered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Remebered Set中。当进行内存回收时,在GC 根节点的枚举范围中加入Remebered Set即可保证不对全堆扫面也不会有遗漏。
不计算维护Remebered Set的操作,G1收集器的运作步骤大致为:
- 初始标记 Initial Marking
- 并发标记 Concurrent Marking
- 最终标记 Final Marking
- 筛选回收 Live Data Counting and Evacuation
垃圾收集器参数总结
参数 | 描述 |
---|---|
UseSerialGC | Client模式下的默认值 Serial + SerialOld |
UseParNewGC | ParNew + SerialOld |
UseConcMarkSweepGC | ParNew + CMS + Serial Old 。Serial Old 作为CMS Concurrent Mode Failure失败后的备用 |
UseParallelGC | Server模式下的默认值。Parallel Scavenge + Serial Old (PS Mark Sweep) |
UseParallelOldGC | Parallel Scavenge + Parallel Old |
SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认8。Eden 8 : Survivor 1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,超过这个值时就会进入老年代 |
UseAdaptiveSizePolicy | 动态调整Java堆中各个区域的大小及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应对新生代的整个Eden和Survivor区的所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
GCTimeRatio | GC时间占总时间的比率,默认99,即允许1%的GC时间。仅在使用ParallelScavenge收集器时生效 |
MaxGCpauseMillis | 设置GC的最大停顿事件,Parallel Scavenge收集器生效 |
CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认68%,使用CMS收集器时生效 |
UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。使用CMS生效 |
CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理。使用CMS生效 |
内存分配与回收策略
对象的内存分配,主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下直接分配在老年代中。
对象优先在Eden分配
大多数情况,对象在新生代Eden区分配。Eden区没有空间时触发 Minor GC。虚拟机提供了-XX:+PrintGCDetails
参数,在发生垃圾收集时打印内存回收日志,进程退出时输出当前的内存个区域分配情况。
大对象直接进入老年代
大对象指需要大量连续内存空间的Java对象,比如很长的字符串或数组。程序中应避免短命的大对象。
虚拟机提供了-XX:PretenureSizeThreshold
参数,令大于这个设置值的对象直接在老年代分配。避免在Eden区及Survivor去之间发生大量的内存复制。参数值为字节数。该参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般不需要设置。如果遇到必须使用此参数的场合,可以参考ParNew 加 CMS 的收集器组合。
长期存活的对象将进入老年代
为了识别那些对象应该放在新生代,那些对象放在老年代,虚拟机给每个对象定义了一个对象年龄Age计数器。如果对象在Eden出声并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1.对象在Survivor区中没度过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15)时,就会被晋升到老年代中。该阈值可以通过设置-XX:MaxTenuringThreshold
设置。
动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到MaxTenuringThreshold才能进入老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代下所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置的值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
大部分情况下会将HandlePromotionFailure开关打开,避免Full GC过于频繁。
JDK6u24后,HandlepromotionFailure参数不会再影响到虚拟机的空间分配担保策略。之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行MinorGC,否则将进行Full GC。
以上是关于深入了解JVM——垃圾收集器与内存分配策略的主要内容,如果未能解决你的问题,请参考以下文章