近期学习了JVM垃圾收集器与内存分配策略,现将部分学习笔记记录如下,以供今后使用。
前言
为了能使Java应用程序正常运行,JVM将内存数据分为程序计数器、虚拟机栈、本地方法栈、Java堆和方法区5个区域。其中程序计数器、虚拟机栈、本地方法栈3个区域是线程私有的,其生命随线程而生,随线程而亡;栈中的栈帧随着方法的进入和退出而执行着入栈和出栈操作,每个栈帧中分配的内存基本上在类结构确定时就已知了,因此这几个区域的内存分配与回收都是确定的,在这几个区域就不需要过多的考虑内存回收问题。而Java堆与方法区则完全不同,一个接口的多个实现类需要的内存可能不同,我们只有在程序运行期间才能知道创建哪些对象,需要分配多少内存,所以这部分内存的分配与回收是动态的,垃圾收集器所关注的就是这部分内存。
既然垃圾收集器关注的是Java堆与方法区的内存回收问题,那么最重要的一个问题就是:哪些内存需要回收?
下面介绍解决这个问题的常用算法。
引用计数算法(Reference Counting)
引用计数算法原理:给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不能被使用的对象。
引用计数算法实现简单,判断效率很高;但JVM没有使用该算法管理内存,主要原因是它很难解决对象间的循环引用问题。
public class ReferenceCountingGC {
public Object instance;
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC(); // objACount = 1
ReferenceCountingGC objB = new ReferenceCountingGC(); // objBCount = 1
objA.instance = objB; // objBCount = 2
objB.instance = objA; // objACount = 2
objA = null; // objACount = 1
objB = null; // objBCount = 1
// 此时进行垃圾回收,objA与objB的引用计数器都不为0,这两个对象不会被回收
System.gc();
}
}
可达性分析算法
可达性分析算法的原理:通过一系列的称为“GC Roots”的对象作为起点,从这个借口开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则此对象不可用。
Java语言中,可作为 GC Roots 的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象;
- 方法区中类的静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(Native方法)引用的对象。
生存还是死亡
即使在可达性分析算法中不可达的对象,也并非是非死不可的,一个对象在真正死亡之前,至少要经历两次标记过程。
如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那么它将被第一次标记并进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已被虚拟机调用过,则虚拟机认为没有必要执行finalize()方法。
如果该对象被判定为有必要执行finalize()方法,那么该对象将会被放置在F-Queue队列中,并在稍后由低优先级的Finalizer线程(一个由虚拟机自动建立的线程)去执行它。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次标记,如果对象在finalize()方法中成功拯救自己(只需要重新与引用链上的任何对象建立关联即可,如把自己this复制给某个类变量或对象的成员变量),那么在第二次标记时它将被移除“即将回收”的集合;否则,这个对象就会被回收了。
任何一个对象的finalize()方法都只会被系统自动调用一次,因此对象也只有在第一次被调用finalize()方法时拯救自己一次,后续再被回收,它的finalize()方法不会被再次执行,也就无法自救了。
public class FinalizeEscapeGC {
public static FinalizeEscapeGC instance;
public void isAlive() {
System.out.println("Yes, i'm still alive!");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize() executed!");
FinalizeEscapeGC.instance = this;
}
public static void main(String[] args) throws InterruptedException {
FinalizeEscapeGC.instance = new FinalizeEscapeGC();
// 第一次回收时对象拯救自己
FinalizeEscapeGC.instance = null;
System.gc();
// 等待1s,让gc执行
Thread.sleep(1000);
if (FinalizeEscapeGC.instance != null) {
FinalizeEscapeGC.instance.isAlive();
} else {
System.out.println("Oh, i'm dead!");
}
// 第二次拯救自己是否能成功呢?看运行结果
FinalizeEscapeGC.instance = null;
System.gc();
Thread.sleep(1000);
if (FinalizeEscapeGC.instance != null) {
FinalizeEscapeGC.instance.isAlive();
} else {
System.out.println("Oh, i'm dead!");
}
}
}
运行结果:
finalize() executed!
Yes, i'm still alive!
Oh, i'm dead!
从运行结果看,对象第一次被回收时拯救自己成功了,但第二次被回收时拯救自己失败了。
回收方法区
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
如果没有任何对象引用常量池中的常量,对于针对字符串常量也需要没有其他地方引用这个字面量,那么发生内存回收时,这些常量有可能会被系统清理出常量池。
类需要同时满足以下3个条件才能算是无用的类:
- 该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例;
- 加载该类的类加载器已经被回收;
- 该类对象的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
JVM提供以下参数,可以查看类加载与卸载信息:
-XX:+TraceClassLoading 查看类加载信息。
-XX:+TraceClassUnLoading 查看类卸载信息。