前言
先放上前几篇文章的Java执行一个类的流程图:
在之前的几篇文章中:
我记录了类加载前的类加载器初始化、双亲委派机制和类加载过程。在类加载之后便是创建对象了,那么对象是怎么创建的呢?
一、对象的创建流程
1、类加载检查
我们在代码中new
一个对象时,JVM首先会检查该类在方法区是否存在常量符号引用,接下来检查该类是否已被加载,即检查该类是否经历过验证、准备、解析和初始化几个类加载步骤。如果该类没有被加载过,那么JVM会先加载该类。
2、对象内存分配
在成功加载一个类之后,想要创建该类的对象,需要先在堆区为对象分配内存,内存大小在类加载结束后就能够确定。那么
- 对象内存被分配到堆区哪里呢?
- 怎么知道堆区中哪一块内存没有被使用?
- 遇到多个对象同时分配内存怎么办?
这里内容太多,放到下一节讲了。
3、对象初始化
这一步类似于类加载流程中的准备,只不过类加载的准备阶段是对类中的静态变量进行零值初始化,对象初始化是将分配好的内存里面的各个实例变量全部进行零值初始化。这一步操作保证在不给对象成员变量进行赋值初始化的情况下也能正常使用。
4、对象头设置
一个对象的内存其实划分为三个部分:对象头、实例数据和填充。实例数据是我们通常使用的对象数据,包含各种成员变量。设置填充部分的目的是让对象的内存总大小是8的倍数,这样会大大提高内存存取的效率。
我们在编程时能够很方便地使用对象的实例数据,但JVM需要时刻检测对象的归属类和运行状态等,因此将这部分信息放在对象头中。下面这张是从网上找的:
对象头分为运行时数据、类型指针和数组长度。
运行时数据包括哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,在32位机占4字节,在64位机占8字节。如上图,在对象头中,对于32位机,GC分代年龄占4位内存,那么很显然分代年龄的最大值为二进制1111
,也就是15。
类型指针(Klass Pointer)存储着该类的类元信息在方法区中的地址,类元信息底层是C++的对象。我们平时编程使用反射的一些方法来获取某个类的信息,这些方法最终也是使用C++实现的,而JAVA层面提供这些方法的类的对象则是存在于堆区的。简单的理解,我们可以把某个类的Class
对象当成是该类C++类元信息的一个镜像,而类型指针则是作为我们程序员使用Class
对象的方法访问C++类元信息的钥匙。在32位机中,类型指针占4个字节,在64位机中,类型指针占8个字节,开启指针压缩后占4个字节。
数组长度字段只有在数组对象中才会有,占4个字节。
5、执行init方法
这一步也类似于类加载的初始化步骤,只不过类加载的初始化步骤只会对类中的静态变量进行赋值初始化,而对象的<init>
初始化则是对实例变量进行赋值初始化。
二、指针压缩
指针压缩,顾名思义,就是压缩指针的内存大小,来达到节省内存的目的。
在64位的机器中,一个对象指针的大小占64位,也就是8个字节,而我们实际使用的电脑,内存大小无非是4G、8G、16G或者几十个G,使用32位就可以表示4G,那么几十个G也只需要使用三十多位来寻址,也就是剩下的二三十位就是闲置的。前面已经分析了对象的内存分配,一个对象至少包含一个类型指针,那么在实际应用中,假设有大量的对象存在,就会浪费掉很多内存空间。使用较大指针在主内存和缓存之间移动数据,会占用较大的宽带,同时GC压力也会很大。
因此存储的时候,可以对对象指针进行压缩,将三十多位的地址压缩到32位,也就是4个字节,在实际使用的时候再解压缩。目前指针压缩所支持的堆内存大小为4G~32G,小于4G则直接去除高32位,大于4G小于32G则进行指针压缩,因此在设置堆内存时最好别超过32位。
JDK1.8默认是打开指针压缩的,下面是一个测试指针压缩的代码:
package com.jim.jvm.classload;
import org.openjdk.jol.info.ClassLayout;
public class JOLTest {
public static void main(String[] args) {
ClassLayout classLayout1 = ClassLayout.parseInstance(new Object());
System.out.println(classLayout1.toPrintable());
ClassLayout classLayout2 = ClassLayout.parseInstance(new int[]{
});
System.out.println(classLayout2.toPrintable());
ClassLayout classLayout3 = ClassLayout.parseInstance(new Test2());
System.out.println(classLayout3.toPrintable());
}
}
class Test2{
int a;
String b;
float c;
byte d;
char[] e;
Test2 f;
}
代码中创建了一个Object对象、一个int数组对象和一个自定义对象,使用org.openjdk.jol.info.ClassLayout
可以将这几个对象的内存分配情况给打印出来。jol
可以在Maven工厂下载,在项目中引入后可直接使用。
下面是默认开启指针压缩情况下的结果:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header