三、垃圾回收

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

1、引用计数法

会造成死循环 (JVM 不是用的这种)

三、垃圾回收_第1张图片

2、 可达性分析算法

  • Java虚拟机中的垃圾回收采用可达性分析来探索所有存活的对象
  • 确定一系列根对象,然后扫描一遍,判断每一个对象是否间接或者直接被根对象引用,如果找不到,表示可以回收
  • 哪些对象可以作为GC Root?

可以使用以下工具

三、垃圾回收_第2张图片

三、垃圾回收_第3张图片

以上都可以当做GC Root

3、四种引用

  • 实际是5种 实线表示强引用

三、垃圾回收_第4张图片

1. 强引用

被强引用的对象,不能被垃圾回收

2. 软引用

  • 被软引用的对象,如果触发了垃圾回收之后,发现还是内存不足,就会将软引用的引用的对象回收,反之,则可以继续使用。

三、垃圾回收_第5张图片

三、垃圾回收_第6张图片

软引用是一次Full GC之后内存不足才会回收,弱引用是一次Full GC之后就会回收

3. 弱引用

  • 被弱引用的对象,如果触发了垃圾回收之后,不管内存是否充足,都会被垃圾回收。

三、垃圾回收_第7张图片

软引用、弱引用所引用的对象如果被垃圾回收,就会加入一个叫做引用队列的空间,因为软引用 弱引用自身也是需要占用内存空间的,但是不好找,如果想要删除软引用或者弱引用,就需要配合引用队列来进行遍历删除。

4. 虚引用

  • 虚引⽤必须和引⽤队列(ReferenceQueue)联合使⽤
  • 被虚引用的对象,如果触发了垃圾回收,就会将虚引用加入到对应的引用队列中,用于跟踪回收对象之前采取的必要的行动。

举例子:

1、创建一个ByteBuffer调用函数之后,会生成一个直接内存(Java垃圾回收不能作用到这块不属于java的内存空间) 这时候会有一个Cleaner的虚引用对象记录当前这块直接内存的地址

2、当ByteBuffer的强引用去除之后,那么ByteBuffer就可以被垃圾回收,这时候指向ByteBuffer的虚引用就会被加入到对应的引用队列中

3、然后会有一个专门的引用线程,会在特定的时间,扫描引用队列中是否有新加入的Cleaner虚引用对象,然后调用他的Unsafe.freeMemory()方法来释放这块直接内存

5. 终结器引用

  • 必须和引⽤队列(ReferenceQueue)联合使⽤
  • 效率低

1、每一个对象都会继承Object父类,Object中有一个方法叫做finallize() 终结方法

2、如果当前对象实现了这个终结方法,然后当前对象没有强引用的时候,那么我们的实现这个终结方法就是为了要让他执行

3、那如何让他执行呢,虚拟机就会创建一个终结器引用,然后指向这个finallize()的方法,当执行垃圾回收时,然后会将终结器引用加入到对应的引用队列中,然后会有一个优先级很低的线程(FinallizeHandler)在特定时间扫描到这个终结器引用,然后会去执行终结器引用 所指向的finallize() ,然后调用他

4、这时候finallize的空间还没有真正被回收,当调用完之后,执行下次垃圾回收的时候,才会垃圾回收它

6. 总结

三、垃圾回收_第8张图片

二、垃圾回收算法

1、标记清除

清除只需要记录对应内存的起始地址,终结地址,然后存到一个空闲队列等待分配

三、垃圾回收_第9张图片

2、标记整理

优点:没有内存碎片

缺点: 牵扯到内存的移动,效率很低

  • 例如如果移动的内存被其他对象引用,那么就需要改变其他对象的引用,这时候效率就比较低了

三、垃圾回收_第10张图片

操作系统知识:克服外部碎片可以通过紧凑技术来解决

3、复制

优点:不会产生碎片

缺点:占用双倍内存空间

  • 标记

三、垃圾回收_第11张图片

  • 复制(完成碎片整理)

三、垃圾回收_第12张图片

  • 清空原来标记内存

三、垃圾回收_第13张图片

  • 最后交换位置

三、垃圾回收_第14张图片

三、分代垃圾回收

1、概念

新生代里面的垃圾是可以快速回收,老年代的垃圾是存活比较久

2、GC_相关参数

三、垃圾回收_第15张图片

3、GC_分析

三、垃圾回收_第16张图片

a.第一次垃圾回收

  • 对象创建之后,会放入新生代伊甸园中,等到伊甸园装满了,就会出发一次Minor GC
  • 然后根据可达性分析算法来判断哪些是Root GC,然后将存活的对象复制到幸存区To
  • 然后经历了一次垃圾回收不死,寿命+1
  • 复制算法,最后会交换From和 To的位置

三、垃圾回收_第17张图片

交换

三、垃圾回收_第18张图片

b.第二次垃圾回收

  • 如果伊甸园再次装满,触发第二次垃圾回收Minor GC
  • 就会将From和伊甸园中Root GC以及相关的对象放入幸存区TO,并且寿命+1
  • 最后From 和 To交换位置,然后新对象就有伊甸园空间可以放入了

三、垃圾回收_第19张图片

幸存区中的对象不会一直待着,会有一个预定值,寿命值超过15就会晋升到老年代中,存活时间比较久的空间

c. 第三次垃圾回收

  • 如果老年代也装满了,伊甸园也装满了,那么这时候就会触发一次Full GC,然后进行一次大回收,将老年代跟新生代的清理。
  • 如果还是内存不够,这时候就会报出outOfMemoryError 内存溢出错误

总结

  • minor gc会引发stop the world(暂停其他用户的线程,垃圾回收线程执行完成之后恢复)
  • 如果发现当前放入的对象太大,在老年代完全足够,然后伊甸园完全不够的情况下,寿命没有到15,也会自动晋升,放到老年代中。
  • 如果一个线程出现了OutOfMemoryError,不会导致整个java进程的结束。

四、垃圾回收器

1、串行

-XX:+UserSerialGC= Serial+SerialOld

三、垃圾回收_第20张图片

  • 因为Serial 和 SerialOld 都是单线程的垃圾回收器,所以只有一个垃圾回收线程在运行
  • 堆内存不够时,触发了垃圾回收,其他线程在一个安全点停下来,防止垃圾回收之后,变更了对象地址,不会导致程序错误,因为是单线程的垃圾回收器,所以只有一个垃圾回收线程在执行,其他都进行阻塞,等待垃圾回收线程执行完毕后,所有线程才恢复运行。
  • 这个图适用于Serial ,也适用SerialOld,因为他们都使用的是单线程回收器,都会执行stop the world,只是使用的回收算法有所不同。

2、吞吐量优先

  • 吞吐量主要衡量程序运行时间中有多少是用于实际工作的,而不是花费在垃圾回收上。
  • 并行的:同一时刻进行

三、垃圾回收_第21张图片

3、响应速度优先(CMS)

  • 并发的: 同一时间段内进行

垃圾回收器在工作的同时,其他的用户线程也能同时进行,垃圾回收线程跟用户线程是并发执行

多个垃圾回收器并行执行,在此期间,不允许我们的用户线程继续运行换句话说就是stop the world (STW)

三、垃圾回收_第22张图片

工作流程:

  • 多个cpu开始并行执行,这时候老年代发生了内存不足,那么这些线程都到达了安全点,暂停下来
  • 这时候CMS回收器就开始工作了,执行一个初始标记的动作,在执行这个动作时,仍需要stop the world,也就是我们其他用户线程就阻塞,暂停下来(初始标记很快, 他只标记根对象,不会遍历所有,所以执行很快)
  • 等到初始标记完成了之后, 用户线程就可以恢复运行了, 与此同时,我们的垃圾回收线程还可以并发标记 (剩余的那些垃圾可达性分析),是并发的,不用暂停用户线程
  • 并发标记之后,还需要做一步(重新标记)此时又需要stop the world,因为你在并发标记的同时,用户线程也在工作,他工作的时候,就有可能将你现有的对象,产生新的对象,改变一些对象的引用,就有可能对你的垃圾回收做了一些干扰,所以并发结束以后,还需要做一步重新标记的 工作,等重新标记完了之后,用户线程又可以恢复运行,这时候我的垃圾回收线程再做一次并发清除
  • 只有在初始标记 和 重新标记的时候才会触发stop the world ,所以响应时间很短,是一个专注于响应时间的老年代垃圾回收器

重新标记阶段,有一个特殊场景,有可能一些新生代的对象会引用老年代的对象,这时候我要进行重新标记的时候,他必须要扫描整个堆,然后通过新生代去引用扫描一遍老年代的对象,做可达性分析,但是这样对性能影响比较大,新生代创建的对象一般都比较多,而且其中很多本身是要作为垃圾的,如果我们从新生代找我们的老年代,这个你就算是找到了,将来这些新生代的垃圾也要被回收掉,所以相当于我们在回收之前多做了一些无用功

可以通过重新标记参数进行一次优化。

4、G1 (Garbage First)

定义:

  • 2004论文发布
  • 2009 JDK 6u14体验
  • 2012 JDK 7u4 官方支持
  • 2017 JDK 9 默认使用 取代 CMS

三、垃圾回收_第23张图片

a. G1垃圾回收阶段

三、垃圾回收_第24张图片

b.Young Collection
  • 会STW
  • 当伊甸园的大小逐渐占满,会触发新生代垃圾回收, 复制进幸存区,然后寿命+1,会STW
  • 当幸存区的对象比较多了,那么就会继续进行复制到其他幸存区,如果寿命到达预值15,就会放到老年代

三、垃圾回收_第25张图片

三、垃圾回收_第26张图片

三、垃圾回收_第27张图片

c. Young Collection + CM
  • Young Gc时会进行GC Root 的初始标记
  • 当老年代占用堆空间比例达到阈值时,进行并发标记。顺着初始标记的Root对象找到对应的引用对象(不会STW)

d. Mixed Collection

会对E、S、O进行全面的垃圾回收

  • 最终标记 会STW
  • 拷贝存活 会STW

  • 有一些老年代经过并发标记阶段,也可能没用了
  • 为什么老年代不是所有都进行拷贝,因为G1对老年代的回收,需要根据最大暂停时间来综合考虑,选择那些存活度低的进行回收,而不是所有都进行回收,这时候就会将这部分存活度比较低的复制到新的老年代区域,来达到时间跟效益的最大化。
  • 这也就是Garbage First由来,优先回收垃圾最多的区域

三、垃圾回收_第28张图片

e. FUll GC
  • 串行老年代内存不足就触发
  • 并行老年代内存不足就触发
  • CMS老年代内存不足也不会触发,经过初始标记,并发标记等一系列流程,最后如果出现并发错误,就会替换成串行,触发FULL GC
  • G1老年代内存不足也不会触发,经过并发标记,混合收集等一系列流程,最后如果回收的速度比产生垃圾的速度快,这时候还是处于并发垃圾收集的阶段,反之,并发收集失败,退化成串行收集,这时候FULL GC
f. Young Collection 跨代引用

初始标记的时候,会进行根的标记,那么会有一部分在老年代中,老年代一般都比较大,存活时间比较久,如果寻找需要遍历一遍老年代来标记,显然效率很低,因此,采取的是一种卡表的技术。

把老年代在进行划分,划分成一个个的card,每个card是512k

如果老年代中的卡引用了新生代中的对象,那么称为脏卡

那么到时候只需要检查那些脏卡就可以,提高效率

新生代会标记哪些引用了我,标记--脏卡 使用(Remembered Set)

会使用一个写屏障,每次发生引用变更,就会触发标记脏卡,存到脏卡队列,等待脏卡线程执行操作

三、垃圾回收_第29张图片

三、垃圾回收_第30张图片

g. Remark

三色标记法

  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是 白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代 表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

也就是并发标记的时候,用户发生的引用变更,会在引用变更中间触发一个写屏障,然后加入到一个队列里面,防止出现删除引用错误的情况。

最后重新标记阶段就会从队列里面一个一个取出来,修改。

看书补充:

当且仅当以下两个条件同时满足时,会产生“对象消失”的问 题,即原本应该是黑色的对象被误标为白色:

·赋值器插入了一条或多条从黑色对象到白色对象的新引用;

·赋值器删除了全部从灰色对象到该白色对象的直接或间接引用 ;

解决方案:

增量更新(解决第一个条件)

  • 如果插入了新纪录是从黑色对象指向白色对象,那么就会将黑色标记成灰色,
  • 然后将这条要插入的引用记录给记录下来,然后并发扫描结束之后,以黑色对象为根,在进行一次扫描

原始快照(解决第二个条件)

  • 如果删除了从灰色对象指向白色对象的记录,那么不管删除与否,都会执行原来的刚开始扫描那一刻的对象图快照来进行搜索
  • 然后将这条要删除的引用记录给记录下来,然后并发扫描结束之后,以灰色对象为根,在进行一次扫描

三、垃圾回收_第31张图片

h. JDK 8u20 字符串去重

三、垃圾回收_第32张图片

i. JDK 8u40 并发标记类卸载

j. JDK 8u60 回收巨形对象

三、垃圾回收_第33张图片

三、垃圾回收_第34张图片

k. 写屏障

可以看做是虚拟机层面对“引用类型字段赋值”这个动作的AOP切面。 在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的 前后都在写屏障的覆盖范畴内。

-----新版本

三、垃圾回收_第35张图片

a. G1垃圾回收器-内存结构

三、垃圾回收_第36张图片

b. 垃圾回收方式
  • 1、年轻代回收(Young GC) 回收新生代
  • 2、混合回收(Mixed GC) 回收新生代 + 老年代

三、垃圾回收_第37张图片

年轻代回收步骤:

优化:

c. 混合回收的执行流程

三、垃圾回收_第38张图片

三、垃圾回收_第39张图片

混合回收

三、垃圾回收_第40张图片

如果在复制过程中,发现整个堆没有空间可以进行复制转移了,那么就会进行FULL GC

三、垃圾回收_第41张图片

五、垃圾回收调优

查看虚拟机运行参数

1、调优领域

  • 内存
  • 锁竞争
  • cpu占用
  • io

2、确定目标

如果是进行科学运算,我们能够接受一点点的延迟,那么我们就可以选择高吞吐量的垃圾回收器 [Parallel]

如果是进行市场项目,我们就需要考虑用户体验,降低延迟,所以选择低延迟垃圾回收器[CMS G1 ZGC Zing]

3、最快的GC是不发生GC

  • 查看FULLGC前后的内存占用,可以考虑以下几个问题
    • 数据是不是太多?
      • resultSet = statement.executeQuery("select * from 大表 limit n")
        • jdbc的调用查询直接使用了全部查询,这里可以使用分页查询 添加limit,减少不必要的查询
    • 数据表示是不是太臃肿?
      • 对象图
        • 例如 我们要查询用户相关信息,直接使用了表连接,将用户相关的所有信息查出来了,造成了不需要的内存空间占用,所以建议用到什么查什么
      • 对象大小
        • 例如 Integer 包装类型占用16 + 4[值] + 4[对齐操作] 也就是24个字节 int 类型 4个字节
        • 所以看看是否是有必要换成int类型来进行搜身
    • 是否内存泄漏?
      • static Map map 使用了map来进行缓存,一直存没有得到释放,最后就会造成空间的浪费
      • 所以可以使用一些第三方的缓存工具redis、cache工具

4、新生代调优

  • 新生代的特点
    • 1、所有的new操作的内存分配非常廉价
      • new新对象会在伊甸园中分配,每一个线程都会在伊甸园中分配一块私有的区域也就是TLAB,new一个对象的时候,首先看一下TLAB缓冲区是否有可用内存,优先在这里进行对象分配
      • 对象分配也有一个线程安全的问题,比如线程1要使用这块内存,那么线程2分配的时候就不能够使用这块内存,就造成内存分配混乱,所以需要考虑这个线程分配安全问题,JVM做的。
      • 如何减少线程之间对内存分配时的并发冲突,TLAB,每个线程使用自己私有的伊甸园内存进行对象分配,这样就算多个线程并发创建对象,也不会对内存占用造成干扰
      • TLAB thread-local allocation buffer
    • 2、死亡对象回收代价为零
    • 3、大部分对象用过即死
    • 4、MinorGC的时间远远低于FUll GC
如何对新生代内存进行调优呢?

可能第一个想到的就是,调大新生代的内存空间大小

这是一个最有效的方式,当然我们也要考虑调大之后存在的一些问题。

三、垃圾回收_第42张图片

调的太小,会经常触发minor Gc

调的太大,会导致老年代变小,容易触发Full Gc,时间更长

随着新生代空间越来越大,吞吐量越来越高,当新生代达到一定的大小的时候,吞吐量会有一定的下降,因为新生代空间变大了,相应的MInor Gc的时间也就变得久了,所以我们需要找到一个最优的点来进行设置。

补充:

但是一般我们还是越大越好原则,这里为什么跟前面不对应呢,这里没有考虑到一个因素,

也就是新生代使用的是标记-复制算法,比较耗时的是复制这一步操作,牵扯到内存的复制和移动,但是新生代中大部分对象都是用过就删的,所以存活的很少,所以我们复制这一步其实耗费的时间也没有那么长,相对于复制,标记的时间就更不值一提了,所以我们还是可以将内存调的很大,效率也不会有一个很明显的下降。

那么设置多少合适呢?
  • 新生代能容纳所有【并发量 * (一次请求-响应过程中产生的对象)】的数据 512M
为什么设置成这样就是理想状态呢?
  • 因为一次请求响应过程中大部分对象是会被回收的,所以只要一次请求响应和并发量不超过新生代的内存,就不会触发垃圾回收,或者说较少的触发垃圾回收,这样就能估算出理想值
幸存区

需要大到能够保留【当前活跃对象(要被回收)+ 需要晋升对象(要去老年代)】

如果设置的比较小,那么虚拟机会动态调整晋升阈值,这就会导致寿命还很小的对象晋升到了老年代,需要等到老年代触发垃圾回收时候才会回收,所以需要我们调整适当得值,使得能够在新生代垃圾回收时候回收掉

但是也需要让晋升阈值配置得当,让长时间存活对象尽快晋升。

三、垃圾回收_第43张图片

5、老年代调优

以CMS为例

  • CMS老年代内存越大越好
  • 一般先不进行调优,如果没有FULL GC那么说明当前的内存已经足够应对,否则可以尝试调优新生代,也就是调整新生代的伊甸园跟幸存区大小以及晋升值。
  • 如果新生代调优之后还发生FULL GC时,观察老年代内存占用,适当调大内存预设的1/4 ~ 1/3
    • -XX:CMSInitiatingOccupancyFraction=percent
    • 建议设置成超过百分之75的时候使用CMS进行垃圾回收,留下25%给浮动垃圾

你可能感兴趣的:(JVM,jvm,java,算法)