对象被垃圾回收的时机
垃圾回收主要面向的是堆中的对象。简单一句就是:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法。
引用计数法: 一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收,如图:
但是,当对象间出现了循环引用的话,引用计数就会失效,如图——当运行完红色框中的代码时,a,b的引用值为2:
当将a,b设置为null后,栈中的变量a,b不再引用堆中的两个内存,此时对象a,b的引用数量为1(因为对象之间存在循环引用):
此时,堆中的对象a,b不会再被使用,但由于引用数量不为0,也不会被回收,从而引发内存泄漏。
可达性分析算法
可达性分析算法是指扫描堆中的对象,看是否能够沿着 GC Root 对象 为起点的引用链找到该对象,找不到,表示可以回收。
如图,X,Y这两个节点是可回收的(它们没有和GC Root相关联):
现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。
可以作为GC Root的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
JVM垃圾回收算法
垃圾回收算法:
- 标记清除算法
- 复制算法
- 标记整理算法
标记清除算法
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
- 根据可达性分析算法得出的垃圾进行标记
- 对这些标记为可回收的内容进行垃圾回收
如图:
优点:标记和清除速度较快
缺点:碎片化较为严重,内存不连贯的
标记整理算法 标记清除算法一样,将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低。
解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。
复制算法 将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收;无碎片,内存使用率低。
优点:
- 在垃圾对象多的情况下,效率较高
- 清理后,内存无碎片
缺点:分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低。
JVM的分代回收
在java8时,堆被分为了两份:新生代和老年代【1:2】,如图:
对于新生代,内部又被分为了三个区域:
- 伊甸园区Eden,新生的对象都分配到这里
- 幸存者区survivor(分成from和to, from和to是相对状态)
- Eden区,from区,to区【8:1:1】
分代回收算法-工作机制:
- 新创建的对象,都会先分配到eden区
- 当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象
- 将存活对象采用复制算法复制到to中,复制完毕后,伊甸园和 from 内存都得到释放
- 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将其复制到from区
- 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会提前晋升)
如图,假设在Eden区被划分完后的状态如下图所示:
此时,伊甸园内存不足,标记伊甸园和from区的存活对象(对象A);将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放,同时from和to发生交换,如下图:
如果,伊甸园再次内存不足:
重复操作2,3后,此时的内存状态为(复制到to之后,from和to发生交换):
随后,再次发生内存不足:
当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升):
ps:在看上面的过程的时候,可能对from和to的相关操作有些疑惑(一会儿的是to一会儿又是from的),其实他们只是一个名字而已,实际就是两块内存,我们可以认为,在进行扫描时,内存中带有数据的为from,即将把数据复制到对应的内存为to。
MinorGC、 Mixed GC 、 FullGC的区别:
- MinorGC【young GC】发生在新生代的垃圾回收,暂停时间短(STW)
- Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
- FullGC: 新生代 + 老年代完整垃圾回收(新生代和老年代内存严重不足时),暂停时间长(STW),应尽力避免
STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成。
JVM的垃圾回收器
在jvm中,实现了多种垃圾收集器,包括:
- 串行垃圾收集器
- 并行垃圾收集器
- CMS(并发)垃圾收集器
- G1垃圾收集器
串行垃圾收集器
Serial和Serial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑
- Serial 作用于新生代,采用复制算法
- Serial Old 作用于老年代,采用标记-整理算法
垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
并行垃圾收集器
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器
- Parallel New作用于新生代,采用复制算法
- Parallel Old作用于老年代,采用标记-整理算法
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
CMS(并发)垃圾收集器
CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行。
初始标记——标记跟GC Root直接关联的对象;
并发标记——追踪引用链,探索与初始标记后的对象相关联的对象;
重新标记——由于在并发标记阶段,其它线程是可以继续执行的,所以可能出现新的引用或减少了引用,所以需要重新标记。
举例:
在上图中,各个标记阶段的标记对象为:
- 初始阶段:对象A
- 并发标记:对象B,C,D
若在并发阶段A指向的对象X:
因此,重新标记阶段:对象C。
G1垃圾回收器
- 应用于新生代和老年代,在JDK9之后默认使用G1
- 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
- 采用复制算法
- 响应时间与吞吐量兼顾
- 分成三个阶段:新生代回收、并发标记、混合收集
- 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
G1的三个阶段:
Young Collection(年轻代垃圾回收)
-
初始时,所有区域都处于空闲状态
-
创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象
-
当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程
-
随着时间流逝,伊甸园的内存又有不足
-
将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代
并发的标记阶段
- 当老年代占用内存超过阈值(默认是45%)后,触发并发标记,这时无需暂停用户线程。
- 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。
- 这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来):
混合收集
- 混合收集阶段中,参与复制的有 eden、survivor、old
- 复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集
针对一个很大的对象,此时会将该对象存储到humongous中,如果一个区域装不下,会分配一个连续的区域来存储巨型对象:
强引用、软引用、弱引用、虚引用的区别
强引用:只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。
软引用:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收。
弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。
虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存。