创建对象流程
- 一个对象的创建过程一般如下:
- 首先是代码执行到new关键词,于是根据new后面的参数到常量池总定位该类的符号引用。
- 如果没有找到这个符号引用,说明类还没被加载,那么进行类的加载、连接和初始化。
- 然后jvm为实例在堆中分配内存,并把该内存空间都初始化为0值。
- 初始化后,jvm会进行一些必要的设置,如,把这个对象是哪个类的实例、在GC中的分代年龄信息放到对象头中。
- 通过构造函数对该对象进行初始化。
为实例分配内存
- 在堆中给实例分配内存的方式有两种:指针碰撞和空闲列表。具体使用哪一种,就要看jvm中使用的是什么垃圾回收机制了。
- 指针碰撞:如果回收空间后能做到压缩整理,使得Java堆中的内存是绝对规整的,即所有已分配内存都凑在一块连续空间,而空闲的凑在另一块,那么只要在中间放着一个指针作为分界点的指示器,在分配空间时把指针向空闲那边移动(即把部分“空闲空间”划到“已分配空间”那边)即可。
- 空闲列表:有一个列表,其中记录了哪些内存块空闲,在分配的时候从列表中找到一块足够大的划分给实例,然后更新列表。
对象具体内容
- 对象在内存中包括三个部分:对象头、实例数据和对其填充。
- 对象头包括对象本身运行时的数据MarkWord(哈希码、GC分代年龄、线程持有的锁、锁状态、偏向时间戳、偏向线程id等)和类型指针(通过该指针可确定是哪个类的实例,如果是数组,还会存储数组的长度)。
- 实例数据是对象真正存储的有效信息,即我们定义的各种字段的内容。
- 对其填充是把实例数据填充补全到8的整数倍,因为HotSpot的内存管理要求对任何对象的大小必须是8的整数倍,而对象头已经被设计为8的整数倍了,但是实例数据就不一定了,所以需要补全。
垃圾回收
判断对象是否已死
- 垃圾收集器在进行垃圾回收之前,要先判断对象是否已死,已死才能被回收。下面介绍两种主流的判断对象已死的算法。
-
引用计数算法:在对象中添加一个引用计数器,每当一个对象,计数器就加一,每当引用失效则减一。计数器为0时,说明对象不被任何人引用,那么可以回收。
public static void method() {
A a = new A();
}public static void main(String[] args) {
method();
}
-
当调用method()时,执行到new,把A的实例赋值给局部变量a时,堆中A的实例的计数器就会加一;当方法结束时,局部变量a随之销毁,那么A的实例的计数器就会减一。
-
python就是使用这种算法,但在Java中,该算法不能解决循环引用问题,即当对象A,B互相引用时,即A引用了B,B也包含了A的引用,那么当它们都为null时,它们的计数器也并不为0,如果采用引用计数算法,那么此时Java就不能回收它们了,显然是错误的。
class A {
private B b;
public void setB(B b) {
this.b = b;
}
}class B {
private A a = new A();
public void setA(A a) {
this.a = a;
}
}public void method() {
A a = new A();
B b = new B();
a.setB(b);
b.setA(a);
} -
堆中的实例A同时被栈内的局部变量a和堆内的实例B的成员变量a引用,实例B同时被栈内的局部变量b和堆内的实例A的成员变量b引用。
- 当method方法执行完毕后,栈被销毁,局部变量a,b自然也被销毁,于是它们对实例A,B的引用都失效,即图中两条红线消失,实例A,B的计数器各自减一;但问题是,堆中的实例不会随着方法完毕而被销毁,于是实例A中的成员变量a仍保持着对实例B的引用,所以实例B的计数器至少还是1,实例A的计数器同理也是1,但此时已经没有地方在用它们了,它们却还是不能被回收。
- 可达性分析算法:判断当前引用的对象是否处于某一GCRoot的引用链上。一类可以作为GCRoot的对象是栈中的对象,因为栈帧随着方法的执行自动进栈和出栈,所以在栈中的对象肯定是有用的,那么其引用的对象也必然是有用的,所以只要在这条引用链上的对象,就肯定得判定为存活。PS:可作为GCRoot的对象:虚拟机栈和本地方法栈中的对象、方法区中的静态属性、方法区中的常量。
- JVM实际也会回收方法区内的常量和不再使用的类的信息。不过并没有强制要求回收方法区,主要原因是性价比太低,即判断条件高,回收空间少。对于常量,如果任何对象都没有引用它,那么就可以回收了,对于类,判断其是否可回收比较复杂:该类的所有实例被回收;加载该类的类加载器被回收;没有通过反射使用该类。