对象
对象创建
JVM运行new指令时,先在常量池中查找该类的符号引用,并检查该类是否已加载、解析和初始化过,如果没有则需先执行类加载过程
随后在Java堆中分配内存,对象所需内存在类加载后便可完全确定,分配方式有两种:
- 指针碰撞:内存是连续的,已使用内存和未使用内存以指针为界,指针向未使用内存移动一段与对象大小相等的距离
- 空闲列表:内存不连续,未使用内存记录在列表,从列表中划分内存
在分配内存中需要保证线程安全,采用2种方式:
- 分配时进行同步
- 在堆中为每个线程预设本地线程分配缓冲区(TLAB),TLAB用完后分配新缓冲区时才需要同步,可通过-XX:+/-UseTLAB选择是否开启此功能
内存(TLAB)分配完成后都会被初始化为零值,只有在Class文件的<init>()方法(即java的构造函数)执行后才会初始化为正确的值
对象的存储结构
对象在堆中的存储结构分为3部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
对象头
对象头包括如下信息:
-
对象自身运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,称为Mark Word
-
类型指针,用于确实对象是哪个类的实例,并非所有虚拟机都有该实现
-
若对象是数组还需要记录数组长度,普通对象则可根据元数据确认对象大小
实例数据
实例数据是代码中所定义的各种类型的字段内容,包括父类及自身的字段
存储顺序与JVM分配策略参数(–XX:FieldsAllocationStyle)及字段在Java中定义的顺序有关
默认分配顺序为longs/doubles、ints、shorts/chars、byte/boolean、Ordinary Object Pointers
默认父类变量出现在子类前,当+XX:CompactFields设为true,则允许子类中较窄的变量插入父类变量的空隙中
字节对齐
字节对齐起占位符作用,用于保证对象大小为8kb的整数倍,并不一定存在
对象的使用
对象的访问方式
通过栈上的reference类型数据指向对象的引用,有句柄和直接指针两种方式
上图为句柄,好处是reference存储的地址不会改变,对象被移动时只会改变句柄内部的指针
上图为直接指针,只需要一次定位,速度更快,Hotspot采用的就是这种方式
直接指针用于准确式内存管理的JVM中,虚拟机可以知道内存中某个位置的数据具体是什么类型,如内存中有一个整数123456,能分辨出它是一个指向了123456的内存地址的引用类型还是一个数值为123456的整数
而非准确式内存管理的JVM只能通过句柄保持引用的稳定(因为垃圾收集后对象会移动位置,无法确定修改的是地址还是数据)
对象的引用类型
强引用(Strongly Reference):类似Object obj = new Object()的引用关系,只有强引用还在,就不会回收被引用的对象
软引用(Soft Reference):一些还有用,但非必要的对象,其关联的对象,在系统OutOfMemoryError前进行回收
弱引用(Weak Reference):强度更弱,其关联的对象在下一次gc时被回收(无论当前内存是否充足都会回收)
虚引用(Phantom Reference):强度最弱,不会对生存时间构成影响,也无法通过虚引用获取实例,其存在目的是为了在对象被gc时收到一个系统通知
对象存活与否
引用计数算法(Reference Counting)
引用计数算法通过在对象中添加引用计数器
- 每当有一个地方引用它时,计数器值就加一
- 当引用失效时,计数器值就减一
- 计数器为零的对象就是不可用的
引用计数算法原理简单、效率高,但有很多额外情况需要考虑,如循环引用
可达性分析算法(Reachability Analysis)
JVM采用的就是可达性分析算法,以GC Roots为根结点,根据引用关系向下搜索(搜索路径称为引用链),若某个对象到GC Roots间没有任何引用链相连,即不可达,则对象不可用
可作为GC Roots的对象有:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象
- 在方法区中类静态属性引用的对象
- 在方法区中常量引用的对象
- 在本地方法栈中JNI引用的对象
- Java虚拟机内部的引用
- 所有被synchronized的对象
- 反映JVM内部情况的JMXBean、本地代码缓存等
除此之外,根据垃圾收集器和回收的内存区域不同,还可以有其他对象临时成为GC Roots,如针对某一区域进行gc时,其关联区域的对象会成为GC Roots
对象的回收
若对象经过可达性分析后发现没有与GC Roots相连接的引用链,其会被第一次标记,随后进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法
下面两种情况无需执行finalize()方法
- 对象没覆盖finalize()方法
- finalize()方法已被JVM调用
若需要执行finalize()方法
- 对象进入F-Queue队列
- JVM创建的Finalizer线程去执行队列中对象的finalize()方法,对象可在finalize()中复活跳脱标记
- 收集器将对队列中的对象进行第二次标记
- 对标记对象进行回收
如下代码利用finalize()中复活对象
public class Test {
public static Test SAVE_HOOK = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize()");
SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable{
SAVE_HOOK = new Test();
selfSave();
selfSave();
}
private static void selfSave() throws InterruptedException {
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
System.out.println("alive");
} else {
System.out.println("dead");
}
}
}
如上调用两次selfSave()复活,一次成功一次失败,因为finalize()只会被系统自动调用一次
finalize()
alive
dead