如上图,给对象一个引用计数器refCount。每有一个对象引用它,计数器加1,当refCount=0的时候,表示对象不再可用。
缺点
很难解决循环引用的问题:
1 2 |
objA.instance = objB; objB.instance = objA; |
如上,即使 objA 和 objB 都不再被访问之后,他们依旧互相引用这,所以计数器不为0。
如上图,从GC Roots开始向下搜索,连接的路径为引用链;
GC Roots不可达的对象被判为不可用;
可作为 GC Root的对象
如上图,虚拟机栈帧中本地变量表引用的对象,本地方法栈中,JNI引用的对象,方法区中的类静态属性引入的对象和常量引用对象都可以作为 GC Root。
**强引用:**类似Object a = new Object();
软引用:SoftReference
,OOM前,JVM会把这些对象列入回收范围进行二次回收,如果回收后内存还是不做,则OOM。软引用SoftReference介绍以及简单用法cache
弱引用:WeakReference
,每次垃圾收集,弱引用的对象就会被清理。
虚引用:PhantomReference
,幽灵引用,不能用来取得一个对象的实例,唯一用途:当一个虚引用引用的对象被回收,系统会受到这个对象被回收了的通知。利用虚引用PhantomReference实现对象被回收时收到一个系统通知
第一小节流程图里面的是否存在与GC Roots相连接的引用链
这个判断子流程是怎么实现的呢,这节我们来仔细探讨下。
一般的,我们都是选取可达性分析算法,这里主要阐述怎么寻找GC Root以及如何检查引用链。
如上图,在一个调用关系为:
ClassA.invokeA() --> ClassB.invokeB() --> doinvokeB() -->ClassC.execute()
的情况下,每个调用对应一个栈帧
,栈帧里面的本地变量表存储了GC Roots的引用。
如果直接遍历所有的栈去查找GC Roots,效率太低了。为此我们引入了OopMap和安全点的概念。
安全点和OopMap
如上图,在源代码编译的时候,会在特定位置下记录安全点,一般为:
通过安全点把代码分成几段,每段代码一个OopMap。
OopMap记录栈上本地变量到堆上对象的引用关系,每当触发GC的时候,程序都都先跑到最近的安全点然后自动挂起,然后再触发更新OopMap
,然后进行枚举GC ROOT,进行垃圾回收:
安全区域
:在一段代码片段之中,引用关系不会发生变化,因此在这个区域中的任意位置开始 GC 都是安全的。如处于Sleep或者Blocked状态的线程。
为了在枚举GC Roots的过程中,对象的引用关系不会变更,所以需要一个GC停顿。
还有一种抢先式中断的方式,几乎没有虚拟机采用:先中断所有线程,发现线程没中断在安全点,恢复它,继续执行到安全点。
找到了该回收的对象,下一步就是清掉这些对象了,HotSpot将去交给CG收集器,详细见后续小节说明。
概览图
还没想到
新生代
,可以根据实际情况,将内存块大小比例适当调整;如下图,分为新生代和老年代。其中新生代又分为一个Eden区和两个Survivor去(from区和to区),默认Eden : from : to 比例为8:1:1
。
可通过JVM参数:-XX:SurvivorRatio
配置比例,-XX:SurvivorRatio=8
表示 Eden区大小 / 1块Survivor区大小 = 8
。
第一次YOUNG GC
当Eden区满的时候,触发第一次Young GC,把存活对象拷贝到Survivor的from区,清空Eden区。
第二次YOUNG GC
再次触发Young GC,扫描Eden区和from区,把存活的对象复制到To区,清空Eden区和from区。如果此时Survivor区的空间不够了,就会提前把对象放入老年代。
默认的,这样来回交换15次后,如果对象最终还是存活,就放入老年代。
交换次数可以通过JVM参数
MaxTenuringThreshold
进行设置。
JDK8 之前
JDK8
如上图,JDK8的方法区实现变成了元空间,元空间在本地内存中。
JVM内存相关参数:
JVM Parameters
内存分配如何保证并发?
当前商业虚拟机都采用该算法。
新生代
:复制算法(CG后只有少量的对象存活)老年代
:标记-整理算法 或者 标记-清理算法(GC后对象存活率高)这一步就是我们真正进行垃圾回收的过程了。
本节概念约定:
并发
:用户线程与垃圾收集线程同时执行,但不一定是并行,可能交替执行;
并行
:多条垃圾收集线程并行工作,单用户线程仍处于等待状态。
以下是垃圾收集器概览图
串行化
:在垃圾回收时,必须赞同其他所有工作线程,知道收集结束,Stop The World
;
在单CPU模式下无线程交互开销,专心做垃圾收集,简单高效。
Client模式
下的默认新生代收集器,用户桌面应用场景分配给虚拟机的内存一般不会很大,所以停顿时间也是在一百多毫秒以内,影响不大。Parallel New?
多线程版本
;Server模式
下的虚拟机中的首选新生代收集器;CMS收集器
搭配使用。
-XX:+UseConcMarkSweepGC
选型默认使用ParNew收集器。也可以使用-XX:+UseParNewGC
选项强制指定它。ParNew收集器在单CPU环境比Serial收集器效果差(存在线程交互开销)。
CPU数量越多,ParNew效果越好,默认开启收集线程数=CPU数量。可以使用
-XX:ParallelGCThreads
参数限制垃圾收集器的线程数。
吞吐量优先收集器
:CMS等收集器会关注如何缩短停顿时间,而这个收集器是为了吞吐量而设计的。吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )
也就是说整体垃圾收集时间越短,吞吐量越高。
-XXMaxGCPauseMillis
:设置最大垃圾收集停顿时间,大于0的毫秒数;
缩短GC停顿时间会牺牲吞吐量和新生代空间。新生代空间小,GC回收就快,但是同时会导致GC更加频繁,整体垃圾回收时间更长。
-XX:GCTimeRatio
:设置吞吞量大小。0~100的整数,垃圾收集时间占总时间的比率,相当于吞吐量的倒数。
-XX:+UseAdaptiveSizePolicy
:GC自适应调节策略开关,打开开关,无需手工指定-Xmn
(新生代大小)、-XX:SurvivorRatio
(Eden与Survivor区比例)、-XX:PretenureSizeThreshold
(晋升老年代对象年龄)等参数,虚拟机会收集性能监控信息,动态调整这些参数,确保提供最合适的 停顿时间或者最大吞吐量。
Serial收集器的老年代版本。使用单线程,标记-整理算法。
Parallel Scavenge收集器的老年代版本,使用多线程,标记整理算法。
JDK1.6之后提供,之前Parallel Scavenge只能与Serial Old配合使用,老年代Serial Old无法充分利用服务器多CPU处理器能力,拖累了实际的吞吐量,效果不如ParNew+CMS组合;
Concurrent Mark Sweep
浮动垃圾
。在CMS并发清理阶段,用户线程会产生垃圾。如果出现Concurrent Mode Failure失败,会启动后备预案:临时启动Serial Old收集器重新进行老年代垃圾收集,停顿时间更长了。-XX:CM SInitiatingOccupancyFraction
设置的太高容易导致这个问题;-XX:+UseCMSCompactAtFullCollection
:在CMS要进行Full GC时进行内存碎片整理(默认开启)。内存整理过程无法并发,会增加停顿时间;-XX:CMSFullGCsBeforeCompaction
:在多少次 Full GC 后进行一次空间整理(默认0,即每一次 Full GC 后都进行一次空间整理);-XX:CM SInitiatingOccupancyFraction
:触发GC的内存百分比,设置的太高容易导致Concurrent Mode Failure失败(GC过程中,用户线程新增的浮动垃圾,导致触发另一个Full GC)。CMS为什么要采用
标记-清除算法
?CMS主要关注低延迟,所以采用并发方式清理垃圾,此时程序还在运行,如果采用压缩算法,则会涉及到移动应用程序的存活对象,这种场景下不做停顿是很难处理的,一般需要停顿下来移动存活对象,再让应用程序继续运行,但是这样停顿时间就边长了,延迟变长。CMS是容忍了空间碎片来换取回收的低延迟。
G1:Garbage-First,即优先回收价值最大的Region(注1)。
注1:G1与收集器将整个Java堆换分为多个代销相等的独立区域,跟踪各个Region里面的垃圾堆积的价值大小,优先回收价值最大的Region。
如上图,G1收集器分为四个阶段:
初始标记
:只标记GC Roots能直接关联到的对象,速度很快。并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能够在正确可用的Region中创建新对象,这阶段需要停顿线程;并发标记
:GC RootsTracing过程。该阶段对象变化记录在线程Remembered Set Logs中。最终标记
:修正并发期间因用户程序运作而导致标记产生变动的部分对象的标记记录。把Remembered Set Logs数据合并到Remembered Set中。这个阶段需要停顿,但是可并行执行;筛选回收
:对各个Region回收价值和成本进行排序,根据用户期望Gc停顿时间制定回收计划。与CMS不一样,这里不用和用户线程并发执行,提高收集效率,使用标记-整理算法,不产生空间碎片。并行与并发
:并发标记,并行最终标记与筛选回收;分代收集
空间整合
:基于标记-整理算法,不会产生碎片。可预测的停顿
:G与收集器将整个Java堆换分为多个代销相等的独立区域,避免在整个Java堆中进行全区域的垃圾回收,跟踪各个Region里面垃圾堆积的价值大小,后台维护一个优先列表,每次根据运行的收集时间,优先回收价值最大的Region。为什么CMS两次标记时要 stop the world
[垃圾收集 (GC)](https://github.com/TangBean/understanding-the-jvm/blob/master/Ch1-Java%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E6%9C%BA%E5%88%B6/02-%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86(GC).md
How Does Reference Counting Garbage Collection Work
软引用SoftReference介绍以及简单用法cache
利用虚引用PhantomReference实现对象被回收时收到一个系统通知
java垃圾回收之复制算法
请问 JVM线程的栈在64位Linux操作系统上的默认大小是多少?
虚拟机OopMap
Understanding Java Garbage Collection Algorithms
Mark-and-Sweep: Garbage Collection Algorithm