JVM笔记 —— 垃圾回收(GC)详解

一、垃圾回收的分类

针对HotSpot JVM的实现,它里面的GC其实准确分类只有两大种:

  • Partial GC:部分收集模式

    • Young GC:只收集年轻代的GC
    • Old GC:只收集老年代的GC。只有CMS中有这个模式。
    • Mixed GC:收集整个年轻代以及部分老年代的GC。只有G1有这个模式
  • Full GC:收集整个堆和方法区。

堆是垃圾回收的主要区域,方法区很少会被回收。
本文所讨论的均指HotSpot JVM

二、死亡对象判断方法

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

1. 引用计数法

给每个对象中添加一个引用计数器:

  • 每当有一个地方引用它,计数器就加 1;
  • 每当引用失效,计数器就减 1;
  • 任何时候计数器为 0 的对象就是不可能再被使用的。

但是引用计数法很难解决对象之间循环引用的问题,因此目前主流的虚拟机中并没有选择这个算法。

在这里插入图片描述

2. 根可达性分析算法

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。
在这里插入图片描述

哪些对象可以作为 GC Roots 呢?
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 类静态常量引用的对象
  • 常量池中被引用的对象
  • 所有被同步锁持有的对象

对象被回收前如果该对象重写了finaize()方法则需先执行此方法后才能被回收。Object 类中的 finalize 方法一直被认为是一个糟糕的设计,影响了 Java 语言的安全和 GC 的性能,JDK9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。
参考:Java基础知识点之finalize方法详解

三、引用类型分类

无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。

Java中将引用分为了强引用、软引用、弱引用、虚引用四种。非强引用通常用来指向某些只需要暂时缓存的数据。

1. 强引用

引用变量默认就是强引用,以下其它三种将引用通过特殊包装的才能形成其它引用。
强引用的对象在GC Roots可达时不会被回收。

2. 软引用 SoftReference

软引用是一种相对强引用弱化了一些的引用,用java.lang.ref.SoftReference实现,可以让对象豁免一些垃圾收集。在可达时,当系统内存充足的时不会被回收,系统内存不足时则会被回收。

注意:SoftReference引用本身是强引用,它内部的(T reference)才是真正的软引用字段,SoftReference就是一个装软引用的容器而已。下面的WeakReference和PhantomReference也是一样的。

public class SoftReferenceDemo { 
     public static void main(String[] args) { 
         Object a = new Object(); 
         SoftReference<Object> softReference = new SoftReference<>(a);//获取a指向对象的一个软引用,放在SoftReference对象中
         //a和软引用指向同一个对象
         System.out.println(a);//java.lang.Object@4554617c
         System.out.println(softReference.get());//java.lang.Object@4554617c 10 
         //内存够用,软引用不会被回收
         a==null;
         System.gc();//内存够用不会自动gc,手动唤醒gc
         System.out.println(softReference.get());//java.lang.Object@4554617c 16 
         //内存不够用时
         try {            //配置Xms和Xmx为5MB
             byte[] bytes = new byte[1024*1024*30];//设置30MB超内存
         } catch (Throwable e){e.printStackTrace();}
           finally {
             System.out.println(softReference.get());//null,被回收
 		 } 
 	 }
 }

软引用的一个应用场景:一个应用需要读取大量的本地图片,如果每次读取都从硬盘读取会严重影响性能,如果一次性全部加载到内存,内存可能会溢出。可以使用软引用解决这个问题,使用一个HashMap来保存图片路径和图片对象管理的软引用之间的映射关系,内存不足时,JVM会自动回收缓存图片对象的占用空间,有效地避免了OOM问题。

//Map<图片路径,图片对象软引用>,在系统内存不足时value所指向的对象会被回收
Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>

3. 弱引用 WeakReference

弱引用需要用java.lang.ref.WeakReference实现,它比软引用的生存期更短,对于弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否够,都会回收该对象的占用内存。

Object a = new Object(); 
WeakReference<Object> softReference = new WeakReference<>(a);//获取a指向对象的一个弱引用

Map中还有一个WeakHashMap,WeakHashMap就是一种弱引用的map,内部的key为弱引用,在GC时如果key指向的对象不存在其它强引用的情况下会被回收掉,而对于value的回收会在下一次操作map时回收掉,所以WeakHashMap适合缓存处理。

4. 虚引用 PhantomReference

虚引用要通过java.lang.ref.PhantomReference类来实现,虚引用不会决定对象的生命周期,如果一个对象只有虚引用,就相当于没有引用,在任何时候都可能会被垃圾回收器回收。它不能单独使用也不能访问对象,虚引用必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅是提供一种确保对象被finalize以后,做某些事情的机制。


因此总结如下,在一次GC中,用于可达性分析的GC Roots本身不会被回收,GC Roots引用链不可达对象的必然会被回收,而在引用链可达的情况中:

  • 强引用的对象不会被回收
  • 仅有软引用的对象在内存不足时会被回收
  • 仅有弱引用或虚引用的对象必然会被回收

在这里插入图片描述

参考:JVM中如何理解强引用、软引用、弱引用、虚引用?
阿里面试:说说强引用、软引用、弱引用、虚引用吧

四、什么是内存泄漏

严格来说,如果某些对象在程序中不会再被用到了,但是这些对象又无法被垃圾收集器回收(GC Roots以及其引用链可达的强引用对象),那么这些对象所占用的内存就处于平白浪费的状态了,这就的内存泄漏。如果这种情况可以累积,随着内存泄漏的增多,就会导致严重的性能问题甚至OOM。

宽泛地说,实际情况中很多时候一些不太好的实践会导致对象的生命周期变得过长,比如不合理地进入了老年代,在老年代中堆积,等到Full GC时才能被回收,这种情况也可以叫“内存泄漏”。

在实际场景中可大概分为以下几种情况:

1. 类变量中引用短期对象

类变量在垃圾回收时被作为GC Roots,而类变量的生命周期一般和JVM程序一致,只有方法区中的对应类被回收才有可能被回收。如果在类变量对象中引用很多短期内使用的对象,那么由于在GC Roots下被强引用,这些短期对象都得不到回收,就造成了内存泄漏。如下一个静态list的例子:

public class test {
    static List list = new ArrayList<>();
    //如果object只是短期内需要使用的对象,那么如果这个方法一直被调用,就会造成内存泄漏
    public void oomTest() {
        Object object = new Object();
        list.add(object);
    }
}

2. 各类连接泄漏

例如数据库连接。如果在获取数据库连接后没有正确的归还或关闭,导致每次访问都创建一个未关闭的连接,就导致了内存泄漏。

案例:多线程访问数据库导致内存泄露的优化过程
数据库连接池内存泄漏问题的分析和解决方案

3. ThreadLocal用完没有remove

4. 内部类持有外部类

如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。

5. 堆外内存泄漏(NIO)

6. 改变哈希集合关键字的hash值

当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。

因为String是不可变类型,我们可以放心地把String 存入HashSet,或者把String当做HashMap的key值。

7. 缓存泄漏

内存泄漏的一个常见来源是缓存,一旦你把对象引用放入到缓存中,就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。
对于这个问题,可以使用WeakHashMap(弱引用)代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。

参考:https://blog.csdn.net/weixin_43899792/article/details/124304136

五、垃圾收集算法

分代收集理论

现代虚拟机的垃圾收集器大多都遵循了分代收集理论,这个理论建立在三个经验假说之上:

  • 弱分代假说:绝大多数对象都是朝生夕灭的。
  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
  • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构,这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。

以下简单讲解下三种垃圾回收方法论

1. 标记 - 清除算法

最早出现也是最基础的垃圾收集算法是标记 - 清除算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收清除掉所有被标记的对象。也可以反过来。

缺点:
执行效率不稳定,如果堆中有大量对象需要回收就必须进行大量的标记与清除动作。
会导致内存空间碎片化,创建对象时需要耗费资源寻找合适的空闲空间,并且创建较大对象时可能找不到足够的连续内存。

如果JVM采用的垃圾收集器采用的是标记清除算法,则堆内存是不规整的,已使用的内存和未使用的内存相互交错,那么虚拟机采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录哪些内存块是可用的,分配内存时在列表中找到一块足够大的空间分配给对象,再更新列表。

2. 标记 - 复制算法

标记 - 复制算法将要回收的空间分为几个区域,注意不要被标记复制的名字忽悠了,标记清除算法中分为标记和清除两个阶段,但是标记复制算法并没有标记阶段,为什么呢?首先要明确判断对象是否存活的核心思想是用根可达算法找出存活对象,由于标记清除算法需要回收垃圾对象,所以需要对存活对象进行标记,然后再清除不可用对象。而复制算法是要复制存活对象到另一块区域,所以在根可达算法发现存活对象后是直接复制到另一块区域,即在根可达分析过程中就已经完成了筛选(复制),待复制完成后,直接清理掉另一块区域即可,所以不需要挨个标记然后去清除。

优点:执行效率较高,且不会产生内存碎片
缺点:内存空间不能充分利用,需要保留一块区域用于下一次垃圾回收时复制存活对象。

Hotspot JVM的堆空间中年轻代的垃圾回收就是基于这种算法,年轻代划分为一个Eden区和两个Survivor区,内存比例默认为8:1:1,一个Survivor区保留,其余可使用,垃圾回收时只需要将通过根可达分析确认存活的对象复制到保留的Survivor区。但是不是每次都只有10%的对象存活,因此需要老年代做分配担保,如果Survivor区空间不足,则将一部分对象直接晋升老年代。

3. 标记 - 整理算法

标记 - 复制算法除了不能充分利用空间,且对象存活率高时需要进行较多的复制操作,因此在存活率较高的老年代就不能采用这种算法。
针对老年代存活率高的特性,有人提出了标记 - 整理算法,标记过程与标记 - 清除算法一样,但是标记完成后不是清除,而是将剩余存活对象向一端移动进行整理。

优点:解决了垃圾回收后空间碎片化的问题,且相比标记 - 复制算法无需保留区域,更充分使用空间
缺点:如果每次回收后都有大量对象依然存活需要移动(例如在存活率高的老年代),则需要更新所有用到这些对象的引用,这将会是一种负担很重的操作,并且这种移动对象的操作要暂停所有用户线程才能进行(这种暂停也被称为STW,stop the world)

如果JVM采用的垃圾收集器采用的是标记整理算法,即回收后会将剩余对象整理到连续的内存空间,使得堆内存规整,则JVM中给对象分配内存的方式是指针碰撞,即在已使用的空间后连续分配内存,以继续保持内存规整。

六、经典垃圾收集器

如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。《Java虚拟机规范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、不同版本的虚拟机所包含的垃圾收集器都可能会有很大差别,不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特点和要求组合出各个内存分代所使用的收集器。

1. Serial / Serial Old / ParNew 收集器

Serial收集器:单线程收集器,并且在进行垃圾收集工作时必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。采用标记复制算法。
Serial Old 收集器:Serial收集器的老年代版本,采用标记整理算法。
在这里插入图片描述
ParNew收集器:Serial收集器的多线程版本,除了使用多线程进行垃圾回收外其他与Serial一样
在这里插入图片描述

2. Parallel Scavenge / Parallel Old 收集器

Java8的默认垃圾收集器组合

Parallel Scavenge收集器:也是使用标记-复制算法的多线程收集器,关注点是吞吐量(高效率的利用 CPU),提供了很多参数供用户找到最合适的停顿时间或最大吞吐量。
Parallel Old 收集器:Parallel Scavenge的老年代版本。使用多线程和“标记-整理”算法

在这里插入图片描述

3. CMS收集器

CMS(Concurrent Mark Sweep)收集器:基于标记清除算法当内存碎片过多时再采用标记整理算法以获得规整的内存空间。是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。并且是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。它在GC过程中使用三色标记法来标记对象,黑色表示当前对象及其直接引用到的对象完成了标记,灰色表示当前对象完成了标记,但是其引用到的对象还没完成标记,白色表示未标记的对象。其垃圾收集步骤如下:

  • 1)初始标记:STW,标记GC Roots直接关联的对象,将GC roots标记为黑色,将GC roots直接引用的对象标记为灰色。耗时短。
  • 2)并发标记:根据GC Roots遍历整个对象图进行标记,首先为灰色,引用到的对象都标记为灰色后就升级为黑色。耗时长但可与用户线程并发进行
  • 3)重新标记:STW,并发标记期间用户线程可能会对引用进行变动,例如将一个垃圾对象又引用起来。因此需对并发标记期间产生的变动进行增量更新标记,耗时短
  • 4)并发清除:对所有未被标记到的白色对象进行垃圾回收,可并发进行

可以看到只有在耗时较短的初始标记和重新标记这两个阶段需要STW,因此能够获得较短的停顿时间
在这里插入图片描述
参考
https://zhuanlan.zhihu.com/p/451245000
https://blog.csdn.net/m0_67788957/article/details/123737277

4. G1 收集器

Java9后的默认垃圾收集器

作为CMS收集器的替代与继承者,G1 收集器是一款“停顿时间模型”的收集器,可以在延迟可控的情况下获得尽可能高的吞吐量。它将堆内存划分为多个大小相等的独立区域(Region),每个区域都可以根据需要动态扮演新/老年代中的空间,G1 收集器会跟踪每个区域垃圾回收的“价值”,并在后台维护了一个优先列表,每次可根据允许的收集时间,优先选择回收价值最大的 一些Region(这也就是它的名字 Garbage-First 的由来) 。

在这里插入图片描述

在这里插入图片描述

参考:JavaGuide

5. ZGC收集器

https://www.bilibili.com/video/BV1xF411B7vZ
https://blog.csdn.net/lovewangyihui/article/details/122442440

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是几个 JVM 垃圾回收相关的面试题及其答案: 1. 什么是垃圾回收垃圾回收是指在程序运行过程中,自动回收不再使用的内存空间,以提高内存利用率和程序的性能。 2. JVM垃圾回收机制是什么? JVM垃圾回收机制采用分代收集算法,将内存分为新生代和老年代,分别采用不同的垃圾回收算法来回收内存。 3. 新生代垃圾回收器有哪些? 新生代垃圾回收器主要有 Serial、ParNew、Parallel Scavenge 等。 4. 老年代垃圾回收器有哪些? 老年代垃圾回收器主要有 Serial Old、Parallel Old、CMS、G1 等。 5. 什么是对象的引用计数算法? 引用计数算法是一种简单的垃圾回收算法,它通过记录每个对象被引用的次数,当引用次数为 0 时,即可将该对象回收。 6. 引用计数算法的缺点是什么? 引用计数算法的缺点是无法处理循环引用的情况,如果两个对象之间相互引用,它们的引用计数一直不为 0,导致无法回收。 7. 什么是标记-清除算法? 标记-清除算法是一种常见的垃圾回收算法,它将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段,标记所有活跃对象,将其打上标记;在清除阶段,清除所有未标记的对象。 8. 标记-清除算法的缺点是什么? 标记-清除算法的缺点是产生大量的内存碎片,导致内存利用率降低。 以上是一些常见的 JVM 垃圾回收面试题及其答案,希望能对你有所帮助。在面试过程中,需要根据具体的问题进行回答,同时也需要对垃圾回收机制和算法有清晰的认识,才能更好地回答相关的问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值