GC——垃圾回收
1、可回收对象(垃圾)的判定
(1)引用计数法
对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何只针对其引用,它就是垃圾。
弊端:如果AB相互持有引用,会导致永远不能被回收。
(2)可达性分析
通过GC Root的对象,开始向下寻找,看某个对象是否可达 。可作为GC Root的对象:
虚拟机栈(栈帧中的本地变量表)中的引用对象;活跃线程中的本地变量引用的对象,以及正在执行的方法中的参数对象
方法区中类静态属性引用的对象;类加载时,静态变量引用的对象
方法区中常量引用的对象;常量池中引用的对象
本地方法栈中引用的对象:JNI创建的全局引用
(3)引用类型
判断对象存活都与引用有关,引用分为四种类型:
* 强引用:在程序中普遍存在,如“Object object = new Object()”这类的引用。只要强引用存在,对象就不能被回收
* 软引用:还有用但非必须的对象。对于软引用关联的对象,在系统内存溢出之前,会对这些对象进行回收,如果回收后内存还不足,才会抛出内存溢出异常。通过SoftReference类实现软引用。
* 弱引用:还有用但非必须的对象。强度比软引用更弱一些,只能存活到下一次下一次GC发生之前。当GC时,如果一个对象只被弱引用关联,则会被回收。通过WeakReference类实现弱引用。
例子:ThreadLocal中的内部类Entry就是使用的WeakReference。
* 虚引用:最弱的一种引用,不会对被关联对象的生存产生任何影响,也不能通过虚引用获取实例对象。为一个对象设置虚引用关联的唯一目的就是在对象被回收时会受到一个系统通知。
2、垃圾回收算法
(1)标记—清除(Mark-Sweep)
* 标记
找出内存中需要回收的对象,并且把他们标记出来。此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时。
* 清除
清除掉被标记需要回收的对象,释放出对应的内存空间
缺点:——标记和清除两个过程都比较耗时,效率不高
——会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无 法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
(2)复制(Copying)
将内存划分为两块相同的区域,每次只使用其中一块。
当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次 清除掉。
缺点:空间利用率降低。
(3)标记—整理(Mark-Compact)
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活 的对象都向一端移动,然后直接清理掉端边界以外的内存。
3、分代收集算法
根据对象存活周期不同,将内存划分为不同区域,采用适当的回收算法。一般分为新生代(Young区)和老年代(Old区)。新生代进行垃圾回收时存活对象较少,适合复制算法,只需要付出少量存活对象的复制成本就能完成收集。
而老年代对象存活率高,没有额外空间对它进行分配担保,就必须采用“标记-清理”或“标记-整理”算法来进行回收。
4、垃圾收集器
垃圾收集器就是内存回收的具体实现。
(1)Serial收集器
Serial收集器是最基本、发展历史最悠久的收集器,曾经(JDK1.3)是虚拟机新生代收集的唯一选择。它是一种单线程收集器,不仅意味着它只会使用一个CPU或是一条收集线程去完成垃圾收集工作,更重要的是其在进行
垃圾收集的时候需要暂停其他线程。
优点:简单高效,拥有很高的单线程收集效率。
缺点:收集过程需要暂停所有线程。
算法:复制算法。
使用范围:新生代。
应用:Client模式下的默认新生代收集器。
(2)ParNew收集器
Serial收集器的多线程版本。
优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差(因为在单CPU会增加额外的线程上下文切换开销)。
算法:复制算法。
使用范围:新生代。
应用:运行在Server模式下的虚拟机中首选的新生代收集器。
(3)Parallel Scavenge收集器
Parallel Scavenge收集器是并行的多线程收集器,相比ParNew,它更关注系统的吞吐量。此处吞吐量=运行用户代码的时间 / (运行用户代码时间 + 垃圾收集时间),如:虚拟机总共运行100分钟,垃圾收集用了1分钟
吞吐量 = (100 - 1)/ 100 = 99%
吞吐量越大,意味着垃圾收集的时间越短,程序代码可以更充分利用CPU资源。
-XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
-XX:GCTimeRatio 设置吞吐量大小
(4)Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,采用“标记-整理”算法,运行过程和Serial收集器一样。
(5)Parallel Old收集器
Parallel Old收集器是Parallel收集器的老年代版本,使用多线程额“标记-整理”算法进行垃圾回收,吞吐量优先。
(6)CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以最短回收停顿时间为目标的收集器,采用“标记-清除”算法,整个过程分为4步:
——init-mark初始标记(STW,stop the world),该阶段进行可达性分析,标记GC Root能关联到的对象(速度很快)
——concurrent-mark并发标记,由前阶段标记过的绿色对象出发,所有可到达的对象都在本阶段中标记。
——remark重新标记(STW),暂停所有用户线程,重新扫描堆中的对象,进行可达性分析,标记活着的对象。因为并发标记阶段是和用户线程并发执行的过程,所以该过程中可能有用户线程修改某些活跃对象的字段,指向了一个未标记过的对象,如下图中红色对象在并发标记开始时不可达,但是并行期间引用发生变化,变为对象可达,这个阶段需要重新标记出此类对象,防止在下一阶段被清理掉,这个过程也是需要STW的。特别需要注意一点,这个阶段是以新生代中对象为根来判断对象是否存活的
——并发清除,进行并发的垃圾清理
由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收是与用户线程一起并发地执行的。
优点:并发收集,低停顿。
缺点:因为使用标记-清除算法,会产生大量空间碎片,并发阶段会降低吞吐量。
使用范围:老年代。
(7)G1收集器
特点:
* 并行和并发(对二者的理解:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生)。
* 分代收集(仍保留了分代的概念)。
* 空间整合,整体上属于“标记-整理”算法,不会导致空间碎片。
* 可预测的停顿。相比CMS,更先进的地方在于能让用户指定在M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是
一部分Region(不需要连续)的集合。
工作过程可以分为以下几步:
——初始标记,标记GC Root能关联的对象,并且修改TAMS的值,需要暂停用户线程。
——并发标记,从GC Root进行可达性分析,找出存活的对象,与用户线程并发执行。
——最终标记,修正再并发标记阶段因用户程序并发执行导致的变动的数据,需暂停用户线程。
——筛选回收,对各个Region的回收价值和回收成本进行排序,根据用户所期望的GC停顿时间制定回收计划。(相对来讲更加智能化)
对于G1的理解:
JDK7开始使用,JDK8非常成熟,JDK9默认的垃圾收集器,适用于新老生代。判断是否需要使用G1收集器:
* 50%以上的堆被存活对象占用
* 对象分配和晋升的速度变化非常大
* 垃圾回收时间比较长
垃圾收集器分类:
* 串行收集器->Serial和Serial Old
只能有一个垃圾回收线程执行,用户线程暂停。 适用于内存比较小的嵌入式设备 。
* 并行收集器(吞吐量优先)->Parallel Scavenge、Parallel Old
多条垃圾收集线程并行执行,但此时用户线程仍然处于等待状态。适用于科学计算、后台处理等弱交互场景。
* 并发收集器(停顿时间优先)->CMS、G1
用户线程和垃圾收集线程同时执行(并不一定是并行的,可能是交替执行的),垃圾收集线程在执行时不会停顿用户线程的运行。适用于相对时间有要求的场景,比如web。
理解吞吐量和停顿时间:
* 停顿时间:垃圾收集器进行垃圾回,终端应用执行相应的时间
* 吞吐量:运行用户代码的时间 / (运行用户代码时间 + 垃圾收集时间)
停顿时间越短,越适合需要和用户交互的程序,良好的响应速度能提升用户体验;高吞吐量则可以使应用程序高效地利用CPU时间,尽快完成程序的运算任务,主要适合再后台运算而不需要太多交互的任务。
这两个指标也是评价垃圾回收器好处的标准,调优也主要是观察这两个变量。
如何选择合适的垃圾回收器:
* 优先调整堆的大小让服务器自己来选择
* 如果内存小于100M,使用串行收集器
* 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
* 如果允许停顿时间超过1秒,选择并行或JVM自己选
* 如果响应时间最重要,并且不能超过1秒,使用并发收集器
5、GC发生的时机
GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。可以通过System.gc()通知JVM进行一次垃圾回收,但具体什么时候执行由JVM决定,但是调用该方法资源消耗较大,不建议使用。
一般以下几种情况会发生GC:
* 当Eden区或者Survivor区不够用了
* 老年代空间不够用了
* 方法区空间不够用了
* System.gc()
6、对minor GC、major GC和FULL GC的理解
minor GC发生在新生代,major GC发生在老年代(通常和Full GC等价,只有CMS的concurrent collection是只清理老年代这个模式),Full GC是清理整个堆空间。《深入理解java虚拟机》中给出的GC日志中的FULL GC是:Full GC不是用来区分新生代GC和老年代GC,而是表示这次垃圾收集的停顿类型,而Full则表示此次GC停顿了所有其他线程(Stop-The-World)。
minor GC触发条件:当Eden区分配满的时候触发。
Full GC触发条件:
* 程序执行了System.gc() //建议jvm执行fullgc,并不一定会执行
* 执行了jmap -histo:live PID命令 //立即触发full gc
* 在执行minor gc的时候进行一系列检查:
在发生minor gc之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,大于则minor gc是安全的。小于的话,则查看HandlePromotionFailure设置的值是否允许失败担保。如果允许,会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次minor gc,虽然此次minor gc有风险;如果小于,或者HandlePromotionFailure设置不允许冒险,这时要进行full gc。流程如下:
具体的GC触发条件根据收集器略有差异,如Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)。
7、查看GC状态
通过 ps -ef | grep java 来查看Java进程ID。(或 jps :查看所有Java进程)
然后通过 jstat -gc 1 5 10 (1-进程ID,5-每5ms打印一次,10-输出10次) 来查看GC情况
参数说明:
S0C:年轻代中第一个survivor(幸存区)的容量 (字节)
S1C:年轻代中第二个survivor(幸存区)的容量 (字节)
S0U:年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
S1U:年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
EC:年轻代中Eden(伊甸园)的容量 (字节)
EU:年轻代中Eden(伊甸园)目前已使用空间 (字节)
OC:Old代的容量 (字节)
OU:Old代目前已使用空间 (字节)
PC:Perm(持久代)的容量 (字节)
PU:Perm(持久代)目前已使用空间 (字节)
YGC:从应用程序启动到采样时年轻代中gc次数
YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)
FGC:从应用程序启动到采样时old代(全gc)gc次数
FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)
GCT:从应用程序启动到采样时gc用的总时间(s)
使用:jmap -heap pid查看使用的垃圾回收器类型和堆栈状态
JVM堆内存参数设置:
- -XX:+PrintFlagsInitial 查看所有的参数的默认初始值
- -XX:+PrintFlagsFinal 查看所有参数的最终值
- jinfo -flag SurvivorRatio pid
- -Xms 初始堆空间内存(默认为物理内存的1/64)
- -Xmx 最大堆空间内存(默认为物理内存的1/4)
- -Xmn 设置新生代的大小(初始值及最大值)
- -XX:NewRatio 配置新生代与老年代在堆结构的占比(默认1:2)
- -XX:SurvivorRatio 设置新生代中Eden和S0/S1空间的比例
- -XX:MaxTenuringThreshold 设置新生代垃圾的最大年龄
- -XX:PrintGCDetails 输出GC详细日志
- -XX:+PrintGC -verbose:gc 打印GC简要信息
- -XX:HandlePromotionFailure 是否设置空间分配担保(JDK7后不再起作用)
8、案例分析
频繁的发生GC对于系统影响并不大,但如果GC时间长就会造成系统卡顿,所以对于内存较大的机器,可以使用G1垃圾回收器来限制GC执行时间来避免一次回收时间过长。