对象的内存布局
在虚拟机(HotSpot)中,对象存储布局分为三个区域
对象头
对象头用于存储自身的运行时数据,如哈希码、GC分代年龄等,另一部分是类型指针,也就是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果说对象是一个数组,那么对象头里面还储存数据长度的数据。
实例数据
实例数据部分存储着对象程序代码中定义的各种类型字段内容,包括从父类继承的和子类总定义的。这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源代码中定义顺序的影响。虚拟机默认将相同长度的字段分配到一起,且父类定义的变量会出现在子类之前。通过配置虚拟机参数也可以使子类较窄的变量插到父类变量空隙中。
对齐填充
这个不是必然存在的,也没有什么含义,就是起到站位符的作用。因为对象头的大小规定是8个字节的整数倍,当对象头和实例数据没有对齐时,就需要通过对齐填充来补全。
对象的访问定位
当对象创建好后,我们需要通过栈上的reference数据操作堆上的具体对象。目前主流的访问方式有两种句柄和直接指针。
句柄
如果使用句柄的话,那么java堆中会划分出一块内存作为句柄池,reference中储存的对象就是句柄池的地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。使用句柄来访问最大的好处就是稳定,在对象被移动(垃圾回收时移动对象是普遍行为)时会改变句柄中的实例数据指针,而 reference 本身不需要修改。
直接指针
如果使用直接指针访问,reference 中存储的直接就是对象地址。
使用直接指针的最大好处就是速度快,它节省了一次指针定位的开销。
判断对象的存活
问:在堆中存放着几乎所有的对象实例,当垃圾回收器进行回收时,首先要做的事情就是判断那些对象还活着,那些已经死去。
答:没有任何引用指向的一个或者多个对象。
引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1,当引用失效时,计数器减 1。
但是引用计数法会存在上图问题,但两个对象互相引用的时候,是没有办法来处理,这个时候需要引入额外的机制来处理,从而比较影响
效率,因此主流虚拟机没有使用。
可达性分析
来判断对象是否存活,可达性分析算法思路就是通过一系列的称为 GC Roots的对象作为起点,往下节点搜索,搜索走过的路程称为引用链,当一个对象到 GC Roots没有任何引用链,则证明此对象不可用。
作为GC Roots 的对象包括下面几种(主要是前四种)
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
- 方法区中类静态属性引用的对象;java 类的引用类型静态变量。
- 方法区中常量引用的对象;比如:字符串常量池里的引用。
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
- JVM 的内部引用(class 对象、异常对象 NullPointException、OutofMemoryError,系统类加载器)。
- 所有被同步锁(synchronized 关键)持有的对象。
- JVM 内部的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
- JVM 实现中的“临时性”对象,跨代引用的对象。
Class回收条件
以上的回收都是对象,那么class要被回收,条件比较苛刻(有参数可以进行控制),必须满足一下条件:
1、该类所有实例都被回收,也就是堆中没有该类的任何实例。
2、加载该类的ClassLoader 已经被回收。
3、该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
4、参数控制
废弃的常量和静态变量的回收其实就和 Class 回收的条件差不多。
Finalize 方法
即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是 没有找到与 GCRoots 的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了 finalize),我们可以在 finalize 中去拯救。
上代码:
public class TestGC {
public static TestGC instance = null;
public void isAlive() {
System.out.println("我还活着");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize 拯救");
TestGC.instance = this;
}
public static void main(String[] args) throws Throwable {
instance = new TestGC();
//进行第一次GC
instance = null;
System.gc();
Thread.sleep(1000);//finalize方法优先级很低,需要等待
if (instance != null) {
instance.isAlive();
} else {
System.out.println("我死了");
}
//进行第二次GC
instance = null;
System.gc();
Thread.sleep(1000);
if (instance != null) {
instance.isAlive();
} else {
System.out.println("我死了");
}
}
运行结果:
可以看到,对象可以被拯救一次(finalize 执行第一次,但是不会执行第二次) 。
代码改一下,再来一次:
public class TestGC {
public static TestGC instance = null;
public void isAlive() {
System.out.println("我还活着");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize 拯救");
TestGC.instance = this;
}
public static void main(String[] args) {
instance = new TestGC();
//进行第一次GC
instance = null;
System.gc();
if (instance != null) {
instance.isAlive();
} else {
System.out.println("我死了");
}
//进行第二次GC
instance = null;
System.gc();
if (instance != null) {
instance.isAlive();
} else {
System.out.println("我死了");
}
}
}
运行结果:
对象没有被拯救,这个就是 finalize 方法执行缓慢,还没有完成拯救,垃圾回收器就已经回收掉了。 所以建议尽量不要使用 finalize,因为这个方法太不可靠。在生产中你很难控制方法的执行或者对象的调用顺序,建议忘了 finalize 方法!因为在 finalize 方法能做的工作,java 中有更好的,比如 try-finally 或者其他方式可以做得更好。