java中内存回收_Java的内存 - 内存回收

这篇承接上一篇 《Java的内存 - 内存模型》,分析内存回收相关的知识点。

垃圾回收包含两个步骤,①标记哪些内存是垃圾 ②回收内存。下面分别说这两个步骤有哪些算法:

1. 垃圾标记

1.1 引用计数算法

没有哪一种 JVM 是使用「引用计数」作为垃圾回收算法的,但这种算法又很经典,所以介绍一下。(Android 的 native 层用到了引用计数)

工作方式:

堆中每一个对象都有一个引用计数器。创建并初始化赋值后,引用计数置为1,每多一次引用,引用计数+1,每有一个引用失效(出作用域 或者 被设置为其他值)时,引用计数-1。引用计数 == 0 的对象可以被回收。

优点:

判定对象是否需要回收的效率高,不需要额外的线程做 GC 的工作,也不会暂停应用。

缺点:

无法检测循环依赖。由于需要实时计数,增加了程序执行时的开销。

应用实例:

Object-C 的 ARC 模式。

1.2 根搜索算法

根搜索算法是 Java 虚拟机主流的找垃圾算法。

首先要知道根集和finalize()相关的东西:

根集:

根集是肯定不需要回收的对象的引用。它包含:

Java栈 和 Native栈 的本地变量表中引用的对象;

方法区中的常量和静态变量引用的对象;

活跃的线程对象等。

关于 finalize() 方法:

finalize() 方法最多只会被 垃圾收集器 执行一次(不包括开发者主动调用)。已经被垃圾收集器执行过一次后,不会再执行第二次。如果一个类没有重写 finalize() 方法,垃圾回收器不会执行其对象的 finalize() 方法。

根搜索算法会有两次标记,第一次标记将需要执行 finalize() 的对象放入 F-Queue 中,等待执行 finalize() 方法;第二次再判断执行完 finalize() 的对象是否依然不可达,并最终确定哪些对象是垃圾。

finalize() 方法是在一个低优先级的线程中执行的。

工作步骤如下:

第一步:获取不可达对象

暂停整个应用(Stop The World);

生成根集(GC Roots);

从根集出发,找出根集中的对象引用的其他对象,并依次沿引用方向遍历,生成引用链;

根据引用链获取所有不可达的对象;

a967b746fbea

根搜索算法

第二步:垃圾的自我救赎

判断这些不可达对象是否需要执行 finalize() 方法;

如果不需要,直接标记为可回收对象,跳过后续步骤;如果需要,将对象加入到 F-Queue 中,等待执行 finalize() 方法;

如果在 finalize() 方法中,其他对象又持有了该对象,那么该对象又变为可达对象了。

第三步:再次获取不可达对象

F-Queue队列执行完后,再次判断执行完 finalize() 方法的这些对象是否可达;

如果仍然不可达,标记为可回收对象。

2.2 垃圾收集

关于垃圾回收,参考 Oracle 官网 《HotSpot 虚拟机内存管理白皮书》。

垃圾回收确保回收的对象必然是不可达对象,但是不确保所有的不可达对象都会被回收。

垃圾收集算法主要有3种: 标记清除算法(Mark-Sweep) 、标记压缩算法(Mark-Sweep-Compact)、复制算法(Copying)。在这3种的基础上派生出其它算法:

分代回收:考虑算法和内存分配的特点,对堆上不同的分代使用不同的回收算法;

并行回收(Parallel)和 串行回收(Serial): 考虑在内存回收时,是一个线程在回收,还是多个线程同时回收;

并发回收 和 Stop-The-World:考虑在内存回收时,是否一边执行应用一边回收,还是完全暂停整个应用;

以下是这些算法的具体信息:

2.2.1 标记清除算法

工作方式:

内部维护了一个空闲内存表,用来记录可分配内存的地址和大小;

工作时,先将标记为可释放的对象的内存释放,然后在空闲内存表中更新可分配内存信息。

a967b746fbea

标记清除算法

优点:

速度快。快的原因,相比后面的标记整理算法,是不需要移动内存。

缺点:

会产生大量的内存碎片;

维护一个空闲列表有一定的额外开销;

分配新内存时,需要遍历空闲列表找到合适的内存块。

2.2.2 标记整理算法

工作方式:

将所有活跃的对象,依次移动到内存的一端;

移动完毕后,清理边界之外的内存。

a967b746fbea

标记整理算法

优点:

不会产生内存碎片;

只需要记录内存末尾的指针,新内存分配时可以立即分配;

缺点:

由于需要移动内存,暂停应用的时间会延长。

2.2.3 复制算法

工作方式:

将堆内存分为两块相同的区域;

内存分配时,只在其中一块内存分配;

当内存不足以分配时,将所有活跃的对象依次复制到另一块区域;

一次性清理掉旧的内存区域。

a967b746fbea

复制算法

优点:

标记和复制可以同时进行;

效率高,清理内存时是对一整块内存进行操作;

不会产生内存碎片。

缺点:

可分配的堆内存减为一半了;

由于需要移动内存,暂停应用的时间会延长。

特点:

该算法的耗时,只跟活跃对象的数量有关,和这个算法管理的堆空间总大小无关。

2.2.3 分代回收

由于不同对象的生命周期是不一样的,因此可以对不同生命周期的对象采取不同的收集方式,以提高回收效率。

不分区有什么缺点:

如果不分区,GC是对整个堆区进行可达性分析、内存移动等,回收会很耗时。

如何划分内存区域:

由于大部分对象的生命周期很短,只有少部分对象会存活较长时间。所以基于它们的生命周期分代是个合适的选择。HotSpot 等虚拟机都把堆区分为 年轻代 和 老年代。

分代是如何减少可达性分析的:

对年轻代做可达性分析时,如果还要遍历老年代,那就没有减少可达性分析的时间。

但是。如果不遍历其它分代,如何知道一个年轻代的对象是否被老年代持有呢?这就产生了跨代引用的问题。

为了解决问题,引入了「跨代引用是 GC Root」的解决办法:如果老年代的 Old 对象,引用了年轻代的 Young 对象,在对年轻代进行可达性分析时,Old 对象算作 GC Root。这样就不用遍历老年代了。

分代回收算法需要有一个表,用来记录所有的跨代引用,很耗内存。HotSpot 使用 CardTable 记录老年代对年轻代的引用。把老年代按照 4KB 的大小分块,每一块对应在 CardTable 中都是1 bit。当值为1时,表示这4KB 的内存中有对年轻代的引用,需要加入到 GC Roots 中。

a967b746fbea

分代回收Card Table

这种解决办法也会有问题:如果老年代的B对象引用了年轻代的A对象,B没有被其它对象引用,实际上A、B都应该被回收,但却把B当作GC Root了。也就是部分不可达对象没有被回收。

如何选择合适的算法:

对于年轻代的对象,它们数量多、生命周期短,且大部分对象都是要回收的,所以需要速度更快的垃圾回收算法。Copying 算法的耗时,只跟堆内活跃对象的数量有关,跟堆的大小无关,所以特别适合用于年轻代的回收。

对于老年代的对象,他们占用内存大,不能使用复制算法,并且需要避免内存碎片,所以使用 标记整理算法。

在回收年轻代时,可达性分析只分析年轻代。在回收老年代时,是对整个堆区做可达性分析。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值