JVM之对象的内存模型、创建过程、对象引用、生命周期
JVM之对象的内存模型、创建过程、对象引用、生命周期
- Java对象内存模型
- 对象头
- 实例数据
- 对齐填充部分
- 对象的创建
- 类加载检查
- 分配内存
- 初始化零值
- 设置对象头
- 执行init方法
- 引用计数法
- 对象的引用
- 强引用
- 软引用
- 弱引用
- 虚引用
- 对象的生命周期
- 创建阶段(Created)
- 应用阶段(In Use)
- 不可见阶段(Invisible)
- 不可达阶段(Unreachable)
- 收集阶段(Collected)
- 终结阶段(Finalized)
- 对象空间重新分配阶段(De-allocated)
- 回收对象
Java对象内存模型
在Hotspot虚拟机中,一个Java对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充。
对象头
Hotspot虚拟机的对象头包括三部分信息:
Mark Word:用于存储对象自身的运行时数据,如:哈希码、GC 分代年龄、锁状态标志等
类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
数组长度:只有数组对象才有,在32位或者64位JVM中,长度都是32bit
实例数据
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充部分
对齐填充部分不是必须存在的,没有什么特别含义,仅起占位作用。
Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍。对象大小必须是8字节的整数倍,而对象头部分正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象的创建
类加载检查
JVM遇到new 指令,会先检查指令的参数能否在常量池中定位到这个类的符号引用,并检查符号引用代表的类是否已被加载过、解析和初始化过。如果没有,则先执行相应的类加载过程。
分配内存
JVM为新生对象分配内存。
对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
内存分配方式
指针碰撞 :
适用于内存规整即没有内存碎片的情况。
用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可
分配GC收集器:Serial, ParNew
空闲列表 :
适用于堆内存不规整的情况。
虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
分配GC收集器:CMS
具体内存分配取决于 Java 堆内存是否规整,而Java堆内存是否规整,取决于GC收集器的算法是标记-清除,还是标记-整理,复制算法内存也是规整的。
初始化零值
JVM将分配到的内存空间都初始化为零值,不包括对象头,保证对象实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
设置对象头
JVM对对象进行必要设置,如对象是哪个类实例、如何找到类元数据信息、对象哈希码、对象GC分代年龄等信息。
执行init方法
此时,从JVM来看,一个新对象已经产生了。从Java程序来看,对象创建才刚开始,init>方法还没有执行,所有字段都为零。
所以,执行new指令之后会接着执行init方法,把对象按照开发者意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不再被使用的,垃圾收集器将回收该对象使用的内存。
优缺:
引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.而且每次加减非常浪费内存。
对象的引用
强引用
在Java中最常见的就是强引用:把一个对象赋给一个引用变量,这个引用变量就是一个强引用。
当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM 也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
软引用
软引用需要用SoftReference类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
public static void main(String[] args) {
// 使用user实例
User user = new User();
user.setName("小白");
user.setAge(20);
System.out.println("user = " + user);
// 使用完user,将它设置为soft引用类型,并且释放强引用
SoftReference softReference = new SoftReference(user);
user = null;
if (softReference != null) {
user = (User) softReference.get();
} else {
// GC由于内存资源不足,可能系统已回收user的软引用,因此需要重新装载
user = new User();
user.setName("大白");
user.setAge(30);
softReference = new SoftReference(user);
}
System.out.println("user = " + user);
}
弱引用
弱引用需要用WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,总会回收该对象占用的内存。
public static void main(String[] args) throws InterruptedException {
// bytes对象
byte[] bytes = "Java".getBytes();
// 将bytes对象用弱引用持有
WeakReference<byte[]> bytesRef = new WeakReference<>(bytes);
System.out.println("第一次GC前" + bytes);
// 进行一次GC后查看对象的回收情况
System.gc();
// 等待GC
Thread.sleep(1000);
System.out.println("第一次GC后" + bytes);
// 将bytes对象的强引用去除
bytes = null;
// 等待GC
System.gc();
Thread.sleep(1000);
System.out.println("第二次GC后" + bytes);
}
第一次GC前[B@1e81f4dc
第一次GC后[B@1e81f4dc
第二次GC后null
虚引用
虚引用需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
对象的生命周期
在Java中,对象的生命周期包括:创建阶段、应用阶段、不可见阶段、不可达阶段、收集阶段、终结阶段、对象空间重分配阶段
等几个阶段。
创建阶段(Created)
为对象分配存储空间
开始构造对象
从超类到子类对static成员进行初始化
超类成员变量按顺序初始化,递归调用超类的构造方法
子类成员变量按顺序初始化,子类构造方法调用
一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段
应用阶段(In Use)
对象至少被一个强引用持有着
不可见阶段(Invisible)
当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然这些引用仍然是存在着的。
不可达阶段(Unreachable)
对象处于不可达阶段是指该对象不再被任何强引用所持有。
可见阶段是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为”GC root ”。存在着这些GC root会导致对象的内存泄露情况,无法被回收。
收集阶段(Collected)
当垃圾回收器发现该对象已经处于不可达阶段并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了收集阶段。
如果对象重写finalize() 方法,则会去执行该方法的终端操作。建议不要重载finazlie()方法
1.会影响JVM的对象分配与回收速度
在分配该对象时,JVM需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法。在该方法的执行时需要消耗CPU时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对象执行两次GC。
2.可能造成该对象的再次复活
在finalize()方法中,如果有其它的强引用再次持有该对象,则会导致对象的状态由收集阶段又重新变为应用阶段。已经破坏了Java对象的生命周期进程,且复活对象不利用后续的代码管理。
终结阶段(Finalized)
当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
对象空间重新分配阶段(De-allocated)
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为对象空间重新分配阶段。
回收对象
GC是由JVM自动完成的,根据JVM系统环境而定,因此,垃圾回收时机是不确定的,即回收对象时机是不确定的。
可以手动进行垃圾回收,如调用
System.gc()
方法通知
JVM进行一次垃圾回收,但具体什么时候运行无法控制。不建议手动调用该方法,因为GC消耗的资源比较大。
通常在以下情况会进行对象垃圾回收
当Eden区或者S区不够用
老年代空间不够用
方法区空间不够用
System.gc()