Java垃圾收集器与内存分配策略@zxj
2019-04-16 18:34:56 0 举报
AI智能生成
Java垃圾回收,垃圾收集器,内存分配策略@zxj
作者其他创作
大纲/内容
垃圾收集器
垃圾收集器模型图
如果两个收集器之间存在连线,表示它们可以搭配使用,
上方属于年轻代,下方属于老年代
上方属于年轻代,下方属于老年代
Serial串行收集器
概念
单线程收集器,使用单线程进行垃圾回收
用于新生代,采用复制算法
进行垃圾回收时,必须暂停其他所有工作的线程
优点
简单高效(与其他收集器的单线程比较)
没有线程交互的开销
缺点
Stop The World,需要暂停所有工作线程
单线程,效率和多线程相比不高
应用场景
适用于单CPU或内存等硬件较弱的场合
Serial Old收集器
概念
它是Serial收集器的老年代版本
用于老年代,使用标记整理算法
ParNew收集器
概念
Serial串行收集器的多线程版本,其余行为和Serial收集器完全一致
用于新生代,采用复制算法
优点
除了Serial串行收集器,只有它能与CMS收集器配合工作
在并发能力强的CPU上,停顿要短于串行回收器
Parallel Scavenge收集器
概念
并行的多线程收集器
新生代收集器
使用复制算法
优点
非常注意吞吐量,并提供了两个参数用于精确控制吞吐量大小
Parallel Old收集器
概念
Parallel Scavenge收集器的老年代版本,并行的多线程收集器
老年代收集器
使用标记整理算法
CMS收集器
概念
并发低停顿收集器
默认回收线程数是(CPU数量+3)/4,在CPU不足4个时,CMS对用户程序的影响就变得很大
老年代收集器
标记清除算法
1.初始标记
需要停顿,只是标记一下GC Roots能关联到的对象,速度很快
2.并发标记
进行GC Roots Trancing追踪,耗时较长
3.重新标记
需要停顿,修正并发标记期间发生变动的对象记录
4.并发清除
进行回收工作,耗时较长
优点
并发收集
低停顿
缺点
由于并发设计,会占用CPU资源导致应用程序变慢,吞吐量降低
无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生
由于CMS并发清理阶段用户线程还在运行,所以还会有新的垃圾不断产生,
这部分垃圾在下一次GC时再清理掉,这部分垃圾称为"浮动垃圾"
这部分垃圾在下一次GC时再清理掉,这部分垃圾称为"浮动垃圾"
因为收集线程和用户线程并行的关系,所以需要预留足够的内存空间给用户线程使用,
因此CMS收集器不能像其他收集器那样等到老年代几乎填满后再进行收集,
需要预留一分部空间提供并发收集时的程序使用,jdk1.6配置下,
CMS收集器当老年代使用了92%的空间就会被激活,如果老年代增长不是太快,
可以适当调高参数-XX:CMSInitiatingOccupancyFraction来提高触发百分比,以便降低内存回收次数,
因此CMS收集器不能像其他收集器那样等到老年代几乎填满后再进行收集,
需要预留一分部空间提供并发收集时的程序使用,jdk1.6配置下,
CMS收集器当老年代使用了92%的空间就会被激活,如果老年代增长不是太快,
可以适当调高参数-XX:CMSInitiatingOccupancyFraction来提高触发百分比,以便降低内存回收次数,
如果CMS收集器运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure失败,
这时虚拟机会临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间很长,
所以该参数的值设置过高会导致大量失败,性能降低
这时虚拟机会临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间很长,
所以该参数的值设置过高会导致大量失败,性能降低
基于标记-清除算法实现,会有大量空间碎片产生
该问题导致老年代还有很大空间剩余,但是仍然无法找到足够大的连续空间分配当前对象,导致触发一次Full GC
解决方案:提供了-XX:+UseCMSFullGCsBeforeCompaction开关参数(默认开启),
用于在CMS收集器要进行FullGC时开启内存碎片的合并整理过程,但是空间碎片问题虽然没有了,
但是停顿时间变长,虚拟机还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction
用来设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值0,表示每次Full GC都将要进行碎片整理)
用于在CMS收集器要进行FullGC时开启内存碎片的合并整理过程,但是空间碎片问题虽然没有了,
但是停顿时间变长,虚拟机还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction
用来设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值0,表示每次Full GC都将要进行碎片整理)
G1收集器
特点
并行与并发
G1能充分利用多CPU的优势,缩短Stop The World停顿时间
分代收集
能够独立管理新生代和老年代
原理
使用G1收集器时,java堆的内存布局与其他收集器有很大区别
它将整个java堆划分为多个大小相等的独立区域Region,新生代和老年代不再是物理隔绝的,他们都是一部分Region(不需要连续)的集合
空间整合
G1从整体上来看是基于标记-整理算法实现的收集器,从局部上看是基于复制算法实现的收集器
这两种算法都意味着G1运行期间不会产生内存空间碎片
可预测的停顿
G1除了追求低停顿外,还建立了可预测的停顿时间模型,可以明确指定在N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过M毫秒
原理
G1跟踪各个Region里的垃圾堆积的价值大小,维护了一个优先级列表,优先回收价值最大的Region
步骤
初始标记
需要停顿,只是标记一下GC Roots能关联到的对象,速度很快
并发标记
进行GC Roots Trancing追踪,耗时较长
最终标记
需要停顿,修正并发标记期间发生变动的对象记录,并将记录保存在线程Remembered Set中
筛选标记
首先对各个Region的回收价值和成本进行排序,根据用户设置的GC停顿时间来指定回收计划
垃圾收集器参数总结
与serial串行回收器相关的参数
-XX:+UseSerialGC
新生代使用Serial收集器
老年代使用Serial Old收集器
-XX:+SurivorRatio
设置eden区大小和survivor区大小的比例,默认为8,即8:1:1
-XX:PretenureSizeThreshold
设置大对象直接进入老年代的阀值
当对象的大小超过这个值时,将直接在老年代分配
-XX:MaxTenuringThreshold
设置对象进入老年代的最大年龄值
每一次Minor GC后,对象年龄+1
任何大于这个年龄的对象,一定会进入老年代
与并行GC相关的参数
-XX:+UseParNewGC
新生代使用ParNew并行收集器
老年代使用Serial Old串行收集器
-XX:ParallelGCThreads
指定收集器工作时的线程数,一般与CPU数量相当
-XX:MaxGCPauseMillis=50
设置目标最大GC时间,仅在使用Parallel Scavenge收集器时生效
JVM尽可能(但不保证)停顿时间小于这个时间
默认200,即200ms
-XX:+GCTimeRatio
设置吞吐量大小,仅在使用Parallel Scavenge收集器时生效
0-100整数
默认99,即不超过1%的时间用于垃圾回收
-XX:+UseAdaptiveSizePolicy
打开自适应GC
新生代的大小,eden区和survivor的比例,晋升老年代的对象年龄等参数会被自动调整
在手工调优比较困难的场合,可以直接使用这种自适用的方式,让JVM自己完成调优工作
手动指定
最大堆-Xmx
目标吞吐量-XX:+GCTimeRatio
停顿时间-XX:MaxGCPauseMillis
-XX:+UseParallelGC
新生代使用Parallel Scavenge并行回收
老年代使用Serial Old串行回收
-XX:+UseParalleOldGC
新生代使用Parallel Scavenge并行回收
老年代使用Parallel Old并行回收
与CMS收集器相关的参数
-XX:+UseConcMarkSweepGC
新生代使用ParNew并行收集器
年老代使用CMS收集器
Serial Old收集器将最为CMS出现Concurrent Mode Failure失败后的后备收集器使用
配合用-XX:+UseParNewGc开启新生代并行回收
-XX:ParallelCMSThreads
手动设置CMS并发线程数
-XX:CMSInitiatingOccupancyFraction
设置CMS阀值,超过这个值则执行一次CMS
JDK1.6默认92,即92%
-XX:+CmsFullGcsBeforeCompaction
设置多少次不压缩的Full GC后,进行一次压缩的Full GC
-XX:+CMSClassUnloadingEnabled
允许对类元数据进行回收
-XX:+CMSParallelRemarkEnable
启用并行重标记
-XX:CMSInitatingPermOccupancyFraction
当永久区占用率达到这一百分比后,启动CMS回收
前提是-XX:+CMSClassUnloadingEnabled激活
-XX:UseCMSInitatingOccupancyOnly
只有达到阀值的时候,才进行CMS回收
-XX:+UseCMSCompactAtFUllCollection
CMS完成后,进行内存碎片整理
独占式
-XX:+CMSIncrementalMode
使用增量模式
比较适合单CPU
与G1收集器相关的参数
-XX:+UseG1GC
开启G1收集器
需要配合开启实验性参数
-XX:+UnlockExperimentalVMOptions
开启实验性参数
-XX:GCPauseIntervalMillis=200
设置目标停顿时间
GC日志
-XX:+PrintGCDetails
开启详细GC日志模式
-XX:+PrintGCTimeStamps
在每行GC日志头部加上GC发生时间,这个时间是相当于JVM的启动时间,单位是秒
-XX:+PrintGCDataStamps
在GC日志的每一行加上绝对日期和时间,推荐同时使用这两个参数,这样在关联不同来源的GC日志时很有帮助
-XX:+PrintHeadAtGc
输出GC回收前和回收后的堆消息,使用这个参数可以更好的观察GC对堆空间的影响
-Xloggc
设置GC日志目录
其他
-XX:+DisableExplicitGC
禁止在代码中显示调用GC
System.GC()将无效
对象已死吗?
引用计数算法
给对象中添加一个引用计数器,每当一个地方引用它则计数器++,当引用失效,则--,当计数器为0的对象就不能再被使用
优点:实现简单,判定效率很高
缺点:无法解决循环引用问题,JVM没有选用该算法来管理内存
可达性分析算法
通过GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径为引用链,
当一个对象到GCRoots没有任何引用链相连,也就是从GC Roots到这个对象不可达时,证明此对象是不可用的
当一个对象到GCRoots没有任何引用链相连,也就是从GC Roots到这个对象不可达时,证明此对象是不可用的
JAVA中,可作为GC Roots对象包括以下几种
1.虚拟机栈(栈帧中的本地变量表)中引用的对象(最常用)
2.方法区中的类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈Native方法所引用的对象
引用类型
强引用
object a = new object();垃圾收集器永远不会回收掉被引用的对象
软引用
用来描述还有用但并非必须的对象, 在系统发生OOM之前,将会把这些对象进行回收
弱引用
用来描述非必须对象,被弱引用关联的对象只能生存到下一次GC之前
虚引用
一个对象是否有虚引用存在,完全不会对其生存时间造成影响,为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知
真正死亡
一个对象真正死亡,至少要经历两次标记
1.如果对象在进行可达性分析后发现没有与GC Roots相连接的引用,那么进行第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,如果该对象没有覆盖finalize()方法,则不执行,如果有必要执行,那么该对象会被放置到一个F-Queue的队列中,稍后由指定线程执行
2.后GC对F-Queue中的进行第二次标记,如果对象在finalize()中重新与引用链上的任何一个对象关联,那么在第二次标记时它将从"即将回收"的集合中移除,反之则被回收(不建议使用该方法进行拯救对象,该方法运行代价高昂,不确定性大,finalize能做的所有工作,使用try-finally或其他方式都可以做的更好,建议不使用该方法)
垃圾回收的区域
1.java head堆
在堆中,尤其是在新生代中,一次垃圾收集一般可以回收70%-95%的空间
2.方法区(永久代)
主要回收废弃常量和无用的类
垃圾收集效率远低于hava heap
垃圾收集算法
标记-清除算法
原理
阶段1:标记处所有需要回收的对象
阶段2:统一回收所有被标记的对象
缺点
1.效率问题,标记和清除两个过程的效率都不高
2.空间问题,标记清除之后产生大量不连续的内存碎片,
空间碎片太多会导致以后在需要分配较大的对象时,无法快速找到足够的连续内存而提前触发另一次GC动作
空间碎片太多会导致以后在需要分配较大的对象时,无法快速找到足够的连续内存而提前触发另一次GC动作
复制算法
原理
将可用内存划分为大小相等的两块
每次只使用一块,当内存用完,就将存活的对象复制到另一块内存上,并将之前使用的那块内存清理掉
每次都对整个半区进行内存回收,内存分配时不需要考虑内存碎片等复杂情况
缺点
将内存缩小为原来的一半,过于浪费
当对象存活率较高时,需要进行很多的复制操作,效率较低
由于老年代种对象存活率很高,所以老年代中一般不能直接选用这种算法
优点
回收后的内存连续,内存分配时不需要考虑内存碎片等复杂情况
改进
新生代分为一块Eden区和两块较小的Survivor区(也称为From和To),内存大小比例为8:1:1
每次使用Eden区和其中一块Survivor区
当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,
然后清理掉Eden和刚才用过的Survivor区
然后清理掉Eden和刚才用过的Survivor区
标记-整理(压缩)算法
原理
先对可回收的对象进行标记
然后让所有存活的对象都向一端移动
最后直接清理掉边界以外的内存
优点
避免了碎片的产生
不需要两块相同的内存空间
分代收集算法
思想
根据对象存活周期的不同,把Java堆分为新生代和老年代
根据各个年代的特点采用最适合的收集算法
应用
新生代
在新生代中,每次垃圾收集都会有大量对象死去,只有少量存活
使用复制算法
老年代
在老年代中,对象存活率高,没有额外空间对它进行分配担保
使用标记清理或标记整理算法
HotSpot算法实现
枚举根节点
概念
可达性分析中会从GC Roots节点中找引用链,可作为GC Roots的节点主要在全局性的引用(常量或类静态属性)
与执行上下文中(例如栈帧中的本地变量表)中,现在很多引用仅仅方法区就有数百兆,如果要逐个检查引用,必然会消耗很多时间
与执行上下文中(例如栈帧中的本地变量表)中,现在很多引用仅仅方法区就有数百兆,如果要逐个检查引用,必然会消耗很多时间
可达性分析工作必须在一个确保一致性的快照中进行,不可以出现分析过程中对象引用关系还在不断变化的情况
这将导致GC进行时必须停顿所有Java执行线程(该事件称为Stop The World),即使是好处不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的
原理
主流jvm使用的都是准确式GC,当停顿发生时,并不需要一个不漏的检查所有应用
在HotSpot实现中,使用一组称为OopMap的数据结构达到这个目的,在类加载完成的时候,就把相应的数据计算出来
这样GC在扫描时就可以直接得知这些信息了,在OopMap的协助下,快速准确完成GC Roots枚举
这样GC在扫描时就可以直接得知这些信息了,在OopMap的协助下,快速准确完成GC Roots枚举
安全点
概念
HotSpot没有为每条指令都生成OopMap,只在特定的位置记录信息,这些位置称为安全点,
即程序执行时并非在所有地方都能停下来开始GC,只有在到达安全点才能暂停
即程序执行时并非在所有地方都能停下来开始GC,只有在到达安全点才能暂停
原理
抢先式中断
首先把所有线程中断,发现有线程中断位置不在安全点,就恢复线程,让它跑到安全点
冷门方案
主动式中断
不对线程操作,设置一个标志,线程主动轮询该标志,发现中断标记时线程就自动挂起
常用方案
安全区域
概念
指在一段代码片段之中,引用关系不会发生变化,在该区域的任意地方开始GC都是安全的
GC日志分析
每种垃圾收集器的日志都是它们自己定义的,以上为两段典型的GC日志
33.125是GC开始时间,该时间是相当于JVM启动时间,单位是秒
GC日志开头的GC和Full Gc表示本次垃圾收集的停顿类型
Full Gc说明本次GC发生了Stop-The-World(全部Java线程暂停),
如果是调用了System.gc()方法所触发的收集,那么在这里将显示Full Gc(System)
如果是调用了System.gc()方法所触发的收集,那么在这里将显示Full Gc(System)
DefNew,Tenured,Perm表示GC发生的区域,
这里显示的区域名称与使用的GC收集器是有关的
这里显示的区域名称与使用的GC收集器是有关的
使用Serial收集器中的新生代名为Default New Generation,所以显示的是DefNew.
3324k->152K(3712k)表示GC前该内存区域已使用3324k->GC后该内存区域已使用152k(该内存区域总容量3712K)
3324K->152K(11904K)表示GC前java堆已使用3324K,GC后java堆已使用152k(java堆总容量11904K)
0.0025925 secs表示该内存区域GC所占用的时间,单位是秒
内存分配与回收策略
对象优先在Eden分配
大多数情况下,对象在新生代的Eden区分配
触发Minor GC(新生代GC)的条件
1.当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
2.当新生代可用空间不足时,会触发Minor GC
新生代可用空间=Eden区+1个Survivor区
分配担保机制
概念
触发Minor GC后,如果当前生存的对象无法放置到一个Survivor区中,则会通过分配担保机制将对象转移到老年代中
工作原理
1.在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
如果条件成立,那么本次Minor GC确保是安全的
如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败
大部分情况下设置为允许担保失败,避免频繁Full GC
在JDK6之后,该参数弃用,默认允许担保失败
2.如果不允许担保失败
则进行一次Full GC
3.如果允许担保失败,会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小
如果大于,将尝试着进行一次Minor GC
如果小于,则进行一次Full GC
4.如果担保失败
如果某次Minor GC后存活的对象激增,远远高于平均值,可能会导致分配担保失败
担保失败后,进行一次Full GC
大对象直接进入老年代
大对象指需要大量连续内存空间的Java对象
最典型的大对象就是很长的字符串和数组
大对象容易导致内存还有很多剩余空间就提前触发垃圾回收动作
-XX:PretenureSizeThreshold参数,让大于该值的对象直接在老年代分配
该参数只对Serial和ParNew两个收集器有效
好处
1.避免在Eden区及两个Survivor区之间发生大量内存的复制
2.避免内存还有很多剩余空间时,就提前触发垃圾回收动作,影响性能
长期存活的对象进入老年代
虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden区出生并经过第一次Minor GC后仍然存活,
并且能被Survivor容纳的话,将被移动到Survivor区,年龄设置为1
并且能被Survivor容纳的话,将被移动到Survivor区,年龄设置为1
对象在Survivor区每经历过一次Minor GC,年龄增加1岁,当年龄增加到一定岁数(默认15岁),将会晋升到老年代中
该年龄阀值可以通过参数-XX:MaxTenuringThreshold设置.
动态对象年龄判定
虚拟机并不是永远要求对象年龄到达阀值才能晋升老年代
如果在survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,
大于或等于该年龄的对象可以直接进入老年代
大于或等于该年龄的对象可以直接进入老年代
收藏
收藏
0 条评论
下一页