JVM垃圾收集器与内存分配策略

目录

 

概述

判断对象是否存活

引用计数法

可达性算法

引用的各种类型

强引用(Strong Reference)

软引用(Soft Reference)

弱引用(Weak Reference)

虚引用(Phantom Reference)

生存还是死亡?

方法区的内存回收

判定废弃常量

判定无用的类

垃圾收集算法

标记-清除算法

复制算法(新生代)

标记-整理算法(老年代)

分代收集算法


概述

程序计数器、虚拟机栈、本地方法这三个区域随线程而生,也随线程而灭;栈帧随着方法的开始而入栈,随着方法的结束而出栈。这几个区域的内存分配和回收都具有确定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

而对于 Java 堆和方法区,一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有在程序运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的正是这部分内存。

判断对象是否存活

如果一个对象不被任何对象或者变量引用,那么我们认为他是无效对象,需要被资源回收。

引用计数法

在对象中添加一个引用计数器,对象被引用时+1;若引用时效则-1,当计数器的值为0时,就认为该对象无效。

引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。但是主流的 Java 虚拟机里没有选用引用计数算法来管理内存,主要是因为它很难解决对象之间循环引用的问题。代码如下(本段内容参考:https://www.zhihu.com/question/21539353): 

public class GcDemo {

        public static void main(String[] args) {
            //分为6个步骤
            GcObject obj1 = new GcObject(); //Step 1
            GcObject obj2 = new GcObject(); //Step 2

            obj1.instance = obj2; //Step 3
            obj2.instance = obj1; //Step 4

            obj1 = null; //Step 5
            obj2 = null; //Step 6
        }
    }

    class GcObject{
        public Object instance = null;
    }

很多文章中并没有说明上述循环引用为何引用计数法不为0。下面结合模型具体分析。

可达性算法

所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。

在Java中能作为GC Roots对象的包括以下几种:

  • Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象
  • Java虚拟机内部引用,及基本数据类型对应Class对象,一些常驻的异常对象(比如NUllPointException、OOM)等,还有系统加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调,本地方法缓存等。

引用的各种类型

判定对象是否存活与“引用”有关。在 JDK 1.2 以前,Java 中的引用定义很传统,一个对象只有被引用或者没有被引用两种状态,我们希望能描述这一类对象:当内存空间还足够时,则保留在内存中;如果内存空间在进行垃圾手收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为了以下四种。不同的引用类型,主要体现的是对象不同的可达性状态reachable和垃圾收集的影响。

强引用(Strong Reference)

类似 "Object obj = new Object()" 这类的引用,就是强引用,只要强引用存在,垃圾收集器永远不会回收被引用的对象。但是,如果我们错误地保持了强引用,比如:赋值给了 static 变量,那么对象在很长一段时间内不会被回收,会产生内存泄漏。

软引用(Soft Reference)

软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

弱引用(Weak Reference)

弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

虚引用(Phantom Reference)

虚引用也称幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响。它仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制。

生存还是死亡?

即使是可达性分析算中判定为不可达的对象,也不是“非死不可”,此时处于“缓刑”阶段,至少要标记两次过程才能真正的“死亡”。

  • 对象在可达性分析后没有与GC Roots相连接的引用链,将会进行第一次标记
  • 将上述对象进行一次筛选,判断是否有比较执行finalize(),如果对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,这两种情况都被认为是“没有必要执行”(==直接回收)
  • 如果上述对象有必要执行finalize(),则将对象放置在F-Q中,由低优先级的Finalizer的线程去执行finalize()方法,finalize()是最后一次逃生机会,只要重新与引用链上的任何一个对象建立关系即可,那么在第二次标机是就会被移除集合。

注:任何一个对象的finnalize()方法都指挥被系统自动调用一次,如果面临下一次回收,他的finalize()方法不会被再次执行。

方法区的内存回收

主要分为两点:

  • 废弃的常量
  • 不再使用的类型

判定废弃常量

只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。比如,一个字符串 "bingo" 进入了常量池,但是当前系统没有任何一个 String 对象引用常量池中的 "bingo" 常量,也没有其它地方引用这个字面量,必要的话,"bingo"常量会被清理出常量池。

判定无用的类

判定一个类是否是“无用的类”,条件较为苛刻。

  • 该类的所有对象都已经被清除
  • 加载该类的 ClassLoader 已经被回收
  • 该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区时创建,在方法区该类被删除时清除。

垃圾收集算法

大多数垃圾收集器都遵循了“分代收集”的理论设计

1.弱分代假说:绝大多数对象都是朝生夕灭

2.强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

标记-清除算法

标记的过程是:遍历所有的 GC Roots,然后将所有 GC Roots 可达的对象标记为存活的对象

清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。与此同时,清除那些被标记过的对象的标记,以便下次的垃圾回收。

这种方法有两个不足

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

复制算法(新生代)

为了解决效率问题,“复制”收集算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完,需要进行垃圾收集时,就将存活者的对象复制到另一块上面,然后将第一块内存全部清除。这种算法有优有劣:

  • 优点:不会有内存碎片的问题。
  • 缺点:内存缩小为原来的一半,浪费空间。

为了解决空间利用率问题,可以将内存分为三块: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一块 Survivor。回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才使用的 Survivor 空间。这样只有 10% 的内存被浪费。

但是我们无法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够,需要依赖其他内存(指老年代)进行分配担保。

分配担保

为对象分配内存空间时,如果 Eden+Survivor 中空闲区域无法装下该对象,会触发 MinorGC 进行垃圾收集。但如果 Minor GC 过后依然有超过 10% 的对象存活,这样存活的对象直接通过分配担保机制进入老年代,然后再将新对象存入 Eden 区。

标记-整理算法(老年代)

标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历 GC Roots,然后将存活的对象标记。

整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。

这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,因此每次垃圾回收会有大量对象存活,如果采用复制算法,每次需要复制大量存活的对象,效率很低。

分代收集算法

根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,针对各个年代的特点采用最适当的收集算法。

  • 新生代:复制算法
  • 老年代:标记-清除算法、标记-整理算法
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值