解密JVM(二) -- 垃圾回收

一、导论

之前的文章,我们学习了jvm的内存结构,其中一个最重要的部分,即堆存在垃圾回收的机制。下面我们详细讲解垃圾回收的相关知识。

2、垃圾回收

二、如何判断对象可以回收

2.1 引用计数法

存在一个弊端:循环引用的问题(jvm没有采用这种算法)

2.2  可达性分析算法

  • java虚拟机采用的一种判断对象是否是垃圾(判断对象是否存活)的算法

Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

  • 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
  • 哪些对象可以作为 GC Root ?

首先需要确定一系列根对象

根对象:就是那些肯定不会被jvm当作垃圾,肯定不会被回收的对象。

- System Class : 系统类,是由启动类加载器加载的类(核心的类)--->  java.lang.Class,包括Object、String类

- Native Stack :本地的

- Busy Monitor:同步锁机制,被加锁的对象

- Thread : 活动线程中的对象,正在运行的线程

在垃圾回收之前,先对堆内存中对象进行一遍扫描,查看每个对象是否被根对象直接或者间接引用。如果是,那么这个对象则不能被回收;反之,这个对象就可以作为垃圾被回收。

 

2.3 四种引用

1. 强引用
只有所有 GC Roots 对象都不通过强引用引用该对象,该对象才能被垃圾回收;
2. 软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象;
可以配合引用队列来释放软引用自身
3. 弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象(FullGC);

可以配合引用队列来释放弱引用自身
4. 虚引用(PhantomReference)
必须配合引用队列使用,主要配合 ByteBuffer 使用,当虚引用的引用对象被回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
5. 终结器引用(FinalReference)
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象

实线箭头代表强引用。虚线表示没有被强引用所直接引用。

(1)虚引用和终结器引用必须配合引用队列使用

虚引用创建时,会关联一个引用队列;终结器引用对象创建时,也会关联一个引用队列。那么,它们是怎么工作的呢?

  • 当创建ByteBuffer的实现类对象时,会创建名为Cleaner的虚引用对象。ByteBuffer会分配一块直接内存,并把直接内存地址传递给虚引用对象。
  • 一旦ByteBuffer没有强引用指向(引用)时,ByteBuffer会被垃圾回收掉;但是其分配的直接内存并不能被java的垃圾回收机制所管理。所以,在ByteBuffer被回收时,对应的虚引用Cleaner对象会进入引用队列。而该引用队列会由一个指定的线程处理,即线程会在引用队列中查找是否存在新入队的Clearen引用对象。若找到,该线程会调用Cleaner的clean方法,按照Clearen引用中记录的直接内存的地址,然后调用Unsafe.freeMemory()方法,讲直接内存释放掉。保证了不会导致直接内存的泄露。

 

(2)虚拟机参数:-Xmx20m  代表设置堆内存大小为20M

强引用

 

(3)软引用 使用场景

byte[]数组作为软引用,当发生一次MinorGC,内存还是不足时,软引用会被回收掉。所以,前4个打印null

(4)软引用配合引用队列:将软引用为null的软引用清除 

 

(5)弱引用

设置堆内存:-Xmx20m

可以保证在内存空间紧张的情况下,对内存空间的释放机制。

弱引用一般在垃圾回收时,会将一些弱引用对象所占用的内存释放。想要释放弱引用对象自身占用的内存,同样需要配合引用队列来实现。

  • Full GC会将所有的弱引用进行垃圾回收

Full GC 是清理整个堆空间—包括年轻代和永久代。

 

三、垃圾回收算法

1、标记清除(Mark Sweep)

注意:标记清除只需要把待回收对象所占用内存的起始地址和结束地址记录下来,保存在空闲地址列表中就可以。清除释放并不是把整个待回收内存(空闲内存所有字节数)进行清零操作。

等到下次再次分配新的对象时,从空闲地址列表找某个合适的空间能容纳新对象;若找到,则进行新对象的内存分配。

  • 优点:速度较快
  • 缺点:导致空间不连续,易产生内存碎片

2、标记整理(Mark Compact)

  • 标记阶段:标记出没有被GC Root引用的对象,可以作为垃圾
  • 整理:避免内存碎片,对象会进行移动,效率较慢

 3、复制(Copy)

将内存区划分成大小相等的两块区域:坐边区称为FROM,右区称为TO,TO始终空闲

复制算法:首先对对象进行一次标记,然后把FROM区存活的对象复制到TO区,并且交换FROM区和TO区的位置

 

四、分代垃圾回收

1、分代垃圾回收概述:根据不同的区,采用不同的垃圾回收算法。

java程序中有些对象被长时间使用,这些长时间使用的对象存放在老年代,而那些用完就可以丢弃的对象存放在新生代

这样,可以根据不同对象的生命周期特点,进行不同的垃圾回收策略。

当创建新的对象时,默认分配Eden的内存空间。

 2、新生代垃圾回收 Minor GC:

对象首先分配在Eden区域。当Eden空间不够时,会触发 minor gc(可达性分析算法),  并且将Eden和 FROM区存活的对象采用 复制算法 复制到 survive TO区中,存活的对象生命周期加 1,会交换 survive FROM 和 TO的指针所指向的位置。这样,新对象又可以再存入Eden。

每次minorGC之后,Eden和survive FROM都会被清理干净,存活的对象采用 复制算法 复制到 survive TO,同时寿命 + 1,最后交换TO 和 FROM。

当survive区中对象寿命超过阈值时(15),会晋升至老年代,最大寿命是15(4bit)。

当有新对象需要放入内存,此时当新生代内存不足,同时老年代空间也不足时,会触发 Full GC,进行老年代的垃圾回收,会进行一次整体垃圾回收(新生代和老年代)。


minor gc 会引发 stop the world,暂停其它的用户线程,等垃圾回收结束之后,用户线程才恢复运行。(垃圾回收过程涉及对象的复制,对象地址会发生改变,若多个线程同时运行,会造成混乱。因为对象发生了移动,地址发生变化了,其它线程访问这个对象时,根据原来的地址找不到该对象)。STW的时间较短。
 

当老年代空间不足,会先尝试触发 minor gc(只要老年代的连续空间大于新生代的对象总空间大小或者平均晋升大小);如果之后空间仍不足,那么触发 Full GC(也会 stop the world),STW的时间更长;老年代的回收效率更低,回收时间长。

若Full GC之后,空间依旧不够,就会触发 OutOfMemoryError,java heap space。

Full GC 采用的垃圾回收算法:标记清除 或者 标记整理。

  • Minor GC触发之后,会采用我们前面分享的 判断对象是否可以回收的 可达性分析算法,沿着GC Root引用链查找,标记哪些对象可以作为垃圾;标记成功之后,接着采用 复制 算法,把存活的对象复制到 幸存区To,且对象存活寿命+1;复制算法,会交换幸存区From 和 幸存区To 的位置。
  • 第一次垃圾回收Minor GC后,内存空间充足,可以继续向伊甸园分配新的对象。
  • 经过一段时间的分配,伊甸园空间再次满了,会触发第二次垃圾回收。第二次垃圾回收,除了查找伊甸园中存活的对象外,还会查找幸存区中的对象,且对象存活寿命再次+1把幸存区中的垃圾对象回收掉;平且再次采用 复制 算法。
  • 注意:幸存区的对象,若其存活时间超过一个阈值(例如经过15次垃圾回收还存活),说明此对象的存活价值比较高,则将此对象转存入老年代。老年代垃圾回收频率较低。
  • 现象:新生代内存也几乎满了,同时老年代内存不够,这时候还有新的对象需要加入内存,怎么办呢?

       这时候就会触发一次Full GC。来触发老年代的垃圾回收,完成一次整个(新生代 -> 老年代)垃圾回收。

 

3、相关VM参数

 

4、案例

(1)没有运行任何代码的情况

 

(2)触发了两次垃圾回收 -- 一部分对象晋升至老年代 

(3)大对象直接晋升至老年代 -- 不会触发GC

(4)OutOfMemoryError

 

 

五、垃圾回收器

1、串行垃圾回收器

单线程垃圾回收器:堆内存较小,适合个人电脑

-XX:+UseSerialGC :可以开启串行垃圾回收器

    串行垃圾回收器分别两部分: Serial + SerialOld 

                                                  Serial 工作在新生代:采用复制 垃圾回收算法 

                                           SerialOld 工作在老年代 :采用 标记+整理 垃圾回收算法 (整理 不会产生内存碎片,但是效率偏低)

Serial 和 SerialOld都是单线程的垃圾回收器,因此只会有一个垃圾回收线程在运行。

堆内存不够,触发了垃圾回收;垃圾回收时,首先需要把当前运行的多个线程在安全点暂停执行:因为在垃圾回收的过程,对象的地址可能会发送改变;为了保证能够安全地使用对象的地址,需要将用户级线程到达安全点暂停下来。垃圾回收线程工作时,才不会被其它线程干扰。等到垃圾回收线程工作完成,其它的用户线程才接着执行。

 

2、吞吐量优先垃圾回收器 -- ParallelGC

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC  开启吞吐量优先的垃圾回收器

        新生代:复制算法        老年代:标记+整理算法

在JDK1.8默认开启,也就是默认是使用该垃圾回收器

多线程垃圾回收器;堆内存较大,多核CPU,适合工作在服务器;

让单位时间内,STW 的时间最短

例如单次STW时间为0.2,而一小时内只发生两次,即

0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高 

开启吞吐量优先的垃圾回收机制:   -XX:+UseParallelGC ~ -XX:+UseParallelOldGC   

(用户进程暂停,垃圾回收进程并行执行)

UseParallelGC :新生代垃圾回收器,采用的是复制 算法

多个垃圾回收线程并行运行,但是和用户级线程的关系是串行执行:

 

 

3、响应时间优先垃圾回收器 -- CMS

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld  :用于开启响应时间优先的垃圾回收器 -- 标记清除算法

UseConcMarkSweepGC  :CMS工作在老年代的垃圾回收器

Concurrent(并发):用户线程和垃圾回收线程并发执行

多线程;堆内存较大,多核 cpu,适合工作在服务器
尽可能让单次 STW(垃圾回收时,需要将其它线程暂停下来) 的时间最短 :

例如单次STW时间0.1,而一个小时内可能发生5次,即

0.1 0.1 0.1 0.1 0.1 = 0.5

垃圾回收线程和用户级线程并发执行(减少STW的时间):

老年代内存不足,用户线程到达安全的暂停下来,CMS开始工作,执行初始标记的动作(需要STW),也即是其它用户级线程阻塞,初始标记速度非常快(因为只标记根对象);初始标记结束后,用户线程恢复运行,同时,垃圾回收线程执行并发标记(不需要STW),标记剩余的垃圾对象;重新标记(STW),因为并发标记的同时,用户线程也在工作,可能会对之前垃圾回收产生干扰,所以需要重新标记;重新标记结束后,用户线程可以继续运行,垃圾回收线程再执行并发清理。

1/4线程进行垃圾回收,3/4作为用户线程。  

 

4、G1(Garbage First)垃圾回收器

优先回收那些垃圾最多的老年代内存区

JDK9之后取代了之前的CMS垃圾回收器,G1为默认垃圾回收器

4.1 适用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms。也就是用户线程工作的同时,垃圾回收线程也并发执行。
  • 超大堆内存,会将堆划分为多个大小相等的 Region(每个Region区都可以独立地作为伊甸园、幸存区、老年代
  • 整体上是标记+整理算法(避免了垃圾回收碎片两个区域之间是复制算法

4.2 相关 JVM 参数
-XX:+UseG1GC  :JDK1.8需要打开G1垃圾回收器开关,启用
-XX:G1HeapRegionSize=size    设置堆内存大小
-XX:MaxGCPauseMillis=time    设定暂停目标的参数

 

4.3  G1垃圾回收阶段

这三个阶段是循环执行:刚开始是新生代垃圾收集,经过一段时间老年代内存超过一个阈值,会在新生代垃圾回收同时进行并发标记;这个阶段完成后,会执行混合收集(会对新生代 survivor幸存区 和老年区 进行大规模垃圾回收);

此时,Eden内存被释放掉,又会再次进行新生待垃圾回收。

 

1)Young Collection

就是新生代垃圾回收

E:伊甸园:刚开始创建的对象分配到Eden

当Eden内存逐渐占满,会触发一次新生代垃圾回收【会触发stw(时间相对短)】,使用复制算法将对象拷贝到survivor幸存区;

经过一段时间,survivor区对象比较多且一些对象存活年龄超过阈值,又会触发新生代垃圾回收,survivor区对象有一部分晋升到老年代,不能晋升的对象会拷贝复制到survivor To区。

S:幸存区(E->S 会STW)

O:(S->O 触发Minor GC)老年代

 

2)Young Collection + CM (ConcurrentMark) 

新生代的垃圾回收+并发标记阶段

  • 在 Young GC 时会进行 GC Root 的初始标记(会STW):初始标记指的是找到根对象
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW,不会影响到工作线程):并发标记是指从根对象出发,顺着引用链标记其它对象

由JVM 参数决定:老年代占用堆空间比例45%会进行并发标记。

3)Mixed Collection

混合收集

会对 E、S、O 进行全面垃圾回收
最终标记(Remark)会 STW:在并发标记过程中,可能漏标记的一些对象。因为并发标记,用户线程也在工作,会产生一些新的垃圾。
拷贝存活(Evacuation)会 STW(复制算法)
-XX:MaxGCPauseMillis=ms (代表最大暂停时间)

G1垃圾回收器会根据设置的最大暂停时间,有选择地进行老年代的回收,也就是选择回收价值高的一部分老年代的内存区进行回收,这部分区域回收后能释放的内存空间更大(尽可能保证复制的区域少,垃圾回收时间变短)。

针对老年代,优先收集那些垃圾最多的区域 ,选择回收价值最高的老年代区进行复制,也就是拷贝那些空间较大的老年代,然后将这些区域进行垃圾回收,主要是为了达到暂停时间尽量短的目标。

4) Full GC

串行和并行的垃圾收集器:由于老年代内存不足,触发的垃圾回收直接称之为 full GC。

  • SerialGC

新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足发生的垃圾收集 - full gc

  • ParallelGC

新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足发生的垃圾收集 - full gc

  • CMS

新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足:与下同

  • G1

新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足:分两种情况

当老年代内存跟堆内存空间占比达到45%以上(阈值),会触发并发标记阶段以及后续的混合收集阶段。这两个阶段工作过程中:若回收速度快于用户线程产生垃圾的速度,这时还不是full gc,还是处于并发垃圾清理阶段,STW时间短;

若回收速度跟不上用户线程产生垃圾的速度,并发收集失败,会退化为 串行垃圾回收器收集,这时成为 full gc,速度较慢。

 

5)Young Collection 跨代引用

  • 新生代回收的跨代引用(老年代引用新生代)问题

根对象 -> 可达性分析算法 -> 存活对象 -> 复制到幸存区

存在问题:需要找到新生代对象的根对象,根对象有一部分是来自于老年代。老年代存活对象通常都比较多。

因此,再将老年代进行细分。

卡表与 Remembered Set
在引用变更时通过 post-write barrier + dirty card queue
concurrent refinement threads 更新 Remembered Set

进行GC Root,不用遍历整个老年代,只需要关注那些dirty card区域

 

6)Remark

并发垃圾回收器:并发标记阶段 和 重新标记阶段

pre-write barrier + satb_mark_queue

  • 并发标记阶段对象的处理状态

黑色:已经处理完

灰色:尚在处理中

白色:还未处理

 

  • 重新标记阶段

 

六、垃圾回收调优

预备知识
掌握 GC 相关的 VM 参数,会基本的空间调整
掌握相关工具
明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则

6.1 查看虚拟机运行参数

找到与垃圾回收器GC相关的参数信息

 

6.2 最快的GC

-- 答案是不发生 GC


查看 FullGC 前后的内存占用,考虑下面几个问题
数据是不是太多?
resultSet = statement.executeQuery("select * from 大表 limit n")
数据表示是否太臃肿?
对象图
对象大小 16 Integer 24 int 4
是否存在内存泄漏?
static Map map = 
可以使用软引用 或者 弱引用 或者 使用第三方缓存实现

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值