java 内存回收不了时_Java的内存 - 内存回收

这篇承接上一篇 《Java的内存 - 内存模型》,分析内存回收相关的知识点。 垃圾回收包含两个步骤,①标记哪些内存是垃圾 ②回收内存。下面分别说这两个步骤有哪些算法:

1. 垃圾标记

1.1 引用计数算法

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

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

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

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

应用实例: Object-C 的 ARC 模式。

1.2 根搜索算法

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

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

根集: 根集是肯定不需要回收的对象的引用。它包含: 1. Java栈 和 Native栈 的本地变量表中引用的对象; 2. 方法区中的常量和静态变量引用的对象; 3. 活跃的线程对象等。

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

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

工作步骤如下:

第一步:获取不可达对象 1. 暂停整个应用(Stop The World); 2. 生成根集(GC Roots); 3. 从根集出发,找出根集中的对象引用的其他对象,并依次沿引用方向遍历,生成引用链; 4. 根据引用链获取所有不可达的对象;

第二步:垃圾的自我救赎 1. 判断这些不可达对象是否需要执行 finalize() 方法; 2. 如果不需要,直接标记为可回收对象,跳过后续步骤;如果需要,将对象加入到 F-Queue 中,等待执行 finalize() 方法; 3. 如果在 finalize() 方法中,其他对象又持有了该对象,那么该对象又变为可达对象了。

第三步:再次获取不可达对象 1. F-Queue队列执行完后,再次判断执行完 finalize() 方法的这些对象是否可达; 2. 如果仍然不可达,标记为可回收对象。

2.2 垃圾收集

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

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

垃圾收集算法主要有3种: 标记清除算法(Mark-Sweep) 、标记压缩算法(Mark-Sweep-Compact)、复制算法(Copying)。在这3种的基础上派生出其它算法: 1. 分代回收:考虑算法和内存分配的特点,对堆上不同的分代使用不同的回收算法; 2. 并行回收(Parallel)和 串行回收(Serial): 考虑在内存回收时,是一个线程在回收,还是多个线程同时回收; 3. 并发回收 和 Stop-The-World:考虑在内存回收时,是否一边执行应用一边回收,还是完全暂停整个应用;

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

2.2.1 标记清除算法

工作方式: 1. 内部维护了一个空闲内存表,用来记录可分配内存的地址和大小; 2. 工作时,先将标记为可释放的对象的内存释放,然后在空闲内存表中更新可分配内存信息。

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

缺点: 1. 会产生大量的内存碎片; 2. 维护一个空闲列表有一定的额外开销; 3. 分配新内存时,需要遍历空闲列表找到合适的内存块。

2.2.2 标记整理算法

工作方式: 1. 将所有活跃的对象,依次移动到内存的一端; 2. 移动完毕后,清理边界之外的内存。

优点: 1. 不会产生内存碎片; 2. 只需要记录内存末尾的指针,新内存分配时可以立即分配;

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

2.2.3 复制算法

工作方式: 1. 将堆内存分为两块相同的区域; 2. 内存分配时,只在其中一块内存分配; 3. 当内存不足以分配时,将所有活跃的对象依次复制到另一块区域; 4. 一次性清理掉旧的内存区域。

优点: 1. 标记和复制可以同时进行; 2. 效率高,清理内存时是对一整块内存进行操作; 3. 不会产生内存碎片。

缺点: 1. 可分配的堆内存减为一半了; 1. 由于需要移动内存,暂停应用的时间会延长。

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

2.2.3 分代回收

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

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

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

分代是如何减少可达性分析的: 对年轻代做可达性分析时,如果还要遍历老年代,那就没有减少可达性分析的时间。 但是。如果不遍历其它分代,如何知道一个年轻代的对象是否被老年代持有呢?这就产生了跨代引用的问题。

为了解决问题,引入了「跨代引用是 GC Root」的解决办法:如果老年代的 Young 对象,引用了年轻代的 Old 对象,在对年轻代进行可达性分析时,Young 对象算作 GC Root。这样就不用遍历其它分代了。 分代回收算法需要有一个表,用来记录所有的跨代引用,很耗内存。HotSpot 使用 CardTable 记录老年代对年轻代的引用。把老年代按照 4KB 的大小分块,每一块对应在 CardTable 中都是1 bit。当值为1时,表示这4KB 的内存中有对年轻代的引用,需要加入到 GC Roots 中。

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

如何选择合适的算法: 1. 对于年轻代的对象,它们数量多、生命周期短,且大部分对象都是要回收的,所以需要速度更快的垃圾回收算法。Copying 算法的耗时,只跟堆内活跃对象的数量有关,跟堆的大小无关,所以特别适合用于年轻代的回收。 2. 对于老年代的对象,他们占用内存大,不能使用复制算法,并且需要避免内存碎片,所以使用 标记整理算法。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java内存回收是由Java虚拟机(JVM)自动管理的,一般情况下无需手动控制。JVM使用垃圾回收器(Garbage Collector)来识别和回收不再使用的对象,释放内存空间。 虽然无法直接控制内存回收的具体机,但可以通过一些技术手段来优化内存使用和垃圾回收的性能,包括: 1. 避免创建过多的临对象:频繁地创建临对象会增加垃圾回收的负担。可以尽量重用对象或使用可变对象,避免不必要的对象创建。 2. 及释放资源:对于使用了外部资源(如文件、数据库连接等)的对象,在使用完后应及手动释放资源,确保及回收。 3. 注意对象的生命周期:确保对象在不再使用被及置为null,这样垃圾回收器可以识别并回收这些无用的对象。 4. 使用合适的数据结构和算法:选择适合应用场景的数据结构和算法,可以减少内存占用和垃圾回收的压力。 5. 调整JVM参数:可以通过调整JVM的参数来优化垃圾回收器的行为,例如堆大小、新生代和老年代的比例、并行或并发的垃圾回收等。不同的应用场景可能需要不同的参数配置。 需要注意的是,手动控制内存回收可能会导致不稳定性和性能问题,一般情况下,建议依赖JVM的自动内存管理机制。只有在特殊情况下,例如需要管理大量的本地资源或使用了大对象,才需要考虑手动进行内存管理。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值