在Java的内存运行时区域的各个部分中:程序计数器、虚拟机栈、本地方法栈3个区域属于线程私有,随线程而生、随线程而灭,因此不需要过多的考虑内存的回收;所以垃圾回收的主要区域就主要集中在Java堆和方法区
方法区由于判定内存回收的苛刻条件以及较低的回收效率,其垃圾回收的实现在Java虚拟机规范中并未做强制要求,由不同的虚拟机实现厂商自主选择(此内存区域的垃圾回收同样至关重要)
最终,垃圾收集器的重点工作区域落在了Java堆
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一,当引用失效时,计数器值就减一,任何时刻计数器为零的对象就是不可能再被使用的
引用计数算法的缺陷就在于很难解决对象之间相互循环引用的问题
/**
* 引用计数算法难以解决对象之间循环引用的问题
* {@link 《深入理解Java虚拟机》第三版 代码清单3-1}
* VM Args:-XX:+PrintGC 或者 -XX:+PrintGCDetails
* 代码在JDK1.8下运行
*/
public class ReferenceCountingGC {
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
/**
* 引用计数算法难以解决对象之间循环引用的问题
* {@link 《深入理解Java虚拟机》第三版 代码清单3-1}
* VM Args:-XX:+PrintGC 或者 -XX:+PrintGCDetails
* 代码在JDK1.8下运行
*/
public class ReferenceCountingGC {
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC objA = null;
ReferenceCountingGC objB = null;
System.gc();
}
}
将示例一和示例二的GC情况对比来看,虚拟机并没有因为两个对象互相引用就放弃回收,这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象是否存活的
通过一系列称为GC Roots的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索走过的路径称为引用链(Reference Chain),当某个对象到GC Roots没有任何引用链可达时,即判定对象不可能再被使用
在Java技术体系里,固定可作为GC Roots的对象包括以下几种:
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完成的GC Roots集合
根据对象在实际应用中情况的不同,在JDK1.2版之后,Java对应用的概念进行了扩充,将引用分为以下几种:
类似“Object obj = new Object()”这类传统意义上引用赋值的引用,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
描述一些还有用但非必需的对象。被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常
描述非必需的引用。被弱引用关联着的对象,只能生存到下一次垃圾收集发生位置;当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联着的对象
最弱的引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例;为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知
要真正宣告一个对象死亡,至少要经历两次标记过程:
/**
* finalize()的起死回生
* {@link 《深入理解Java虚拟机》第三版}
* VM Args:-XX:+PrintGCDetails
* 代码在JDK1.8下运行
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, I'm still alive !");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed !");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
SAVE_HOOK = null;
System.gc();
// Finalizer线程优先级低,所以暂停当前线程等待一段时间,以让finalize()方法执行
Thread.sleep(5);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
}else {
System.out.println("no, I'm dead !");
}
// 任何一个对象的finalize()方法都只会被系统自动调用一次,因此第二次面临回收时,finalize()方法不会再执行
SAVE_HOOK = null;
System.gc();
Thread.sleep(5);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
}else {
System.out.println("no, I'm dead !");
}
}
}
由于方法区的可回收判定条件苛刻、回收效率较低,所以Java虚拟机规范并未强制要求方法区必须实现垃圾收集,但这并非意味着在方法区就完全不需要垃圾回收,在Sun公司的Bug列表中就曾有多个因为低版本HotSpot虚拟机对此区域未完全回收导致的内存泄露
方法区的内存回收主要针对两部分内容:废弃的常量、不再使用的类型
常量池中的常量,不再被任何字符串对象引用,即可判定常量已被废弃,可以被清理出常量池并回收内存
不再被使用的类型的判定,需要同时满足三个条件:
同时满足上述三个条件的类型将会被判定为已废弃,但是否被回收,则还取决于具体虚拟机的实现
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。