垃圾回收
1.如何判断对象可以被垃圾回收的条件
1.1.引用计数法
- 如果一个对象被其他变量所引用,则让该对象的引用计数+1,如果该对象被引用2次则其引用计数为2,依次类推。
- 某个变量不再引用该对象,则让该对象的引用计数-1,当该对象的引用计数变为0时,则表示该对象没有被其他变量所引用,这时候该对象就可以被作为垃圾进行回收。
引用计数法弊端:循环引用时,两个对象的引用计数都为1,两个对象都不被使用了,但是两个对象的计数都不能归零,导致两个对象都无法作为垃圾被释放回收。最终就会造成内存泄漏!
1.2.可达性分析算法
判断Java对象是否是垃圾的算法: 可达性分析算法
该算法要判断根对象: 肯定不能被当成垃圾被回收的对象
在垃圾回收前,JVM会对堆内存的对象进行扫描,判断每一个对象是否能被**GC Root(根对象)**直接或者间接的引用,如果能被根对象直接或间接引用则表示该对象不能被垃圾回收,反之则表示该对象可以被回收:
- 可以作为GC Root的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
1.3.五种引用
1.3.1.强引用
上图实心线表示强引用:比如,new 一个对象M,将对象M通过=(赋值运算符),赋值给某个变量m,则变量m就强引用了对象M。
强引用的特点:只要沿着GC Root的引用链能够找到该对象,就不会被垃圾回收;只有当所有GC Root都不引用该对象时,才会回收强引用对象。
1.3.2.软引用
上图中宽虚线所表示的就是软引用
软引用的特点:当GC Root没有直接通过强引用指向软引用对象时,若垃圾回收后内存不足,则会回收软引用所直接引用的对象。因为此时JVM认为软引用对象不够重要,要把内存腾出来给别的对象.
1.3.3.弱引用
当GC Root没有直接通过强引用指向弱引用对象时,无论垃圾回收后内存是否足够,都会回收弱引用所直接引用的对象。
如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理。
无论内存是否足够,在回收弱引用所指向的对象时,弱引用本身也不会被清理。
如果想要释放软(弱)引用对象的内存,需要使用引用队列(也可不配合使用),手动释放内存
以下两种引用必须配合引用队列使用,两引用对象创建的时候都会关联一个引用队列
1.3.4.虚引用
在直接内存的创建ByteBuffer实现类对象时,会创建一个cleaner虚引用对象,ByteBuffer会分配一块直接内存空间,并且会把地址传递给cleaner虚引用对象;当强引用对象不引用ByteBuffer的时候,ByteBuffer就会自己被被垃圾回收掉,但是他的直接内存不能被垃圾回收,所以当ByteBuffer被垃圾回收后,JVM就会让cleaner虚引用对象放入到虚引用队列中,此时队列中就会有一个ReferenceHandler线程到线程中找有无新入队的虚引用,找到就会执行虚引用中的clean方法中的unsafe.freeMemory方法释放直接内存
1.3.5.终接器引用
所有对象都会继承自Object父类,Object父类中有个finallize(终结)方法,当GC Root对象没有重写finallize方法并且没有强引用引用重写中终极方法的对象时,他就可以被当成垃圾进行回收,当该对象没有被根对象强引用时,JVM会创建该对象的终接器引用,该对象被垃圾回收时,JVM会把终接器引用对象加入到引用队列,此时该对象没有立刻被垃圾回收,再由一个优先级很低的线程FinalizeHandler,他也会到引用队列中找有无新加入的终接器引用对象,若有,则根据终接器引用对象找到要作为垃圾回收的对象,并调用该对象的finalize方法,调用完后,下一次垃圾回收时就可以真正把该对象占用的内存回收掉
小结:
-
强引用:无论内存是否足够,不会回收。
-
软引用:内存不足时,回收该引用关联的对象。(可以选择配合引用队列使用,也可以不配合)。
-
弱引用:垃圾回收时,无论内存是否足够,都会回收。(可以选择配合引用队列使用,也可以不配合)。
-
虚引用:任何时候都可能被垃圾回收器回收。(必须配合引用队列使用)。
2.垃圾回收算法
2.1.标记-清除算法
内存释放不是把对象占用的字节清零,只需要把没有被GC Root对象引用链引用的对象所占用内存的起始结束的地址记录下来,放在空闲的地址列表里,下次再分配新对象时,会到空闲地址列表中找有无一块足够的空间容纳新对象,若有则给新对象内存分配,并不会内存清零
优点: 速度快,只需记录垃圾对象的地址
缺点: 易产生内存碎片(不连续的内存空间),无法给连续内存空间的新对象分配内存
2.2.标记-整理算法
避免标记清除时遗留下的内存碎片问题,在清理垃圾过程,把可用对象向前移动,让内存更紧凑
优点: 没有内存碎片
缺点: 效率低,速度慢,需要移动对象
2.3.标记-复制算法
-
当需要回收对象时,先将GC Root直接引用的的对象(不需要回收)放入TO中
需要被垃圾回收的对象在FROM中保持不动
-
然后清除FROM中的所有需要回收的对象
-
最后 交换 FROM 和 TO 的位置:(FROM换成TO,TO换成FROM)
复制算法:将内存分为等大小的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收FROM中不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。
缺点: 占用双倍的内存空间
3.分代垃圾回收
实际中,JVM中会多种垃圾回收算法一起结合协同工作,具体实现在JVM虚拟机中叫分代垃圾回收机制
3.1.分代回收原理
虚拟机把整个堆内存中大的区域划分为两块: 新生代,老年代
划分原因: Java中经常使用的对象放在老年代中,用完就丢弃的对象放在新生代中,这样就可以对对象不同生命周期特点进行不同垃圾回收策略
-
新生代垃圾回收发生频繁,处理的都是朝生夕死的对象,回收速度相对较快
-
老年代垃圾回收许久才发生一次,处理的都是更有价值会长时间存活的对象,回收速度相对较快
针对不同区域采用不同垃圾回收算法,更有效地对垃圾回收进行管理
新生代又划分三个小区域: 伊甸园,幸存区FROM,幸存区TO
3.2.新生代垃圾回收流程
新创建的对象都被放在了新生代的伊甸园中:
当伊甸园创建的对象过多,导致内存不足时,会触发一次新生代的垃圾回收MinorGC,先标记伊甸园中不被清理的对象,标记完通过复制算法把伊甸园中幸存对象复制到幸存区TO中,并让他们寿命+1,再交换FROM和TO。完后伊甸园内存就被释放
同理,继续向伊甸园新增对象,如果满了,则进行第二次Minor GC:
流程相同,仍需要存活的对象寿命+1:(下图中FROM中寿命为1的对象是新从伊甸园复制过来的
再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1!
如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代中:
如果新生代老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收:
每次新生代执行Minor GC 后,eden和from里不被引用的对象就被回收掉
当伊甸园区和FROM区内存都不足以放入新对象时,由于内存紧张的原因,新对象就会提前进入老年代
小结:
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
- minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
- 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,使得STW的时间更长
3.3.相关VM参数
参考文章: JVM常用内存参数配置
3.4.GC分析
大对象处理策略:
大对象在老年代空间足够,但新生代空间肯定不够的情况下,会直接晋升到老年代,不会引起新生代的GC
线程内存溢出:
某个子线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行。
这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常。
4、垃圾回收器
JVM中垃圾回收器的结构图:
相关概念
并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上
吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(
吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间)
),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
。
4.1. 串行垃圾回收
使用场景:
- 单线程运行
- 适用场景:堆内存较小,个人电脑(CPU核数较少)。
开启串行回收器的JVM参数 : -XX:+UseSerialGC = Serial + SerialOld
Serial
:表示新生代,采用复制算法;SerialOld
:表示老年代,采用的是标记整理算法。
安全点:让其他线程都在这个点停下来,以免垃圾回收移动对象地址时,使得其他线程找不到被移动的对象。
因为是单线程,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态。
4.1.1 Serial 收集器
Serial(新生代)收集器是最基本的、最早的收集器:
特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有用户的线程工作(Stop The World),直到它收集结束。会出现延迟(卡顿).性能不高.
4.1.2 ParNew 收集器
ParNew收集器其实就是Serial收集器的多线程版本:
特点:多线程,多个线程并行执行、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads
参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题,单CPU下,需要切换线程,可能不如Serial
4.1.3 Serial Old 收集器
Serial Old是Serial收集器的老年代版本:
特点:同样是单线程收集器,采用标记-整理算法。一般不直接使用,而是作为CMS垃圾收集失败的后备方案
具体回收过程:
4.2. 吞吐量优先垃圾回收器
吞吐量: 垃圾回收时间占程序运行时间的占比,占比越低,吞吐量越高
使用场景:
- 多线程运行
- 适用场景:堆内存大,多核CPU(适合服务器)。
- 单位时间内,让STW(stop the world,停掉其他所有工作线程)时间最短
- JDK1.8默认使用的垃圾回收器
并行的新生代垃圾回收器,和并行的老年代垃圾回收器(默认开启)
-XX:+UseParallelGC~-XX:+UseParallelOldGC
// 2.采用自适应的大小调整策略:调整新生代(伊甸园 + 幸存区FROM、TO)内存的大小
-XX:+UseAdaptiveSizePolicy
// 3.调整吞吐量的目标:吞吐量 = 垃圾回收时间/程序运行总时间
-XX:GCTimeRatio=ratio
// 4.垃圾收集最大停顿毫秒数:默认值是200ms
-XX:MaxGCPaiseMillis=ms
// 5.控制ParallelGC运行时的线程数
-XX:ParallelGCThreads=n
4.2.1 Parallel Scavenge 收集器
这个垃圾收集器是JDK1.8的JVM默认的新生代垃圾收集器
与吞吐量关系密切,故也称为吞吐量优先收集器:
特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与ParNew收集器类似)。
与ParNew收集器最重要的一个区别:该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略。
部分JVM参数
java -XX:+PrintCommandLineFlags -version
: 打印JVM默认初始化堆和最大堆大小以及
GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy
参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation
)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold
)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
Parallel Scavenge收集器使用两个参数控制吞吐量:
XX:MaxGCPauseMillis
控制最大的垃圾收集(暂停)停顿时间。每次GC的时间将尽量保持不超过设置的值,暂停时间不是越短越好,因为时间短了,GC收集暂停时间越短,GC的次数就越频繁
XX:GCRatio
直接设置吞吐量的大小。
4.2.2 Parallel Old 收集器
这个垃圾收集器是JDK1.8的JVM默认的老年代垃圾收集器
是Parallel Scavenge收集器的老年代版本:
特点:多线程,采用标记-整理算法(老年代没有幸存区)。
4.3. 响应时优先垃圾回收器
使用场景:
- 多线程运行
- 适用场景:堆内存大,多核CPU(适合服务器)。
- 尽可能让单次STW时间变短(尽量不影响其他线程运行)
4.3.1 CMS 收集器
jdk1.8开启CMS收集器自动与ParNew新生代收集器一起配合使用
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器:
特点:基于标记-清除算法实现。多线程,并发收集、低停顿,但是会产生内存碎片。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器的运行过程分为下列4个阶段:
简单总结:
补充: 重新标记: 只追踪并发标记过程中产生变动的对象,产生变动的对象会放入到一个队列中供重新标记工程遍历
初始标记:标记GC Roots能直接关联到的对象。速度很快但是仍存在Stop The World问题。
并发标记:进行GC Roots 整个链路的过程,找出存活对象,同时和用户线程并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
并发清除:对标记判断已经死亡的对象进行清除回收,不会暂停其他用户线程。
其中初始标记和重新标记需要暂停用户线程,其他阶段都是并发执行,所以总体上暂停时间更短.
CMS收集器的内存回收过程是与用户线程一起并发执行的。
jdk1.8用并行那个,cms后续因为有问题被淘汰了
4.4.G1
定义:优先收集垃圾最多的区域,主要目的是达到暂停时间短
Garbage First,JDK 9以后默认使用,而且替代了CMS 收集器,G1和CMS都是并发的垃圾回收器:
适用场景:
- 同时注重吞吐量和低延迟(响应时间)。也是并发执行的垃圾收集器
- 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域,每个区域都可以独立作为伊甸园,幸存区,老年代。化整为零,加快标记,拷贝速度
- 整体上是标记-整理算法避免CMS收集器产生的碎片问题,两个区域之间是复制算法。
相关参数:JDK8 并不是默认开启的,需要参数开启:
// G1开关
-XX:+UseG1GC
// 所划分的每个堆内存大小:必须是1248
-XX:G1HeapRegionSize=size
// 垃圾回收最大停顿时间
-XX:MaxGCPauseMillis=time
4.4.1.G1垃圾回收阶段
这是个循环的过程:
新生代垃圾收集–>老年代内存超过阈值,新生代垃圾收集+并发标记–>上一个阶段垃圾收集完毕,进行混合收集: 会对新生代 幸存区和老年代进行较大规模收集–>伊甸园内存都被释放掉,会重新回到新生代垃圾收集
每个阶段的工作流程:
4.4.2 Young Collection 新生代垃圾回收-第一阶段
G1会对堆内存划分成大小相等一个个区域,每个区域都可以独立作为伊甸园,幸存区,老年代,刚开始类加载创建的对象会分配到伊甸园区,伊甸园区会提前设好大小,
当伊甸园区内存被占满,会触发Young Collection,同时也会触发STW,但时间较短,该垃圾回收会将幸存对象以复制算法放入到幸存区,
程序再工作一段时间,幸存区的对象多了,并且部分幸存对象存活年龄超过一段时间,又会触发一次新生代垃圾回收,幸存区部分超过年龄的对象会晋升到老年代,不够年龄的会再次复制到另一个幸存区中,伊甸园幸存的新对象也会复制到新的幸存区
4.4.3 Young Collection + CM (Current Mark 新生代垃圾回收和并发标记)-第二阶段
- 在 Young GC 时会对 GC Root 根对象进行初始标记。不会占用到老年代的内存
- 在老年代占用堆内存的比例达到阈值时,进行并发标记(不会STW),阈值可以根据用户来进行设定:
4.4.4 Mixed Collection 混合收集-第三阶
E:伊甸园 S:幸存区 O:老年代
会对E、S 、O 进行全面的回收。
混合收集的两个步骤:
-
最终标记 先STW,再标记,对之前并发标记产生的浮动垃圾,可能改变并发标记对象的引用,对并发标记结果产生影响
-
拷贝存活 先STW,老年代把垃圾最多的区域回收,拷贝部分存货对象
混合收集阶段的新生代垃圾回收 -
伊甸园区的幸存对象会被复制到新的幸存区中,其他幸存区的幸存对象也会复制到新的幸存区中,幸存区中符合晋升条件的会晋升到老年代中,
混合收集阶段的老年代垃圾回收 -
老年代区域经过并发标记标记存活对象和死亡对象,把老年代中幸存对象复制到新的老年代区中,但并不会保存所有的幸存对象,因为G1回收器会根据最大暂停时间挑出回收价值最高的区域进行回收,要复制保留的对象少了,就可以达到最大暂停时间的目标,垃圾回收时间就变短.复制一方面是为了保留幸存对象,一方面是为了整理内存减少空间碎片
4.4.5 Full GC
CMS和G1老年代内存不足触发的垃圾收集器不同
用G1举例: (CMS情况类似)
当老年代的内存占整个堆内存的比例达到45%阈值时(默认),G1就会触发并发标记和混合收集两个阶段,两个阶段工作时,若回收速度高于用户线程产生垃圾的速度,这时还是并发垃圾收集的阶段,暂停时间短,还不是fullGC;
当G1的垃圾回收速度低于用户线程产生垃圾的速度,并发收集就会失败,此时会退化为串行的fullGC,速度很慢,暂停响应时间更长
4.4.6 Young Collection 跨代引用
例子: 老年代中有个List集合,当需要往集合中add新对象,此时新对象被创建在新生代的伊甸园区,加入到集合后就变成老年代引用新生代
用卡表的技术,加速新生代垃圾回收: 把老年代细分为一个个card,每个卡约为512k,若老年代中有一个卡中的的对象引用了新生代的对象,这个卡就对应为脏卡,加快遍历GC Root 根对象,提高效率
- 粉红色区代表脏卡区,新生代伊甸园中有 Remembered Set ,将来通过 新生代对伊甸园做垃圾回收的时候可以先从Remembered Set找到对应的脏卡当中 遍历 GC Root ,减少GC Root 的遍历时间
- 在引用变更时,通过写屏障(post-write barrier) 完成脏卡更新操作,会把脏卡的指令放在脏卡队列中,将来由一个线程完成脏卡更新操作
Remembered Set: 保存外部对新生代中幸存对象的引用(脏卡)
4.4.7 Remark (三色标记法)
表示并发标记状态对象间的引用情况
被箭头引用的对象都是强引用对象,垃圾回收时会被保留下来;黑色表示处理完毕,灰色表示正在处理,白色表示未被处理,被箭头引用的灰色和白色对象最终都会变成黑色
当出现一种情况,在并发标记还没结束过程中,在C被B引用,由于用户线程修改了C对象的引用地址,比如当C对象被A对象作为属性引用,因为C之前被处理过是被当做垃圾回收的对象,而A则是处理过的,以后不会处理他了,等到整个并发标记结束后,C对象就被遗漏了,实际上C被A对象引用了,但是却被误认为是垃圾回收掉
为了解决对引用做进一步检查,会用到 remark 重新标记阶段,防止标记遗漏的现象发生
原理如下:
当对象的引用地址发生改变时,JVM会给该对象加入一个写屏障,写屏障的代码就会被执行,写屏障就会把该对象加入到一个队列中,并且该对象改为正在处理的状态表示还未处理完,等到整个并发标记结束了,进入重新标记阶段,先STW,这时重新标记线程就会把队列中的对象取出再做检查,当发现队列中的对象是灰色的就进一步判断处理,当发现有强引用引用着他就不会让他被误当做垃圾回收掉
通过写屏障技术,在对象引用地址改变前,加入一个队列并且表示为正在处理的
4.4.8 JDK 8u20 字符串去重
优点与缺点:
- 优点:节省了大量内存。
- 缺点:新生代回收时间增加,导致略微多占用CPU。
字符串去重开启指令 -XX:+UseStringDeduplication
:
案例分析:
String s1 = new String("hello");// 底层存储为:char[]{'h','e','l','l','o'}
String s2 = new String("hello");// 底层存储为:char[]{'h','e','l','l','o'}
- 将所有新分配的字符串(底层是
char[]
)放入一个队列。 - 当新生代回收时,G1并发检查是否有重复的字符串。
- 如果字符串的值一样,就让他们引用同一个字符串对象。
注意,其与String.intern()
的区别:
intern
关注的是字符串对象。
字符串去重关注的是char[]
数组。
在JVM内部,使用了不同的字符串标。
4.4.9 JDK 8u40 并发标记类卸载
在所有对象经过并发标记阶段以后,就能知道哪些类不再被使用。如果一个类加载器
的所有类都不在使用
时,则卸载它所加载的所有类
。
并发标记类卸载开启指令:-XX:+ClassUnloadWithConcurrentMark
默认启用。
4.4.10 JDK 8u60 回收巨型对象
- H表示巨型对象,当一个对象占用大于region的一半时,就称为巨型对象。
- G1不会对巨型对象进行拷贝。
- 回收时被优先考虑。
- G1会跟踪老年代所有incoming(巨型对象的脏卡)引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。
巨型对象越早回收越好,最好是在新生代的垃圾回收就回收掉~