JVM高级特性与实践(二):对象存活判定算法(引用) 与 回收

垃圾回收器GC(Garbage Collection)

于1960年诞生在MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。

GC需要完成的3件事情:

哪些内存需要回收?
什么时候回收?
如何回收?

内存的动态分配与内存回收机制似乎“自动化”起来。

了解GC和内存分配等底层知识目的:

因为在排查各种内存溢出、内存泄漏问题、垃圾收集成为系统达到更高并发量的瓶颈时,需要对这些“自动化”技术实施必要的监控和调节。

一,Java内存运行时区域的各个部分:

程序计数器、虚拟机栈、本地方法栈 3个区域随着线程而生,也随线程而灭;
栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。

每一个栈帧中分配的内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,

在这几个区域内不需要过多考虑回收问题,因为方法结束或线程结束时,内存自然跟随着回收了。

而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,
多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,

这部分内存的分配和回收都是动态的,垃圾回收器关注的是这部分内存

二,堆存活对象的判断

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

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。

效率分析

例如微软公司的COM(Component Object Model)技术、使用ActionScript3的FlashPlayer、Python语言都是用了该算法进行内存管理。

但是Java虚拟机中并没有引用该算法来管理内存,最主要的原因是它很难解决对象之间互相循环引用的问题。

举例证明

  • 对象objA 和对象objB都有字段instance,赋值令objA.instance = objB;objB.instance = objA;
  • 互相引用着对方,导致它们引用计数不为0,所以引用计数器无法通知GC收集器回收它们。
/**
* testGC()方法执行后,objA和objB会不会被GC呢?
*/
public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /**
* 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过
*/
private byte[] bigSize = new byte[2 * _1MB]; public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA; objA = null;
objB = null; // 假设在这行发生GC,objA和objB是否能被回收?
System.gc();
}
}
[Full GC (System)  [Tenured: 0k->210K(10240K),  0.0149142 secs] 4603K->210K(19456K),
[Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

GC日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就不回收它们,从侧面说明虚拟机并不是通过引用计数算法来判断对象是否存活的。

2 . 可达性分析算法(Reachability Analysis)

(Java、C#、甚至是最古老的Lisp)的实现中,都是通过可达性分析来判定对象是否存活的。

此算法的基本思路是:

通过一系列的称为“GC Roots”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),
当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是GC Roots 到这个对象不可达)时,则证明此对象时不可用的。

对象object5、object6、object7虽然互相有关联,但是它们到GCRoots是不可达的,所以它们将会被判定为可回收对象。

在Java中,可作为GCRoots的对象包括以下几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象

三,finaliza()

使在可达性分析算法中不可达的对象,也并非是要宣判“死亡”的,它们暂时都处于“缓刑”阶段,要真正宣告一个对象“死亡”,首先要经历两次标记过程:

第一次标记:对象在进行可达性分析后发现没有与 GC Roots相连接的引用链,是否要执行对象的finaliza() 方法。
第二次标记:当对象没有覆盖finaliza() 方法,或者finaliza() 方法已经被虚拟机调用过,视为“没有必要执行”。

对象复活:

如果这个对象被判定为有必要执行finaliza() 方法,那么此对象将会放置在一个叫做 F-Queue 的队列中,
并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。 这里所谓的“执行”是指虚拟机会触发此方法,但并不承诺会等待它运行结束,

原因是:

如果一个对象在finaliza() 方法中执行缓慢,或者发生了死循环(更极端的情况),
将很可能导致F-Queue 队列中的其它对象永久处于等待,甚至导致整个内存回收系统崩溃。

finaliza() 方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue 队列中的对象进行第二次小规模的标记。

  • 如果对象想在finaliza() 方法中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可,
  • 例如把自己(this关键字)赋值给某个类变量或者对象的成员变量,这样在第二次标记时它将被移出“即将回收”的集合;

如果对象这时候还没有逃脱,基本上它就被回收了。

通过以下代码展示一个对象的finaliza()被执行,但是它仍然可以存活的例子:

【一次对象自我拯救的演示】
/**
* 此代码演示了两点:
* 1.对象可以在被GC时自我拯救。
* 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
* @author zzm
*/
public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() {
System.out.println("yes, i am still alive :)");
} @Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
} public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC(); //对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
} // 下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
finalize method executed!
yes, i am still alive :)
no, i am dead :(

由以上结果可知,SAVE_HOOK 对象的finalize() 方法确实被GC收集器触发过,并且在收集前成功逃脱了。

另一个值得注意的地方,代码中有两段一模一样的代码段,执行结果却是一次逃脱成功,一次失败。

这是因为任何一个对象的finalize() 方法都只会被系统调用一次,如果对象面临下一次回收,它的finalize() 方法不会再被执行,因此第二次逃脱行动失败。

有关finaliza()方法的建议:

  • 需要特别说的是,finalize() 方法,不建议开发人员使用这种方法拯救对象。
  • 应当尽量避免使用它,因为它不是C/C++中的析构函数,而是Java刚诞生时为了使C/C++程序员更容易接受它所做的一个妥协。
  • 它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。
  • 有些教材中描述它适合做“关闭外部资源”之类的工作,这完全是对此方法用途的一种自我安慰。
  • finalize() 能做的工作,使用try-finally 或者其它方法都更适合、及时,所以作者建议大家可以忘掉此方法存在。

四 . 回收方法区

在方法区中进行垃圾收集的“性价比”较低。
在堆中,尤其是新生代,常规应用进行一次垃圾收集可以回收70%~95%的空间,而方法区的效率远低于此。

方法区的垃圾收集主要回收两部分:

废弃常量和无用类。

“废弃常量”的回收

回收废弃常量与回收Java堆中的对象非常类似。

以常量池中字面量的回收为例,

例如一个字符串“abc”已经进入常量池,但是无任何String对象引用常量池的此常量,也无其它引用此字面量,“abc”常量会被系统清理出常量池。

常量池中的其他类(接口)、方法、字段的符号引用也是如此。

“无用类”回收的条件

判定一个类是否是“无用类”的3个条件:

  • 该类的所有实例已被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已被回收。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类的方法。

这里仅说“可以”,并非如同“对象”不使用了就必然回收。

方法区主要应用于:

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,以保证方法区不会溢出。

jvm高级特性(2)(判断存活对象算法,finaliza(),方法区回收)的更多相关文章

  1. jvm高级特性(4)(内存分配回收策略)

    JVM高级特性与实践(四):内存分配 与 回收策略 一. 内存分配 和 回收策略 1,对象内存分配的概念: 往大方向讲,它就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配), ...

  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践》【PDF】下载

    <深入理解Java虚拟机:JVM高级特性与最佳实践>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230062566 内容简介 作为一位 ...

  3. jvm高级特性(6)(线程的种类,调度,状态,安全程度,实现安全的方法,同步种类,锁优化,锁种类)

    JVM高级特性与实践(十三):线程实现 与 Java线程调度 JVM高级特性与实践(十四):线程安全 与 锁优化 一. 线程的实现 线程其实是比进程更轻量级的调度执行单位. 线程的引入,可以把一个检查 ...

  4. jvm高级特性(5)(1)(原子性,可见性,有序性,volatile,概述)

    JVM高级特性与实践(十二):高效并发时的内外存交互.三大特征(原子.可见.有序性) 与 volatile型变量特殊规则 简介: 阿姆达尔定律(Amdahl):该定律通过系统中并行化与串行化的比重来描 ...

  5. 读书笔记-《深入理解Java虚拟机:JVM高级特性与最佳实践》

    目录 概述 第一章: 走进Java 第二章: Java内存区域与内存溢出异常 第三章: 垃圾收集器与内存分配策略 第四章: 虚拟机性能监控与故障处理 第五章: 调优案例分析与实战 第六章: 类文件结构 ...

  6. 转:JVM 内存初学 (堆(heap)、栈(stack)和方法区(method) )

    原文地址:JVM 内存初学 (堆(heap).栈(stack)和方法区(method) ) 博主推荐 深入浅出JVM 这本书 先了解具体的概念:JAVA的JVM的内存可分为3个区:堆(heap).栈( ...

  7. 《深入理解Java虚拟机:JVM高级特性与最佳实践》读书笔记

    第一部分 走进Java 一.走进Java 1.概述 java广泛应用于嵌入式系统.移动终端.企业服务器.大型机等各种场合,摆脱了硬件平台的束缚,实现了“一次编写,到处运行”的理想 2.java技术体系 ...

  8. JVM高级特性与实践(一):Java内存区域 与 内存溢出异常

    套用<围城>中的一句话,“墙外面的人想进去,墙里面的人想出来”,用此来形容Java与C++之间这堵内存动态分配和垃圾收集技术所围成的“围墙”就再合适不过了. 对于从事C.C++的开发人员而 ...

  9. jvm高级特性(1)(内存泄漏实例)

    jvm内存结构回顾: .8同1.7比,最大的差别就是:元数据区取代了永久代.元空间的本质和永久代类似,都是对JVM规范中方法区的实现. 不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中, ...

随机推荐

  1. win10如何安装和创建 证书

    .下载winsdksetup.exe .在 MMC 管理单元中查看证书 打开一个命令提示符窗口. 类型mmc然后按 ENTER 键. 请注意,若要查看本地计算机存储中的证书,您必须具有管理员角色. 上 ...

  2. oracle 新建数据库 ,新建用户

    net manager   数据库名----电脑名localhost 1521  , 服务名  orcl (oracle 版本不一样, 不同版本不一样,,)  然后测试.. sys 账号登录  新建用 ...

  3. 557. Reverse Words in a String III

    static int wing=[]() { std::ios::sync_with_stdio(false); cin.tie(NULL); ; }(); class Solution { publ ...

  4. 2018.09.19 atcoder Card Game for Three(组合数学)

    传送门 简单组合数学想优化想了半天啊233. 我们只需考虑翻开n张A,b张B,c张C且最后一张为A的选法数. 显然还剩下m+k−b−cm+k-b-cm+k−b−c张牌没有选. 这样的话无论前n+b+c ...

  5. 2018.07.23 hdu5828 Rikka with Sequence(线段树)

    传送门 这道题维护区间加,区间开根,区间求和. 线段树常规操作. 首先回忆两道简单得多的线段树. 第一个:区间覆盖,区间加,区间求和. 第二个:区间开根,区间求和. 这两个是名副其实的常规操作. 但这 ...

  6. vs2010 EF4.0 访问mysql

    需要安装mysql-connector-net-6.3.5 6.8.9的安装完后在dbfirst里找不到对应的提供程序 链接字符串里需要 指定下编码(如果不是gbk的话) <add name=& ...

  7. time & datetime 模块

    在平常的代码中,我们常常需要与时间打交道.在Python中,与时间处理有关的模块就包括:time,datetime,calendar(很少用,不讲),下面分别来介绍. 在开始之前,首先要说明几点: 一 ...

  8. returning into 语句

    returning into 语句用于执行完语句后返回的值,具体返回执行之前或者之后的结果,多用于在存储过程中 如下所述:delete语句的returning into语句返回的是delete之前的结 ...

  9. Tomcat 环境变量配置

    1.变量和常量 i 和 0 2.环境变量 cmd >set 查看所有环境变量 %PATH% 系统指定可执行文件的搜索路径,可以是 .exe .bat String path=“C:\WINDOW ...

  10. 第二届普适计算和信号处理及应用国际会议论文2016年 The 2nd Conference on Pervasive Computing, Signal Processing and Applications(PCSPA, 2016)

    A New Method for Mutual Coupling Correction of Array Output Signal 一种阵列输出信号互耦校正的新方法 Research of Robu ...