一、如何判断对象可以回收
1、引用计数法
会造成死循环 (JVM 不是用的这种)
2、 可达性分析算法
- Java虚拟机中的垃圾回收采用可达性分析来探索所有存活的对象
- 确定一系列根对象,然后扫描一遍,判断每一个对象是否间接或者直接被根对象引用,如果找不到,表示可以回收
- 哪些对象可以作为GC Root?
可以使用以下工具
以上都可以当做GC Root
3、四种引用
1. 强引用
被强引用的对象,不能被垃圾回收
2. 软引用
- 被软引用的对象,如果触发了垃圾回收之后,发现还是内存不足,就会将软引用的引用的对象回收,反之,则可以继续使用。
软引用是一次Full GC之后内存不足才会回收,弱引用是一次Full GC之后就会回收
3. 弱引用
- 被弱引用的对象,如果触发了垃圾回收之后,不管内存是否充足,都会被垃圾回收。
软引用、弱引用所引用的对象如果被垃圾回收,就会加入一个叫做引用队列的空间,因为软引用 弱引用自身也是需要占用内存空间的,但是不好找,如果想要删除软引用或者弱引用,就需要配合引用队列来进行遍历删除。
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. 总结
二、垃圾回收算法
1、标记清除
清除只需要记录对应内存的起始地址,终结地址,然后存到一个空闲队列等待分配
2、标记整理
优点:没有内存碎片
缺点: 牵扯到内存的移动,效率很低
- 例如如果移动的内存被其他对象引用,那么就需要改变其他对象的引用,这时候效率就比较低了
操作系统知识:克服外部碎片可以通过紧凑技术来解决
3、复制
优点:不会产生碎片
缺点:占用双倍内存空间
三、分代垃圾回收
1、概念
新生代里面的垃圾是可以快速回收,老年代的垃圾是存活比较久
2、GC_相关参数
3、GC_分析
a.第一次垃圾回收
- 对象创建之后,会放入新生代伊甸园中,等到伊甸园装满了,就会出发一次Minor GC
- 然后根据可达性分析算法来判断哪些是Root GC,然后将存活的对象复制到幸存区To
- 然后经历了一次垃圾回收不死,寿命+1
- 复制算法,最后会交换From和 To的位置
交换
b.第二次垃圾回收
- 如果伊甸园再次装满,触发第二次垃圾回收Minor GC
- 就会将From和伊甸园中Root GC以及相关的对象放入幸存区TO,并且寿命+1
- 最后From 和 To交换位置,然后新对象就有伊甸园空间可以放入了
幸存区中的对象不会一直待着,会有一个预定值,寿命值超过15就会晋升到老年代中,存活时间比较久的空间
c. 第三次垃圾回收
- 如果老年代也装满了,伊甸园也装满了,那么这时候就会触发一次Full GC,然后进行一次大回收,将老年代跟新生代的清理。
- 如果还是内存不够,这时候就会报出outOfMemoryError 内存溢出错误
总结
- minor gc会引发stop the world(暂停其他用户的线程,垃圾回收线程执行完成之后恢复)
- 如果发现当前放入的对象太大,在老年代完全足够,然后伊甸园完全不够的情况下,寿命没有到15,也会自动晋升,放到老年代中。
- 如果一个线程出现了OutOfMemoryError,不会导致整个java进程的结束。
四、垃圾回收器
1、串行
-XX:+UserSerialGC= Serial+SerialOld
- 因为Serial 和 SerialOld 都是单线程的垃圾回收器,所以只有一个垃圾回收线程在运行
- 堆内存不够时,触发了垃圾回收,其他线程在一个安全点停下来,防止垃圾回收之后,变更了对象地址,不会导致程序错误,因为是单线程的垃圾回收器,所以只有一个垃圾回收线程在执行,其他都进行阻塞,等待垃圾回收线程执行完毕后,所有线程才恢复运行。
- 这个图适用于Serial ,也适用SerialOld,因为他们都使用的是单线程回收器,都会执行stop the world,只是使用的回收算法有所不同。
2、吞吐量优先
- 吞吐量主要衡量程序运行时间中有多少是用于实际工作的,而不是花费在垃圾回收上。
- 并行的:同一时刻进行
3、响应速度优先(CMS)
垃圾回收器在工作的同时,其他的用户线程也能同时进行,垃圾回收线程跟用户线程是并发执行
多个垃圾回收器并行执行,在此期间,不允许我们的用户线程继续运行换句话说就是stop the world (STW)
工作流程:
- 多个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
a. G1垃圾回收阶段
b.Young Collection
- 会STW
- 当伊甸园的大小逐渐占满,会触发新生代垃圾回收, 复制进幸存区,然后寿命+1,会STW
- 当幸存区的对象比较多了,那么就会继续进行复制到其他幸存区,如果寿命到达预值15,就会放到老年代
c. Young Collection + CM
- Young Gc时会进行GC Root 的初始标记
- 当老年代占用堆空间比例达到阈值时,进行并发标记。顺着初始标记的Root对象找到对应的引用对象(不会STW)
d. Mixed Collection
会对E、S、O进行全面的垃圾回收
- 有一些老年代经过并发标记阶段,也可能没用了
- 为什么老年代不是所有都进行拷贝,因为G1对老年代的回收,需要根据最大暂停时间来综合考虑,选择那些存活度低的进行回收,而不是所有都进行回收,这时候就会将这部分存活度比较低的复制到新的老年代区域,来达到时间跟效益的最大化。
- 这也就是Garbage First由来,优先回收垃圾最多的区域
e. FUll GC
- 串行老年代内存不足就触发
- 并行老年代内存不足就触发
- CMS老年代内存不足也不会触发,经过初始标记,并发标记等一系列流程,最后如果出现并发错误,就会替换成串行,触发FULL GC
- G1老年代内存不足也不会触发,经过并发标记,混合收集等一系列流程,最后如果回收的速度比产生垃圾的速度快,这时候还是处于并发垃圾收集的阶段,反之,并发收集失败,退化成串行收集,这时候FULL GC
f. Young Collection 跨代引用
初始标记的时候,会进行根的标记,那么会有一部分在老年代中,老年代一般都比较大,存活时间比较久,如果寻找需要遍历一遍老年代来标记,显然效率很低,因此,采取的是一种卡表的技术。
把老年代在进行划分,划分成一个个的card,每个card是512k
如果老年代中的卡引用了新生代中的对象,那么称为脏卡
那么到时候只需要检查那些脏卡就可以,提高效率
新生代会标记哪些引用了我,标记--脏卡 使用(Remembered Set)
会使用一个写屏障,每次发生引用变更,就会触发标记脏卡,存到脏卡队列,等待脏卡线程执行操作
g. Remark
三色标记法
- 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是 白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代 表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
也就是并发标记的时候,用户发生的引用变更,会在引用变更中间触发一个写屏障,然后加入到一个队列里面,防止出现删除引用错误的情况。
最后重新标记阶段就会从队列里面一个一个取出来,修改。
看书补充:
当且仅当以下两个条件同时满足时,会产生“对象消失”的问 题,即原本应该是黑色的对象被误标为白色:
·赋值器插入了一条或多条从黑色对象到白色对象的新引用;
·赋值器删除了全部从灰色对象到该白色对象的直接或间接引用 ;
解决方案:
增量更新(解决第一个条件)
- 如果插入了新纪录是从黑色对象指向白色对象,那么就会将黑色标记成灰色,
- 然后将这条要插入的引用记录给记录下来,然后并发扫描结束之后,以黑色对象为根,在进行一次扫描
原始快照(解决第二个条件)
- 如果删除了从灰色对象指向白色对象的记录,那么不管删除与否,都会执行原来的刚开始扫描那一刻的对象图快照来进行搜索
- 然后将这条要删除的引用记录给记录下来,然后并发扫描结束之后,以灰色对象为根,在进行一次扫描
h. JDK 8u20 字符串去重
i. JDK 8u40 并发标记类卸载
j. JDK 8u60 回收巨形对象
k. 写屏障
可以看做是虚拟机层面对“引用类型字段赋值”这个动作的AOP切面。 在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的 前后都在写屏障的覆盖范畴内。
-----新版本
a. G1垃圾回收器-内存结构
b. 垃圾回收方式
- 1、年轻代回收(Young GC) 回收新生代
- 2、混合回收(Mixed GC) 回收新生代 + 老年代
年轻代回收步骤:
优化:
c. 混合回收的执行流程
混合回收
如果在复制过程中,发现整个堆没有空间可以进行复制转移了,那么就会进行FULL GC
五、垃圾回收调优
查看虚拟机运行参数
1、调优领域
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、新生代调优
-
-
- new新对象会在伊甸园中分配,每一个线程都会在伊甸园中分配一块私有的区域也就是TLAB,new一个对象的时候,首先看一下TLAB缓冲区是否有可用内存,优先在这里进行对象分配
- 对象分配也有一个线程安全的问题,比如线程1要使用这块内存,那么线程2分配的时候就不能够使用这块内存,就造成内存分配混乱,所以需要考虑这个线程分配安全问题,JVM做的。
- 如何减少线程之间对内存分配时的并发冲突,TLAB,每个线程使用自己私有的伊甸园内存进行对象分配,这样就算多个线程并发创建对象,也不会对内存占用造成干扰
- TLAB thread-local allocation buffer
-
- 2、死亡对象回收代价为零
- 3、大部分对象用过即死
- 4、MinorGC的时间远远低于FUll GC
如何对新生代内存进行调优呢?
可能第一个想到的就是,调大新生代的内存空间大小
这是一个最有效的方式,当然我们也要考虑调大之后存在的一些问题。
调的太小,会经常触发minor Gc
调的太大,会导致老年代变小,容易触发Full Gc,时间更长
随着新生代空间越来越大,吞吐量越来越高,当新生代达到一定的大小的时候,吞吐量会有一定的下降,因为新生代空间变大了,相应的MInor Gc的时间也就变得久了,所以我们需要找到一个最优的点来进行设置。
补充:
但是一般我们还是越大越好原则,这里为什么跟前面不对应呢,这里没有考虑到一个因素,
也就是新生代使用的是标记-复制算法,比较耗时的是复制这一步操作,牵扯到内存的复制和移动,但是新生代中大部分对象都是用过就删的,所以存活的很少,所以我们复制这一步其实耗费的时间也没有那么长,相对于复制,标记的时间就更不值一提了,所以我们还是可以将内存调的很大,效率也不会有一个很明显的下降。
那么设置多少合适呢?
- 新生代能容纳所有【并发量 * (一次请求-响应过程中产生的对象)】的数据 512M
为什么设置成这样就是理想状态呢?
- 因为一次请求响应过程中大部分对象是会被回收的,所以只要一次请求响应和并发量不超过新生代的内存,就不会触发垃圾回收,或者说较少的触发垃圾回收,这样就能估算出理想值
幸存区
需要大到能够保留【当前活跃对象(要被回收)+ 需要晋升对象(要去老年代)】
如果设置的比较小,那么虚拟机会动态调整晋升阈值,这就会导致寿命还很小的对象晋升到了老年代,需要等到老年代触发垃圾回收时候才会回收,所以需要我们调整适当得值,使得能够在新生代垃圾回收时候回收掉
但是也需要让晋升阈值配置得当,让长时间存活对象尽快晋升。
5、老年代调优
以CMS为例
- CMS老年代内存越大越好
- 一般先不进行调优,如果没有FULL GC那么说明当前的内存已经足够应对,否则可以尝试调优新生代,也就是调整新生代的伊甸园跟幸存区大小以及晋升值。
- 如果新生代调优之后还发生FULL GC时,观察老年代内存占用,适当调大内存预设的1/4 ~ 1/3
-
- -XX:CMSInitiatingOccupancyFraction=percent
- 建议设置成超过百分之75的时候使用CMS进行垃圾回收,留下25%给浮动垃圾