您的位置:首页 > 编程语言 > Java开发

Java原理之垃圾回收机制

2018-03-02 21:14 316 查看

1. Java GC - 垃圾回收机制

任何一种GC算法都会发生”stop-the-world”. JVM会因为要执行GC而停止应用的执行,当”stop-the-world”发生时,除了GC所需线程以外,所有线程都处于等待状态,直到GC任务完成.GC优化很多时候就是减少”stop-the-world”发生的时间.

1.1 JVM内存模型

根据JVM规范,JVM内存共分为虚拟机栈,堆,方法区,程序计数器,本地方法栈五部分.如下:



1. 虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建.栈里面存着”栈帧”,每个方法都会创建一个栈帧,存放着局部变量表(基本数据类型和对象引用),操作数栈,方法出口等.栈的大小可以固定也可以动态扩展.当栈的调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,不过这个深度范围不是一个恒定值.

2. 本地方法栈: 主要与虚拟机使用的Native方法相关,Native方法为C/C++程序,不太需要关心.

3. PC寄存器: 也叫程序计数器.JVM支持多个线程同时运行,每个线程都有自己的程序计数器.若当前执行的JVM方法,则该寄存器保存当前执行指令的地址;若执行的native方法,PC寄存器空.

4. : 堆内存是所有JVM线程共享的部分,在虚拟机启动的时候就已经创建了. 所有的对象和数组都在堆上进行分配. 这部分空间可以通过GC进行回收. 当申请不到空间时就会抛出OutOfMemoryError.

5. 方法区: 方法区也是线程共享.主要存储类的信息,常量池,方法数据,方法代码等.逻辑上属于堆的一部分,为了与堆区分,通常又叫”非堆”.

在JDK 1.8中, HotSpot已经没有这个区了,取而代之的是Metaspace(元空间).

1.2 按代的垃圾回收机制

1.1.1 弱年代假设

java中不能显式地分配和注销内存.切记不要显式调用System.gc()来手动的清理内存,严重影响系统性能.Java的垃圾回收机制能够自动寻找不必要的垃圾对象,并清理掉他们.

垃圾回收器会在下面两张假设(hypotheses)成立情况下被创建:

1. 大多数对象会很快变得不可达

2. 只有很少的由老对象(创建时间较长的对象)指向新对象的引用

这些假设称之为弱年代假设(weak generational hypothesis).

1. 新生代(Young generation): 绝大多数最新被创建的对象会被分配到这里. 据IBM统计,98%的对象瞬间消失,2%对象存活较长时间,所以很多对象被创建在新生代,然后消失.对象从这个区域消失的过程称之为minor GC.

如果在年轻带存活足够长时间而没有被清理掉,将会被复制到老年代

-XX:MaxTenuringThreshold: Young GC后存活下来的次数,HotSpot默认15次,大于该值将对象提升到老年代

实际上并不强制要求对象的年龄必须大于MaxTenuringThreshold,如果Survivor中年龄相同的对象所有大小之和超过Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代.

2. 老年代(Old generation): 老年代空间一般比年轻代大,能存放更多对象,在老年代发生的GC次数也比年轻带少.当老年代内存不足时,发生Major GC(或Full GC).

(1) -XX:+UseAdaptiveSizePolicy: 控制是否采用动态控制策略,如果启用,则动态调整java堆中各个区域的大小以及进入老年代的年龄,如果对象比较大(比如长字符串或大数组),Young空间不足,则较大对象直接分配到old上.

(2) -XX: PretenureSizeThreshold: 控制直接升入old的对象大小,大于这个值直接分配到old.

3. 永久代(PermGen): 其实指的是方法区.

永久代的回收主要分为两类: 常量池中的常量,无用的类信息

(1) 常量的回收很简单,没有引用了就可以被回收

(2) 对无用的类进行回收必须保证3点:

1) 类的所有实例都已经被回收

2) 加载类的ClassLoader已经被回收

3) 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)

1.1.2 GC执行详情



新申请对象被分配到 Young, 存活较长的新对象转移到Old. 如果老年代对象需要引用新生代的对象,则在老年代中存在一个”card table”, 512 bytes大小的块, 所有老年代的对象指向新生代对象的引用都会被记录这个表中.当针对新生代执行GC的时候,只需要查询card table来决定是否可被收集,而不用查询整个老年代.这个card table由一个write barrier管理,给GC带来了很大的性能提升,虽然由此可能带来一些开销,但GC时间被显著减少.如下:



1.1.2.1 新生代的Minor GC

新生代用来保存第一次被创建的对象,分为三个空间:

* 一个伊甸园空间(Eden)

* 两个幸存者空间(Survivor)

执行过程如下:

1. 绝大多数刚刚被创建的对象都会存放在Eden,当Eden满的时候,执行Minor GC,将消亡的对象清理掉,剩余的对象复制到Survivor0(此时,Survivor1是空的,两个Survivor总有一个是空的).

2. 此后,每次Eden满了,就执行一次Minor GC,将剩余对象添加到Survivor0

3. 当Survivor0也满的时候,将其中仍然存活的对象直接复制到Survivor1,清空Survivor0

4. 之后每次Eden满的时候,执行Minor GC,Eden中的存活对象复制到Survivor1(此时,Survivor0是空的)

5. 当两个Survivor切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值将对象提升到老年代)之后,仍然存活的对象被复制到Old.

通过频繁的Minor GC将数据移动到老年代的过程可以用如下图描述:



HotSpot虚拟机使用了两种技术来加快内存分配。他们分别是是”bump-the-pointer“和“TLABs(Thread-Local Allocation Buffers)”.

1.1.2.2 不同类型的GC

老年代空间的GC事件基本上是在空间已满时发生,执行的过程根据GC类型的不同而不同,JDK 1.7一共有几种类型的GC,网上有点混乱,我使用如下一种包含较多GC的说法:



其中的问号代表正在开发的Garbage First(G1)收集器.

Serial收集器(-XX:+UseSerialGC): 新生代收集器,stop-the-world, 使用”停止复制算法”, 使用一个线程进行GC,其他线程暂停. -XX:+UseSerialGC默认使用Serial +Serial Old的模式进行内存回收(虚拟机在Clien模式下的默认值)

ParNew收集器(-XX:+UseParNewGC):新生代收集器,stop-the-world, 停止复制算法, Serial收集器的多线程版,用多个线程GC,关注缩短垃圾收集时间. 默认ParNew + Serial Old.使用-XX: ParallelGCThreads来设置执行内存回收的线程数.

Parallel Scavenge收集器(-XX:+UseParallelGC):新生代收集器,stop-the-world,停止复制算法,关注CPU吞吐量(运行用户代码时间/总时间),比如,JVM运行100分钟,其中运行用户代码99分钟,垃圾收集1分钟,则吞吐量99%,这种收集器能最高效利用CPU,适合运行后台计算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适合用户交互,提高用户体验). 高吞吐量能够高效利用CPU时间,尽快完成程序的运算任务,所以程序停顿时间较长,不适合太多交互的任务.

Serial Old收集器: 老年代收集器,stop-the-world, 单线程收集器,标记整理算法(mark-sweep-compact).

1) 第一步 mark(标记): 标记老年代中依然存活的对象

2) 第二步sweep(清理):从头检查堆内存空间,并且只留下依然幸存的对象

3) 第三部compact(压缩): 从头开始,顺序填满堆内存空间,并且将内存空间分为两部分,一个保存着对象,另一个空着.

JDK 1.5之前,Serial Old和Parallel Scavenge搭配使用.

Parallel Old收集器(-XX:+UseParallelOldGC): 老年代收集器,stop-the-world,多线程,多线程机制和Parallel Scavenge差不多,使用标记整理算法(不同于Serial Old, 这里的是mark-Summary-compact. summary汇总的意思是将幸存的对象复制到预先准备好的区域,而不是像sweep那样清理掉废弃的对象),Parallel Old出现后(JDK 1.6), 与Parallel Scavenge配合有很好的效果,充分体现了Parallel Scavenge收集器吞吐量优先的效果.默认Parallel Scavenge + Parallel Old.

CMS(Concurrent Mark Sweep)(-XX:+UseConcMarkSweepGC)收集器:老年代收集器,致力于获取最短的回收停顿时间,使用标记清楚算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小. 默认ParNew + CMS + Serial Old, 优先ParNew +CMS, 用户线程内存不足时( concurrent mode failure),采用备用方案Serial Old收集.



CMS GC最复杂:

1) 标记(initial mark): 只是查找那些距离类加载器最近的幸存对象,停顿时间短暂

2) 并行标记(Concurrent mark): 所有被幸存对象引用的对象会被确认是否已经被追踪和校验, 标记过程中,其他线程依然执行

3) 并发预处理(concurrent preclean): 此阶段主要是进行一些预清理,因为标记和应用线程是并发执行的,因此会有些对象的状态在标记后会改变,此阶段正是解决这个问题,因为之后的Rescan阶段也会stop the world,为了使暂停的时间尽可能的小,也需要preclean阶段先做一部分工作以节省时间

4) concurrent abortable preclean: 加入此阶段的目的是使cms gc更加可控一些,作用也是执行一些预清理,以减少Rescan阶段造成应用暂停的时间

* -XX:CMSMaxAbortablePrecleanTime:当abortable-preclean阶段执行达到这个时间时才会结束

* -XX:CMSScheduleRemarkEdenSizeThreshold(默认2m):控制abortable-preclean阶段什么时候开始执行,

即当eden使用达到此值时,才会开始abortable-preclean阶段

* -XX:CMSScheduleRemarkEdenPenetratio(默认50%):控制abortable-preclean阶段什么时候结束执行

5) 重新标记(remark): 再次检查那些并行标记步骤中增加或者删除的幸存对象引用的对象.

6) 并行交换(concurrent sweep): 转交垃圾回收过程处理.垃圾回收工作和其他线程同时运行.

7) 并发重置(concurrent reset): 收集器做一些收尾的工作,以便下一次GC周期能有一个干净的状态

这种GC类型导致的暂停时间会极其短暂,CMS GC也被称为低延迟GC,经常应用在对于响应时间要求十分苛刻的应用上.

缺点:

+ 比其他GC类型占用更多的内存和CPU

+ 默认情况下不支持压缩,收集结束时会产生大量的空间碎片,空间碎片过多时,将会给大对象分配带来很大的麻烦.

+ 无法处理浮动垃圾(Floating Garbage),可能出现”Concurrent Mode Failure”而导致另一次Full GC. CMS GC线程运行时,用户还会产生新的垃圾,这部分垃圾出现在标记过程之后,CMS无法在本次收集中处理掉,只好留着下次清理,这部分垃圾被称为浮动垃圾.

考虑到CMS GC时不会进行Compact,因此加入:

+ -XX:+UseCMSCompactAtFullCollection : CMS GC后会进行内存的compact

+ -XX:CMSFullGCsBeforeCompaction=4 : 在Full GC 4次后会进行compact

在使用这个GC类型之前需要慎重考虑, 如果因为内存碎片过多而导致压缩任务不得不执行,那么stop-the-world时间要比其他任何GC类型都长,你需要考虑压缩任务发生的频率以及执行时间.

配置一个收集器的情况下,默认会使用的另外一个收集器的对应关系如下:

配置参数新生代收集器老年代收集器
+UseSerialGCSerialSerial Old
+UseParNewGCParNewSerial Old
+UseConcMarkSweepGCParNewCMS + Serial Old
+UseParallelGCParallel ScavengeSerial Old
+UseParallelOldGCParallel ScavengeParallel Old

1.1.3 GC触发条件

1.1.3.1 Minor GC触发条件

当Eden区域分配内存时,发现内存空间不足,JVM就会触发Minor GC,程序中的System.gc()也可以触发.

1.1.3.2 CMS GC触发条件

老年代或持久代已经使用的空间达到设定的百分比时,CMSInitiatingOccupancyFraction这个设置old区,perm区也可以设置.

JVM自动触发(JVM动态策略,也就是悲观策略,基于之前GC的频率以及老年代的增长趋势来评估什么时候开始执行),如果不希望JVM自动决定:

-XX:UseCMSInitiatingOccupancyOnly=true 关闭JVM自动推断.

设置了 -XX:CMSClassUnloadingEnabled , CMS默认不会对永久代就行回收,设置该参数启用.

1.1.3.3 Full GC触发条件

Full GC触发有两种情况,Full gc时,整个应用会停止:

1. concurrent-mode-failure: 当CMS GC正在进行时,此时有新的对象进入Old代,但是Old空间不足. CMS GC运行时需要为用户线程留够足够的内存空间,默认情况下,

-XX:CMSInitiatingOccupancyFraction=68,表示老年代使用了68%的空间后就会触发CMS GC.如果应用中老年代增长不是很快,可以适当提高此参数,以便降低内存回收次数,获取更好的性能.但是过高的值,容易导致CMS GC运行期间预留的内存无法满足程序需要,导致如上错误,触发Full GC,性能反而降低.

2. promotion-failed: 当进行Young GC时,有部分Young对象仍然可用,但是S0或者S1放不下,需要放入Old,但是此时Old无法容纳.

影响CMS GC时长以及触发条件的参数是以下两个参数:

+ -XX:CMSMaxAbortablePrecleanTime=5000

+ -XX:CMSInitiatingOccupancyFraction=80

G1收集器(–XX:+UseG1GC): 不同于之前介绍的所有GC, G1收集器没有年轻代和老年代的概念.堆内存空间被划分许多连续的区域(region).如下图所示:



每个对象被分配到不同的格子,当一个区域装满之后,对象被分配到另一个区域,并执行GC.这中间不再有新生代移动到老年代的三个步骤.这个类型是为了替代CMS GC而被创建的,因为CMS GC在长时间运行时会产生很多问题.G1最大的好处就是性能,比之前介绍的任何一种GC都要快.

各个GC之间的比较示意图如下图所示:



2. 监控Java GC

下列工具以及JVM参数并不适用于所有的JVM,因为GC信息没有强制标准,本文适用HotSpot(Oracle JVM).GC监控根据访问接口不同,分为CUI和GUI两大类.

1. CUI: jstat, 或者启动时候选择JVM参数”verbosegc”

2. GUI: 最常用: jconsole, jvisualvm 和 Visual GC

2.1 jstat

jstat是HotSpot JVM提供的一个监控工具,其他监控工具还有jps和jstatd. jstat不仅提供GC信息,还提供类装载操作信息以及运行时编译器的操作信息.

$> jstat –gc  ${vmid} 1000

S0C       S1C       S0U    S1U      EC         EU          OC         OU         PC         PU         YGC     YGCT    FGC      FGCT     GCT
3008.0   3072.0    0.0     1511.1   343360.0   46383.0     699072.0   283690.2   75392.0    41064.3    2540    18.454    4      1.133    19.588
3008.0   3072.0    0.0     1511.1   343360.0   47530.9     699072.0   283690.2   75392.0    41064.3    2540    18.454    4      1.133    19.588
3008.0   3072.0    0.0     1511.1   343360.0   47793.0     699072.0   283690.2   75392.0    41064.3    2540    18.454    4      1.133    19.588

$>


vmid(虚拟机id),运行在本地机器上的vmid成为lvmid,通常为PID. ps和jps都可以获得pid.

执行”jstat -gc 1000”(或1s)会每隔一秒展示一次GC监控数据. “jstat –gc 1000 10”会每隔一秒展现一次,总共10次. jstat 参数:

参数名称描述
gc输出每个堆区域的当前可用空间以及已用空间(伊甸园,幸存者等等),GC执行的总次数,GC操作累计所花费的时间
gccapactiy输出每个堆区域的最小空间限制(ms)/最大空间限制(mx),当前大小,每个区域之上执行GC的次数。(不输出当前已用空间以及GC执行时间)
gccause输出-gcutil提供的信息以及最后一次执行GC的发生原因和当前所执行的GC的发生原因
gcnew输出新生代空间的GC性能数据
gcnewcapacity输出新生代空间的大小的统计数据
gcold输出老年代空间的GC性能数据
gcoldcapacity输出老年代空间的大小的统计数据
gcpermcapacity输出持久带空间的大小的统计数据
gcutil输出每个堆区域使用占比,以及GC执行的总次数和GC操作所花费的事件
输出对应列的意义如下表所示:

说明jstat参数
S0C输出Survivor0空间的大小。单位KB-gc -gccapacity -gcnew -gcnewcapacity
S1C输出Survivor1空间的大小。单位KB同上
S0U输出Survivor0已用空间的大小。单位KB-gc -gcnew
S1U输出Survivor1已用空间的大小。单位KB同上
EC输出Eden空间的大小。单位KB同S0C
EU输出Eden已用空间的大小。单位KB-gc -gcnew
OC输出老年代空间的大小。单位KB同S0C
OU输出老年代已用空间的大小。单位KB-gc -gcold
PC输出持久代空间的大小。单位KB-gc -gccapacity -gcold -gcoldcapacity -gcpermcapacity
PU输出持久代已用空间的大小。单位KB-gc -gcold
YGC新生代空间GC时间发生的次数-gc-gccapacity-gcnew-gcnewcapacity-gcold-gcoldcapacity-gcpermcapacity-gcutil-gccause
YGCT新生代GC处理花费的时间-gc-gcnew-gcutil-gccause
FGCfull GC发生的次数-gc-gccapacity-gcnew-gcnewcapacity-gcold-gcoldcapacity-gcpermcapacity-gcutil-gccause
FGCTfull GC操作花费的时间-gc-gcold-gcoldcapacity-gcpermcapacity-gcutil-gccause
GCTGC操作花费的总时间-gc-gcold-gcoldcapacity-gcpermcapacity-gcutil-gccause
NGCMN新生代最小空间容量,单位KB-gccapacity -gcnewcapacity
NGCMX新生代最大空间容量,单位KB-gccapacity -gcnewcapacity
NGC新生代当前空间容量,单位KB-gccapacity -gcnewcapacity
OGCMN老年代最小空间容量,单位KB-gccapacity -gcoldcapacity
OGCMX老年代最大空间容量,单位KB
OGC老年代当前空间容量制,单位KB
PGCMN持久代最小空间容量,单位KB-gccapacity -gcpermcapacity
PGCMX持久代最大空间容量,单位KB
PGC持久代当前空间容量,单位KB
PC持久代当前空间大小,单位KB
PU持久代当前已用空间大小,单位KB-gc -gcold
LGCC最后一次GC发生的原因gccause
GCC当前GC发生的原因-gccause
TT老年化阈值。被移动到老年代之前,在新生代空存活的次数-gcnew
MTT最大老年化阈值。被移动到老年代之前,在新生代空存活的次数-gcnew
DSS幸存者区所需空间大小,单位KB-gcnew
假如两次full GC的时间是 67ms,那么其中的一次full GC可能执行了10ms而另一个可能执行了57ms。),平均每次full gc耗时33.5ms. 但是只看平均值无法分析真正GC问题, 为了更好地检测每次GC处理时间,最好使用 –verbosegc来替代数据平均数。

2.2 -verbosegc

-verbosegc是在启动一个Java应用时可以指定的JVM参数之一. jstat可以监控任何JVM应用,即使没有指定任何参数,但是其输出是按照每次设定好的时间. -verbosegc需要在启动的时候指定,每次GC发生时输出,结果为GC前后新生代和老年代空间大小,执行时间,适用于需要了解单次GC的效果.下面是-verbosegc可用参数:

1. -XX:+PrintGCDetails

2. -XX:+PrintGCTimeStamps

3. -XX:+PrintHeapAtGC

4. -XX:+PrintGCDateStamps (from JDK 6 update 4)

如果只是用了-verbosegc,默认会加上 -XX:+PrintGCDetails, -verbosegc参数并不独立,经常组合起来使用,每次GC发生时都会看到如下格式结果:

[GC [<collector>: <starting occupancy1> -> <ending occupancy1>, <pause time1> secs] <starting occupancy3> -> <ending occupancy3>, <pause time3> secs]


collectorminor gc使用的收集器的名字
starting occupancy1GC执行前新生代空间大小
ending occupancy1GC执行后新生代空间大小
pause time1因为执行minor GC,Java应用暂停的时间
starting occupancy3GC执行前堆区域总大小
ending occupancy3GC执行后堆区域总大小
pause time3Java应用由于执行堆空间GC(包括major GC)而停止的时间
参考:

1. http://blog.csdn.net/u013812939/article/details/48782343

2. http://blog.csdn.net/wwwxxdddx/article/details/50981089

3. http://www.importnew.com/1993.html

4. http://alaric.iteye.com/blog/2263682

5. https://www.cnblogs.com/paddix/p/5309550.html

6. https://blogs.oracle.com/jonthecollector/our-collectors
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: