JVM——垃圾回收

发布于:2025-05-24 ⋅ 阅读:(19) ⋅ 点赞:(0)

目录

1.如何判断对象可以回收

引用计数法: 当前对象被一个变量所引用,则该对象引用计数+1。 当对象的引用计数为0时,则应该回收!

可达性分析算法:

1.1四种引用

一。强引用

二.软引用

三.弱引用

四.虚引用

软引用_应用

软引用_引用队列

2.垃圾回收算法

(1)标记 清除

(3)标记复制算法

3.分代垃圾回收

3.1总结

4.垃圾回收器

4.1六种常见垃圾回收器

4.1.1补充

4.2.G1_详解

4.2.1.只谈G1的工作流程

4.2.2.与CMS的比较

5.垃圾回收调优

5.1GC调优_新生代

5.2GC调优_新生代_幸存区

5.3GC调优_老年代


1.如何判断对象可以回收

引用计数法: 当前对象被一个变量所引用,则该对象引用计数+1。 当对象的引用计数为0时,则应该回收!

缺陷:这种情况就会导致内存泄露

可达性分析算法:

一些根(GC Root)对象: 肯定不会被回收的对象。java虚拟机进行垃圾回收的时候,会先去堆中将所有对象进行一遍扫描。看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收!
否则不会被回收!

1.1四种引用

一。强引用

平时new的都是强引用.只要能够沿着GC Root的引用链找到它,就不会被回收!
没有GC Root直接/间接引用该对象了,则该对象会被回收!

二.软引用

当垃圾回收且内存不足时,会被回收掉。(但是如果被某个强引用直接引用,则不会被回收)

三.弱引用

当垃圾回收的时候,及时内存够也会被回收掉(但是前提是没有被某个强引用直接引用)

软引用和弱引用也可以配合"引用队列"来使用,也可以不配合。
"软引用"和"弱引用"本身也是两个对象!
当这个两个对象引用的对象被回收后,他两就可以放到"引用队列"中去!

四.虚引用

当[虚引用]对象创建的时候,就会关联一个[引用队列]
虚引用的典型用法(释放直接内存):
在虚引用引用的对象被垃圾回收时,虚引用对象自己就会被放进引用队列,
从而间接的有一个线程来调用虚引用对象的方法,调用Unsafe.freeMemory来释放直接内存!

软引用_应用

案列:比如很多图片资源,如果使用强引用引用这些图片资源,则会容易发生内存溢出.。
改进: 在内存紧张时,将该占有的内存释放掉,以后要使用到时,再读取一遍!
    要采用软/弱引用!

改进前:

list  --直接强引用-->  byte[]

改进后(通过软/弱引用):
list  --强引用一个[弱引用]对象--SoftReference --软引用--> byte[]

软引用引用的对象只要没有被根对象强引用,当内存紧张时,就会触发垃圾回收!

软引用_引用队列

软引用引用的对象在内存紧张时被回收,我们也希望将软引用对象本身也回收掉!

软引用对象本身也是占用内存的!
如何清理一个无用的软引用对象本身呢??
- 需要使用
[引用队列]来完成软引用的清理!

如何关联软引用和引用队列??

        -在创建引用对象的时候,将[引用队列]作为参数传入!

2.垃圾回收算法

(1)标记 清除

         优点: 速度快

         缺点: 容易产生内存碎片.可用内存的连续空间不大!

(2)标记整理
        和标记清理第一阶段是一样的,区别在第二步整理上!
        解决内存碎片问题,在整理过程中会让可用的内存紧凑起来,这样可用内存就比较连续!
            优点: 解决了内存碎片的问题
            缺点: 时间长 耗性能!

(3)标记复制算法

新生代使用标记复制算法的时候,每次将伊甸园和幸存区from中存活对象放入幸存区to,如果幸存区to没有足够的空间存放存活的对象,这些对象将会通过"分配担保机制"直接进入老年代,这是安全的。(因为研究表明新生代中98%的对象都熬不过第一轮回收)

标记复制

 将存活的对象从from区域/伊甸园复制到to区域,并进行寿命+1操作!然后交换from和to。

缺点:     
            会占用双份的内存!

JVM中这三种算法都是结合着一起使用,不会只使用一种!

 

3.分代垃圾回收

java虚拟机会结合上述三个算法协同工作,具体的实现叫做"分代"的垃圾回收机制!

将堆的内存划分为两大块,叫做"新生代"和"老年代"
"新生代"又分为[伊甸园][幸存区From][幸存区To]

java中有些对象长时间使用的放在老年代,有些用的时间短的放在新生代。
根据java对象的生命周期的变化存放在不同的位置.并采取不同的垃圾回收的策略.
老年代中很久触发一次垃圾回收,而新生代垃圾回收的频率高一些!

Minor GC垃圾回收流程:

开始创建的对象都会直接被存放到[新生代][伊甸园]中.当[伊甸园]中内存快满时,会触发
一次gc垃圾回收,注意,新生代的垃圾回收一般被称为
"Minor GC"。然后会根据"可达性分析算法"根据GC root引用链去找哪些对象是没用的,哪些对象是有用的。经过一次gc后存活下来的对象会采用"复制算法"被放到[新生代]的[幸存区 To]中,且该对象的
寿命+1。伊甸园中要被清理的对象就会被直接清理掉!并交换[幸存区 From]
[幸存区 To]的的指针,也就是交换了位置!
然后就可以继续向伊甸园中放一些对象!


第二次触发Minor GC的时候,会去"伊甸园"和"幸活区 From"中根据可达分析算法寻找
需要被回收的垃圾。然后将存活下来的存放到[存活区 To]中,再最终交换from和to的位置.
当经历了一定次数的垃圾回收后,仍然在[幸存区 From]中存活的对象,显然是有价值的!就会被放入老年代中!

3.1总结

* minor gc会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。

4.垃圾回收器

1.垃圾回收算法是方法论,垃圾回收器则是方法论的实践者。
  不同的垃圾回收器负责不同的区域,并采取不同的垃圾回收算法。

1.串行的垃圾回收器
* 单线程的垃圾回收,用一个线程来完成垃圾回收!
* 堆内存较小,适合个人电脑.

2.并行的垃圾回收器(吞吐量优先)
* 多线程
* 适合堆内存较大的.需要多核cpu
* 让单位时间内,STW的时间最短.  
* 垃圾回收时间越低.你的吞吐量越高!

3.并发的垃圾回收期(响应时间优先)


* 多线程
* 适合堆内存较大的.需要多核cpu
* 尽可能让单次stop the world的时间最短
  

并发:用户线程和垃圾回收线程可以同时在执行!

4.1六种常见垃圾回收器

jdk 1.8 分代模型:

常见的垃圾回收器:


(1)串行垃圾回收器(Serial  +  SerialOld):
        单线程的工作方式(会暂停所有用户线程)

注意演化的变化!!

(2)并行的垃圾回收器[高吞吐量](Parallel Scavenge + Paraller Old) 并行的垃圾回收器[jdk 8默认的垃圾回收器]

    综上四种垃圾回收器 Serial SerialOld PS PO都会导致一个现象: 用户线程的阻塞!

用户线程暂停 <==>  STW 
    垃圾收集的时候,用户在访问的时候会直接卡主...没有线程给用户来用。

(3)并发的垃圾回收器[高响应速度] ParNew(新生代) + CMS(老年代)
        ParNew是在Serial基础上做的改良,配合CMS使用,但大致没什么变化!

别的老年代的Serial Old和PO都采取"标记整理",但是CMS采取"标记清除"!!

(1) 初始标记: 

        找到所有GC Root根对象,短暂STW.简单标记一下

(2) 并发标记:
        - 根据GC Root根对象进行对象图的遍历.且该阶段不会暂停用户线程,允许gc和用户线程并发执行
        - 通过写屏障技术记录下发生错标问题的黑色对象..并放入队列中

(3) 重新标记:
        会通过"增量更新"的解决方案解决上一阶段产生的错标问题.
(4) 并发清理:
        清理掉确定会被回收的垃圾

* 浮动垃圾:

在gc线程与用户线程并发进行的阶段,gc线程在"并发标记"阶段,在搜集垃圾,
    但是在此时阶段用户线程的操作导致的原先通过"初始标记"的"非垃圾对象"变为"垃圾对象", 而此时gc收集器无法检测到,则该垃圾对象被称为"浮动垃圾"。

* 错标:

在"初始标记"的垃圾对象,在"并发标记"阶段因为用户线程的影响变为了非垃圾对象.
    此时称为错标!

注意:

  •     (1)浮动垃圾无所谓,在下次垃圾回收的时候会把该垃圾回收。不会有什么特别的影响。 但是"错标"的线程,会导致要使用的对象被回收了,就会造成影响。所以接下来的"重新标记"阶段就是解决"错标"的问题!!!
  •     (2)"初始标记"和"重新标记"都会产生极其短暂的STW.但可忽略不计!
  •     (3)当CMS并发处理失败的时候,会立马切换Serial Old来清理!
  •     (4) CMS解决错标问题采用了"增量更新"的方式。对于那些黑色增加对白色的引用,会通过写屏障的技术记录下来,再重新标记阶段进行以黑色为根重新扫描一遍,来解决错标的问题!

不好的地方:

  • (1)内存碎片
  • (2)浮动垃圾
  •  (3)如果CMS运行期间预留的空间不足以让用户线程分配一个对象,则并发失败。 并发失败时会先冻结用户线程,然后启动Serial Old来收集老年代。

总结:
    (1)CMS解决了什么样的问题??
        - 解决了STW时间过长的问题,使垃圾回收时用户的等待时间变短
    
    (2)jdk 1.8之前都是分代垃圾回收,所以就是这么分代垃圾回收器.
        jdk 1.8之后都是分区垃圾回收.所以G1 ZGC等就没有老年代 新生代这种概念了!

4.1.1补充

1.吞吐量

2.是否要发生STW的一个重要依据是什么???
* 是否要移动对象!


3.标记清除和标记整理最大的本质区别是什么?
(1) 前面的标记动作都一样, 标记整理算法需要所有存活的对象移动挨到一起.而标记清除算法直接删除,所以也会有内存碎片.


4.为什么CMS要采用标记清除?而不是标记整理??
- 因为标记整理算法涉及到对象的移动,在对象移动阶段必须STW.而CMS的第四阶段并发清理阶段,是不会STW的,是要允许用户
  线程不能被STW的

五. CMS并发失败的问题?
在第四阶段并发清理阶段CMS不会STW,依旧gc线程和用户线程一起运行。并不能像别的老年代垃圾回收器那样等待垃圾快满了再回收,而是在并发清理阶段预留一定的空间给程序运行。当无法为程序分配足够的空间时,则并发失败,冻结用户线程,然后换为Serial Old来清理。

别的垃圾回收器: 我gc线程垃圾回收之后你用户线程再使用,。

CMS就是在并发清理(回收阶段)也会让你用户线程继续跑,所以CMS就需要在垃圾回收阶段预留给用户线程足够的空间来使用,一旦用户线程要使用的空间大于预留的空间,则并发失败!

六.CMS最严重的两个问题?以及优化??
- 标记清除造成的内存碎片问题
晋升失败时候,新生代发生Minnor GC,幸存区中放不下要存活的对象,只能触发对象担保机制,晋升老年代,由于内存碎片过多,导致大对象没法被放下!

解决: 让CMS进行一定次数的Full GC的后进行一次标记整理的算法,控制住内存碎片的数量.
       可以通过参数配置...     

   

- 并发失败时的Serial Old

解决: 降低触发CMS GC的阈值...让其预留够足够的空间!
 

4.2.G1_详解

定义:Garbage First

适用场景:

  • 同时注重吞吐量和低延迟,默认暂停目标是200ms
  • 超大堆内存,会将堆划分为多个大小相等的Region
  • 整体上是标记+整理算法,两个区域之间是复制算法

G1整体上时标记整理算法,局部上(两个region之间)是标记复制算法...
G1综合PS PO的高吞吐量 + CMS的高响应,做一个综合更全面的!是一种基于"停顿时间模型"的方式构成的垃圾回收器!

引言:

0.停顿时间模型??
作为CMS收集器的替代者和继承人,G1是基于“停顿时间模型”诞生的的垃圾收集器,
停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。

1.G1摒弃了以往对堆中区域分代的划分思想,而是将堆划分为大小相同的区(Region),但是依旧传承着
   不同的角色,相同的区也依旧可以被看作伊甸园 幸存区 老年代区域,G1会针对扮演不同角色的区域都
   进行对应的策略进行处理. G1会有选择的选择垃圾最多的区域进行回收,这就是Mixed GC模式..

2.如何实现的"停顿时间模型"以及为何高效??
(1)
G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,
即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
(2)
更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

4.G1高效的地方
- 基于分区的内存划分,避免了分代回收时对整个大区域的垃圾回收.分区回收只需要回收一个区的整数倍
- G1维护的每个区的回收价值的优先级列表,使得回收更有针对性优先回收价值高的,更加高效.
基于上述,实现了G1回收时可控的将垃圾回收的时间控制在制定的毫秒内,从而满足了"停顿时间模型"..

4.2.1.只谈G1的工作流程

- 初次标记(STW)
        只是简单标记GC Root根对象.
- 并发标记(不会STW)5
        * 允许用户线程一起运行,可达性分析沿着GC Root标记的根对象对对象图进行一个全扫描!标记出垃圾..
        * 通过写屏障的技术记录下会发生错标的对象

- 最终标记(STW)
        * 解决发生的错标问题,通过
原始快照的算法解决.重新扫描一下被记录下来的灰色对象!
- 混合回收(STW)    [因为标记整理算法需要移动对象,所以一定要STW]
        *  根据G1跟踪生成的对于不同区的优先级列表,对区进行有选择的进行回收,优先回收那些回收价值大的区!
            根据用于设置的允许停顿的时间值(默认200ms 停顿时间模型)生成对应的回收计划,然后选择多个region生成一个
            回收集,将回收集先拷贝到空的区中,然后清除掉旧的回收集.因为涉及对象的移动所以会STW.    

4.2.2.与CMS的比较

(1) 垃圾回收算法
与CMS的
“标记-清除”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。  

(2) 内存占用方面
就内存占用来说,虽然G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且
堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和
其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间;相比起来CMS的卡表就相当简单,
只有唯一一份,而且只需要处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝
生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的

注意:
对卡表的维护工作,都是通过写屏障的技术完成的!
 CMS中因为只需要维护一份卡表(老年代和新生代的引用问题),所以卡表的维护是同步的方式!
 G1中每一份region都需要持有一个Remembered Set(记忆集),所以卡表的维护较为繁琐,所以采取异步的方式!

5.垃圾回收调优

2.确定目标

3.最快的GC是不发生GC

5.1GC调优_新生代

一.内存调优的点
(1)先检查代码由于自身问题可以优化的点
(2)再进行内存调优
    优先从新生代中调优...


二.新生代作用
1.所有new操作的内存分配非常廉价
(1)TLAB(thread-local allocation buffer): 
    每一个线程都会在伊甸园中给它分配一个私有的区域,叫TLAB
    每次new一个对象,会检查TLAB缓冲区中有没有该对象,如果有,会优先在这个区域里面进行对象内存分配!
(2).为什么要有这个东西??
    对象分配其实也会有线程安全问题.线程1要该对象,在没分配完的时候线程2也要这个对象....会有线程安全问题
TLAB作用就是让每个线程用自己私有的这块伊甸园内存来进行对象分配!


2.死亡对象的回收代价为0
新生代的垃圾回收采用的都是复制算法,都是将伊甸园和幸存区from中存活对象复制到to中
直接清除伊甸园和from中垃圾对象
3.大部分新生代对象用过即死
4.Minor GC的时间远远低于Full GC

三.新生代越大越好??
如果新生代过小,会频繁触发minor GC,会导致STW..响应时间就会加大
如果新生代过大,一直不会minor GC,甚至会导致Full GC的触发!!
Oracle建议新生代的内存占整个堆的25%以上,50%以下...
新生代的大小还是找到一个合适的点比较重要!

5.2GC调优_新生代_幸存区

幸存区中都是这两类对象:


(1)即将要被回收的
(2)需要晋升的对象,目前还没有达到阈值

1.幸存区大到保留[当前活跃对象 + 需要晋升对象]
2.晋升阈值配置得当,让长时间存活对象尽快晋升.不然会在新生代中每次gc的时候复制来复制去,效率很低...

5.3GC调优_老年代

CMS为例
1.CMS的老年代内存越大越好
   避免CMS的产生的浮动垃圾过多而导致的并发失败....

2.

3.