对象创建与内存分配
Java中创建对象并为其分配内存的过程如下:
- 1、当执行到new指令时,虚拟机会先检查对应的类是否被加载过,如果没有被加载,那么执行类加载的过程
- 2、加载完毕后就需要为对象分配内存空间
- 3、初始化操作,比如将空间初始化为零值,调用构造函数
- 4、虚拟机堆对象进行必要的设置,比如该对象是哪个类的实例、如何才能找到类的元数据信息,对象的GC分代年龄等信息。
为对象分配内存
根据堆内存的规整状态(堆内存是否规整又由垃圾收集器的算法规则决定)常用如下两种方式:
- 指针碰撞:
如果堆内存是整齐的,使用的内存和未使用的内存以分界点分开,那么为新生对象分配内存就是将分界点挪动对象同等大小的位置就可以了。
虚拟机采用了CAS和失败重试的方式保证内存分配的线程安全。 - 空闲列表
如果堆内存是不整齐的,使用的内存和未使用的内存没有明确的分界点,零零散散的
虚拟机必须得记录哪些内存是正在使用的,哪些内存是未使用的
分配内存的时候需要找一块大于或者等于当前对象大小的内存空间,这种分配算法称为空闲列表
对象复活
在根搜索中得到的不可达对象并不是立即就被标记成可回收的,而是先进行一次标记放入F-Queue等待执行对象的finalize()方法,执行后GC将进行二次标记,复活的对象之后将不会被回收。因此,使对象复活的唯一办法就是重写finalize()方法,并使对象重新被引用。
package com.cdai.jvm.gc;
public class DeadToRebirth {
private static DeadToRebirth hook;
@Override
public void finalize() throws Throwable {
super.finalize();
DeadToRebirth.hook = this;
}
public static void main(String[] args) throws Exception {
DeadToRebirth.hook = new DeadToRebirth();
DeadToRebirth.hook = null;
System.gc();
Thread.sleep(500);
if (DeadToRebirth.hook != null)
System.out.println("Rebirth!");
else
System.out.println("Dead!");
DeadToRebirth.hook = null;
System.gc();
Thread.sleep(500);
if (DeadToRebirth.hook != null)
System.out.println("Rebirth!");
else
System.out.println("Dead!");
}
}
要注意的两点是:
第一,finalize()方法只会被执行一次,所以对象只有一次复活的机会。
第二,执行GC后,要停顿半秒等待优先级很低的finalize()执行完毕。
对象访问方式
创建好Java对象之后,就需要使用对象。在虚拟机栈上只定义了一个指向堆内的引用,主流有两种方式去操作对象:
- 使用句柄
此种方案会在堆中创建一个句柄池,栈中的引用存储的是对象句柄的地址,因此栈中的引用指向堆中的句柄池中的某个句柄,而对象句柄则存储了对象实例数据和类型数据的信息(它们的具体地址),示意图(来自《深入理解Java虚拟机》)如下:
- 直接指针
此种方案就是栈中的引用存储的就是堆中对象的地址,直接指向堆中的对象,因此在堆中对象的存储布局中就要考虑如何存放类型数据等信息,示意图(来自《深入理解Java虚拟机》)如下:
说明:上述两种方式各有优缺点。
对于句柄方式,由于栈引用存储的是对象句柄地址,因此对象移动时(GC引发)只需要修改对应句柄指向的对象地址即可,栈中的引用不用修改;其缺点就是访问较慢,因为有两次指针定位。
对于直接指针方式,它的优点就是只有一次指针定位,访问速度快,当然句柄方式的优点也是其缺点。Sun的HotSpot虚拟机是使用直接指针的方式来访问对象的。
对象回收
程序计数器、虚拟机栈、本地方法栈。这几个区域完全不用管回收问题,因为方法结束或者线程结束的时候他们所占用的内存就自然跟着一起释放了,3个区域随线程而生,随线程而灭。
所以我们只需要管堆和方法区。尤其是堆,因为一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,这部分内存的分配和垃圾回收都是动态的。
1、引用计数法(ReferenceCounting)
- 1算法
给对象中添加一个引用计数器,每当有一个地方引用他时,计数器值就+1,;当引用失效时,计数器值就-1;任何时刻计数器为0的对象就是不可能在被使用。
- 2 优缺点
(1)、优点
判定效率很高
(2)、缺点
不会完全准确,因为如果出现两个对象相互引用的问题就不行了。如下代码所示:
/**
* testGC()方法执行后会不会被GC? 不会!!!!
*
* @author TongWei.Chen 2017-09-05 11:15:53
*/
public class ReferenceCountingGC {
public Object instance = null;
public static void testGC() {
//step 1
ReferenceCountingGC objA = new ReferenceCountingGC();
//step 2
ReferenceCountingGC objB = new ReferenceCountingGC();
//相互引用
//step 3
objA.instance = objB;
//step 4
objB.instance = objA;
//step 5
objA = null;
//step 6
objB = null;
//假设在这行发生CG,objA和objB是否能被回收? 不能!!!!
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
(3)、分析上述代码
step1:objA的引用+1 =1
step2:objB的引用+1 =1
step3:objB的引用+1 =2
step4:objA的引用+1 =2
step5:objA的引用-1 =1
step6:objB的引用-1 =1
很明显,到最后两个实例都不再用了(都等于null了),但是GC却无法回收,因为引用数不是0,而是1,这就造成了内存泄漏。也很明显,现在虚拟机都不采用此方式。
可达性分析算法(Reachability Analysis)
- 1 算法
通过一系列的GC Roots的对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
说明:
(1.1)、红色代表不可达对象(可回收对象)
(1.2)、千万注意!!!!!上图并不是说方法区全可达,虚拟机栈部分可达,本地方法栈全部不可达,而只是为了说明这三个部分可以作为GC Roots!
- 2 可以作为GC Roots的对象包括以下几点
(2.1)、虚拟机栈(栈帧中的本地变量表)中引用的对象。
(2.2)、方法区中的类静态属性引用的对象或者常量引用的对象。
(2.3)、本地方法栈中JNI(就是native方法)引用的对象。