【JVM】一文读懂垃圾回收机制

如何判断对象可以被回收

为了确定哪些对象是垃圾,jvm为我们提供了一些算法去判定。常见的判断是否存活有两种方法:引用计数法和可达性分析。

引用计数法

为每一个创建的对象分配一个引用计数器,用来存储该对象被引用的个数。当该个数为零,意味着没有人再使用这个对象,可以认为“对象死亡”。每当有一个地方去引用它时候,引用计数器就增加1。但是,这种方案存在严重的问题,就是无法检测“循环引用”:当两个对象互相引用,它俩的计数都不为零,因此永远不会被回收。而实际上对于开发者而言,这两个对象已经完全没有用处了。

缺点:如果两个对象互相引用,计数器都为1,即使他们都没有被使用,都不会被清理。这种方法不使用本java虚拟机中。

互相引用的情况

可达性分析算法

可达性分析基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。不能到达的则被可回收对象。

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有的对象
  • 扫描堆中的对象,判断是否能根据GC Root的引用链找到该对象,找不到则回收。

哪些对象可以作为GC Root?

  • 虚拟机栈(帧栈中的本地变量表)中引用的对象。
  • 方法区中静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 JNI 引用的对象。

MAT是eclipse出品的一个java内存的分析工具 ,可以用来找到可以作为GC Root的对象。

通过下面这个案例进行分析:

/**
 * 演示GC Roots
 */
public class Demo2_2 {

    public static void main(String[] args) throws InterruptedException, IOException {
        List<Object> list1 = new ArrayList<>();
        list1.add("a");
        list1.add("b");
        System.out.println(1);
        System.in.read();  //停留等待键盘输入数据

        list1 = null;
        System.out.println(2);
        System.in.read();  //停留等待键盘输入数据
        System.out.println("end...");
    }
}

运行代码,此时代码停留在第一步,使用 jps 命令查看当前进程ID:

 使用   jmap -dump:format=b,live,file=文件地址.bin 线程ID 命令将当前内存情况转储成一个文件:

然后在运行窗口随便输入一个什么,让程序向下运行,然后再转储现在的内存情况。

接着使用MAT工具打开生成的两个文件:

在list=null后,GC Root中list对象被回收。 

Java中的五种引用

无论使用何种方法判断对象是否存活,都和引用息息相关,引用有以下几种:

强引用 软引用 弱引用 虚引用 终结器引用

强引用

一般平常代码中大部分引用都是强引用,指向某一对象的所有强引用都断开,该对象才能被回收。

软、弱引用

软引用和弱引用都用来描述一些还有用,但是非必须的对象。弱引用比软引用更弱。

指向某一被软、弱引用的对象的所有强引用都断开,该对象可能被回收。

  • 软引用:如果垃圾回收之后,内存依然不足,只被软引用的对象会被回收。
  • 弱引用:只要发生垃圾回收,只被弱引用的对象就会被回收。也就是只能生存到下次垃圾回收之前。
  • 对象回收后,软弱引用本身转移到引用队列中。
  • 遍历引用队列,释放引用。

        

虚引用

  • 虚引用的ByteBuffer,没有被强引用,被回收掉,分配的直接内存尚未回收
  • 虚引用进入引用队列中,RefferenceHandler在队列中寻找到虚引用Cleaner
  • 调用Unsafe.freeMemory()方法释放直接内存;
  • 释放引用。

终结器引用

  • 终结器对象引用的对象没有被强引用,在被回收前,终结器引用转移到引用队列,一个优先级较低的线程finallize在引用队列中寻找终结器引用;
  • 并找到终结器引用的对象,调用finalize()方法;
  • 下次垃圾回收时,回收该对象。

回收算法

标记清除算法

“标记-清除”算法是最基础的收集算法。算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象(标记过程参见1.2可达性分析)。后续的收集算法都是基于这种思路并对其不足加以改进而已。
“标记-清除”算法的不足主要有两个:

  • 效率问题:标记和清除这两个过程的效率都不高
  • 空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

复制算法

复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。这样每次只需要对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等复杂情况,只需要移动指针,按照顺序分配即可。

  • 不会有内存碎片
  • 需要占用双倍内存空间

 标记整理算法

标记整理算法在标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。

  • 速度慢
  • 不会有内存碎片

流程图如下:

 分代收集算法

不同对象的生命周期是不一样的,不同周期对象课采用不同垃圾回收算法,以提高效率。这种算法没什么特别的,无非是上面内容的结合罢了,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。

虚拟机中的共划分为三个代:新生代(Young Generation)老年点(Old Generation)持久代
(Permanent Generation)
。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系
不大。年轻代和老年代的划分是对垃圾收集影响比较大的。

新生代

HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden(伊甸园)区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

GC刚开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着,From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

 老年代

当老年代空间不足,尝试Minor GC,空间仍不足,触发Full GC,触发 STW,时间相比于Minor GC会长的多,因为新生代和老年代回收算法不同,且老年代中对象多,回收较为复杂,采用标记-清理算法或者标记-整理算法。

Minor GC 和 Full GC的区别

       新生代GC(Minor GC):Minor GC指发生在新生代的GC,因为新生代的Java对象大多都是朝生夕死,所以Minor GC非常频繁,一般回收速度也比较快。当Eden空间不足以为对象分配内存时,会触发Minor GC。

       老年代GC(Full GC/Major GC):Full GC指发生在老年代的GC,出现了Full GC一般会伴随着至少一次的Minor GC(老年代的对象大部分是Minor GC过程中从新生代进入老年代),比如:分配担保失败。Full GC的速度一般会比Minor GC慢10倍以上。当老年代内存不足或者显式调用System.gc()方法时,会触发Full GC。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程芝士

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值