什么是垃圾回收
垃圾回收(Garbage Collection)是JVM垃圾回收器提供的一种用于空闲时间不定时回收无任何应用的对象占据的内存空间的一种机制。
Java中的引用
在Java中,内存分为栈内存和堆内存,其中堆内存又分为新生代和老年代。Java中的引用可以看做是一个指针,它指向在堆内存中分配的对象。Java中有四种引用类型,分别是强引用、软引用、弱引用和虚引用。
强引用
强引用是最普遍的引用,常见引用方式如下:
1 | Object obj = new Object(); |
这种显示声明的引用,比如obj
对Object
对象的引用,和str
对abcd
的引用都是强引用。
只要引用链没有断开(比如没有设置obj = null
),强引用就不会断开。即便内存空间不足,抛出OutOfMemoryError
终止程序也不会回收具有强引用的对象。
软引用
软引用是通过SoftReference
类来表示的。常见引用如下:
1 | SoftReference<Object> objSoftRef = new SoftReference<Object>(new Object()); |
在内存不足时JVM会回收该对象。常见的应用场景是页面缓存、图片缓存等。
弱引用
弱引用通过WeakReference
类来表示。常见引用如下:
1 | WeakReference<Object> objSoftRef = new WeakReference<Object>(new Object()); |
无论内存是否充足,都会回收弱引用关联的对象。ThreadLocal
中ThreadLocal.ThreadLocalMap
的实现就是用的弱引用。
虚引用
虚引用通过PhantomReference
类来实现。如下:
1 | ReferenceQueue queue = new ReferenceQueue(); |
虚引用仅仅只是提供了一种确保对象被finalize
之后来做某些失去的机制,当一个对象只有虚引用指向它时,如果JVM在下一次垃圾回收时决定回收该对象,就会将该虚引用加入到与之关联的引用队列中。
引用队列可以用来跟踪对象何时被回收,如果虚引用与之关联的对象被回收,就会在引用队列中添加一个通知。应用程序可以通过检查引用队列来了解对象何时被回收,并执行一些相应的操作,例如释放相关的资源或者记录相关的日志。
四大GC算法
Tracing算法(标记-清除算法)
标记-清除算法是最基础的收集算法,为例解决引用计数法的问题而提出的。它使用了根集(Root Set)的概念,分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程就是前面的根搜索算法中判定垃圾对象的标记过程
优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
缺点:
- 标记和清除的过程效率不高。而且需要使用一个空闲列表来记录所有的空闲区域以及大小。
- 容易产生大量不连续的内存碎片。
Copying算法(复制算法)
复制算法是先将内存一分为二。一块是对象块,新进程都是装入对象块,当这一块用完后,就将还存活的对象都复制到另一块——空闲块上面。然后交换两个块的角色,清除垃圾。
优点:比标记-清除高效
缺点:不适合存活对象较多的场合,比如老年代
Compaction算法(标记-整理算法)
该算法标记的过程和标记-清除算法中的标记过程是一样的,但在标记后,不是直接将对象清理,而是让所有存活的对象都向一端移动,最后清理掉端边界以外的内存。
优点:
- 相比于复制算法,空间利用率高。
- 相比标记-清除算法,不会产生内存碎片。
缺点:GC暂停的时间更长,而且要更新引用地址
分代算法
分代算法是以上几种算法的综合引用。它先根据对象的存活周期将内存分为几块:新生代和老年代。在JDK8以前还有个永久代,不过JDK8以后永久代就被元空间取代了。
现在堆大小=新生代+老年代。比例新:老=1:2
新生代
几乎所有新生成的对象首先都是放在年轻代的,也有少部分大对象(如大数组)是直接分配老年代的。
新生代采用复制算法,但不再是将内存一分为二,而是划分为eden区、survivor 0区、survivor 1区,比例(8:1:1),这个也是可以调整的。
刚生成的对象一般是在eden区,当eden区满了后,就进行一次新生代GC。将存活的对象复制到survivor 0区,此时survivor 1区是空白的。接着将新生成的对象放到eden区,直到survivor 0区也满了,则将eden区和survivor 0区的存活对象复制到survivor 1区,清理eden和survivor 0,此时,survivor 0区变成空白的,survivor 0和survivor 1 区功能互换(即eden区满了则GC后放到survivor 1区)。
每当对象熬过一次新生代GC,则年龄+1,直到15(默认的,可修改)后,将其复制到老年代。
老年代
老年代中存放一些生命周期比较长的对象,或者是一些大对象。
老年代GC的频率比较低,标记存活率高。一般采用标记-整理算法
Minor GC、Major GC、Full GC
JVM在进行GC时,并非每次都针对上面三个内存区域(新生代、老年代;方法区)一起回收的,大部分时候回收的都是新生代。
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种基本类型:一种是部分收集(Partial GC),另一种是整堆收集(Full GC)。
- 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
1、新生代收集(Minor GC / Young GC):只是新生代的垃圾收集
2、老年代收集(Major GC / Old GC):只是老年代的垃圾收集
3、混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。注意:目前只有CMS GC会有单独收集老年代的行为,<font color="red">很多时候Major GC和Full GC混淆使用</font>,需要具体分辨是部分收集还是整堆收集。
注意: 目前只有G1 GC会有这种行为
- 整堆收集:收集整个Java堆和方法区的垃圾收集。
Minor GC触发机制
当Eden区空间不足时,就会触发Minor GC。因为Java对象大多都具备朝生夕死的特性,所以Minor GC频繁,一般回收速度也比较快。
Minor GC会引发STW,暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行。
Major GC触发机制
当老年代空间不足时,会触发Major GC。如果出现了Major GC,经常会伴随至少一次的Minor GC(但并非绝对,Paralle Scavenge收集器的收集策略就有直接进行Major GC的策略选择过程)。
Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。
如果Major GC后,内存还不足,就报OOM了。
Full GC触发机制
触发Full GC执行的情况有如下五种:
1、调用System.gc()时,系统建议执行Full GC,但不是必然执行的
2、老年代空间不足
3、方法区空间不足
4、通过Minor GC后进入老年代的平均大小大于老年代的可用内存
5、由Eden区、S0区向S1区复制时,对象大小大于S1区可用内存,则把该对象转存到老年代,切老年代可用内存小于该对象大小。
Full GC时开发或调优中尽量要避免的。这样STW时间会短一些
总结
流程图如下:
针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to。
关于垃圾回收:频繁在新生代收集,很少在老年代收集,几乎不在永久代/元空间收集。
⚠️注意以下几点:
1、每次YGC后,都是把Eden区和S0(或S1)区还存活的对象放到S1(或S0)区。也就是说,每次YGC后,Eden区都是空的。
2、只有Eden区满了才会进行YGC,S0(或S1)区满了并不会触发YGC