「JVM 内存管理」GC 可回收的对象

程序计数器虚拟机栈本地方法栈这 3 个区域随线程生灭,栈中栈帧的大小在类结构定型时便已确定,出入栈则随线程进出方法而进行;可见这几个区域的内存分配和回收都是确定的,无需过多考虑;

堆和方法区是明显不确定的(具备运行时的动态性),GC 所关注的便是这部分内存的管理;

如何找到可被 GC 回收的对象(不可能再被任何地方使用的对象)?

1. 引用计数算法(Reference Counting)

每个对象添加一个引用计数器,当有地方引用该对象时,对象的计数器加一;引用失效时,计数器减一;计数器为零,说明对象不可能被任何地方使用的;

原理简单、判断效率高;但有很多例外的情况(循环引用)需要考虑,这需要配合大量额外的处理才能保证正确性;

应用案例:Microsoft COM(Component Object Model)、Python、FlashPlayer、Squirrel 等的内存管理;

2. 可达性分析(Reachability Analysis)

通过一系列 GC Roots 根对象(起始节点集)根据引用关系进行搜索,形成的搜索链路即引用链(Reference Chain);当对象不在任何引用链时(GC Roots 到这个对象不可达时),则该对象是不可能再被使用的;

Java 固定可以作为 GC Roots 的对象如下:

  • VM Stack(栈帧中的本地变量表)中引用的对象,如方法的参数、局部变量、临时变量等;
  • Method Area 中类静态属性引用的对象,如 Java 类的静态变量引用的对象;
  • Method Area 中常量引用的对象,如字符串常量池(String Table)里的引用;
  • Native Method Stack 中 JNI 引用的对象;
  • JVM 内部引用的对象,如基本数据类型对应的 Class 对象、常驻的异常类型(NPE、OOM)、系统类加载器等;
  • 所有被同步锁(synchronized 关键字)持有的对象;
  • JMXBean、JVMTI 中注册的回调、本地代码缓存等;

此外可能根据 GC 类型的不同,回收内存区域的不同,还会有一些临时性对象(如分代回收中,关联区域的对象)加入 GC Roots;

应用案例:Java、C#、Lisp 的内存管理;

3. 引用(Reference)

引用(Reference),Reference 类型存储的数值代表另一对象所在内存的起始地址,即 Reference 数据表示该对象的引用;

缓存可被认为是被引用未被引用之间的存在;

JDK 1.2 之后,引用按强度可分为如下 4 种:

  • 强引用(Strongly Reference),传统意义上的引用,赋值引用;只要强引用关系还存在,GC 永远不会回收该被引用的对象;

  • 软引用(SoftReference),描述一些还有用,但非必须的对象;软引用关联的对象在系统将要发生内存溢出前,会被 GC 列入回收范围内进行第二次回收,若此次内存依旧不足,才会抛出 OOM;

  • 弱引用(WeakReference),描述一些非必须对象,比弱引用更弱;弱引用关联对象只能生存到下一次 GC(无论内存是否足够);

  • 虚引用(PhantomReference),为在对象被 GC 回收时收到一个系统通知而设计的引用关系;对关联对象的生存完全不构成影响;

4. finalize()

对象经过可达性分析并判定为不可达对象后,不会被直接 GC 回收,实际的回收要经历两次标记:当 GC Roots 不可达,对象会被标记第一次;随后会判定该对象是否要执行 finalize() 方法(根据对象的 finalize() 方法是否被复写,或是否已被 JVM 调用过,复写了且未被执行的,才会判定为需要执行),需要执行 finalize() 方法的对象会进入一个 F-Queue 队列,随后 Finalizer 线程会执行对象的 finalize() 方法,若复写的 finalize() 中将该对象重新与 GC Roots 建立关联(如将对象赋到 GC Roots 链上的对象上),则在随后的第二次标记中会将该对象从即将回收的集合移除,如此该对象就能存活下来;

任何对象的 finalize() 方法只会被系统自动调用一次;

finalize() 方法是 Java 刚诞生时迁就 C、C++ 程序员的一项妥协,官方明确声明不推荐使用;运行代价高,不确定性大,调用顺序无法保障;

关闭外部资源之类的清理工作也不应使用 finalize(),try-finally 或其他方式都可以更好、更及时的处理;

5. 方法区 GC

方法区 GC 的对象主要是废弃的常量(没有任何对象引用常量池中的常量,包括 JVM 层面)和不再使用的类型(类的实例已被回收、加载该类的类加载器已被回收、该类对应的 Class 对象没有被任何地方引用,无法在任何地方通过反射访问该类的方法);

满足卸载条件的类型也不一定会被回收,而是可以通过如下 VM 参数进一步控制:

# 控制 HotSpot 是否进行类卸载
-Xnoclassgc
# 查看类加载和卸载信息
-verbose:class           # Product 版 JVM
-XX:+TraceClassLoading   # Product 版 JVM
-XX:+TraceClassUnLoading # FastDebug 版 JVM

大量使用反射、动态代理、CGLib 等字节码框架,频繁动态生成自定义类加载器的场景,通常需要 JVM 具备类型卸载的能力,以确保释放方法区的内存压力;

方法区 GC 的性价比通常是比较低的,《Java 虚拟机规范》不要求 JVM 在方法区实现垃圾收集,也存在未实现或未完整实现方法区类卸载的收集器(JDK 11 的 ZGC 不支持类卸载);


上一篇:「JVM 内存管理」OutOfMemoryError 异常
下一篇:「JVM 内存管理」GC 算法理论与发展过程

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Aurelius-Shu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值