1. java对象结构
- 不同的JVM的对象结构的实现不一样,这里以HotSpot JVM为例。HotSpot JVM并没有将Java实例对象直接一对一的映射到本地(native)的C++对象,而是设计了一个oop-klass模型。
- 什么是OOP? 实际上,OOP(Ordinary Object Pointer,普通对象指针)是指
对象-类
二者中的对象,表示对象的实例信息,从名字看是一个指针,实际并不仅仅是一个内存地址,而是对内存地址的一个描述或者对内存中数据结构的一个描述。所以,JVM中的对象的类被定义为oopDesc。 - 为了区别于Java语言中的Object对象,
JVM对象实例的C++类型
为instanceOopDesc,其基类为oopDesc,代码如下:
class oopDesc {
friend class VMstructs;
private:
volatile markOop mark; // 对象头
union_metadata {
wideKlassOop _klass; // 普通指针
narrowOop _compressed_klass; // 压缩类指针
}
private:
// 省略不相干的代码
}
class instanceOopDesc:public oopDesc { // 普通对象类型
// 省略不相干的代码
}
class arrayOopDesc:public oopDesc { // 数组对象类型
// 省略不相干的代码
}
- 每当在Java代码中创建一个对象时,JVM会创建一个instanceOopDesc实例来表示这个对象,此对象实例存放在堆区。类似地,每当在Java代码中创建一个数组时,JVM会创建一个arrayOopDesc实例来表示。所以,一个普通Java对象的底层为一个instanceOopDesc实例。
- 在oop-klass模型中什么是Klass呢? 实际上,Klass指的是
对象-类
二者中的类。为了区别于Java语言的Class类,JVM中用Klass来描述类型,Klass包含元数据和方法信息,用来描述语言层的类型。
// 用来描述语言层的类型
class Klass :public Metadata {
// 省略不相干的代码
// 指向java.lang.Class的 instance,mirroring this class即是这个类的影子类
OopHandle _java_mirror;
}
// 在虚拟机层面描述一个Java类
class InstanceKlass:public Klass{
// 省略不相干的代码
}
HotSpot为每一个已加载的Java类创建一个InstanceKlass对象,用来在JVM层表示Java元数据对象。但是这个InstanceKlass对象就是给JVM内部用的,并不直接暴露给Java层。实际上,给Java层用的类元数据对象为java.lang.Class类型的对象,或者说java.lang.Class类型的实例。
一份类的元数据就出现了两个对象:
- 一个Java层的java.lang.Class类型的实例
- 一个JVM层的InstanceKlass类型的实例。
根据前面的Java对象的底层介绍,一个普通Java对象的底层为一个instanceOopDesc实例。我们知道,Java层的java.lang.Class类型的实例也是一个普通对象,所以Class对象也就对应到一个instanceOopDesc实例。这个instanceOopDesc实例,被称为JVM层InstanceKlass实例的Java镜像
-
InstanceKlass实例可以导航到其Java镜像,具体的成员为 _java_miror(参考上面的代码),可以导航到instanceOopDesc实例,也就是java.lang.Class类型的实例。
-
大致了解oop-klass模型后,接下来就比较好介绍Java对象(Obiect实例)结构了,其实际上是C++中instanceOopDesc的结构。
Java对象(Obiect实例)结构包括三部分:
对象头
、对象体
和对齐字节
。
- 对象头
对象头包括三个字段,- 第一个字段叫作Mark Word(标记字),用于存储自身运行时的数据例如GC标志位、哈希码、锁状态等信息。
- 第二个字段叫作Class Pointer(类对象指针),用于存放此对象的元数据(InstanceKlass)的地址。虚拟机通过此指针可以确定这个对象是哪个类的实例。
- 第三个字段叫作Array Length(数组长度)。如果对象是一个Java数组,那么此字段必须有,用于记录数组长度的数据;如果对象不是一个Java数组,那么此字段不存在,所以这是一个可选字段。
- 对象体
对象体包含了对象的实例变量(成员变量),用于成员属性值,包括父类的成员属性值。这部分内存按4字节对齐。 - 对齐字节
对齐字节也叫作填充对齐,其作用是用来保证Java对象在所占内存字节数为8的倍数(8Nbytes)。HotSpotVM的内存管理要求对象起始地址必须是8字节的整数倍。对象头本身是8的倍数,当对象的实例变量数据不是8的倍数,需要填充数据来保证8字节的对齐。
2. 对象结构中的核心字段作用
对Object实例结构中几个重要的字段的作用做一下简要说明:
- Mark Word(标记字)字段主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode。
- Class Pointer(类对象指针)字段是一个指向方法区中类元数据信息的指针,意味着该对象可随时知道自己是哪个Class的实例。
- Array Length(数组长度)字段也占用32位(在32位JVM中)的字节,这是可选的,只有当本对象是一个数组对象时才会有这个部分。
- 对象体用于保存对象属性值,是对象的主体部分,占用的内存空间大小取决于对象的属性数量和类型。
- 对齐字节并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。当对象实例数据部分没有对齐(8字节的整数倍)时,就需要通过对齐填充来补全。
3.对象结构中的字段长度
-
Mark Word、Class Pointer、Aray Length等字段的长度都与JVM的位数有关。
-
Mark Word 的长度为JVM的一个Word(字)大小,也就是说32位JVM的Mark Word为32位,64位JVM为64位。
-
ClassPointer(类对象指针)字段的长度也为JVM的一个Word大小,即32位的JVM为32位,64位的IVM为64位。所以,在32位JVM虚拟机中,Mark Word和Class Pointer这两部分都是32位的;在64位JVM虚拟机中,Mark Word和Class Pointer这两部分都是64位的。
-
对于对象指针而言,如果JVM中对象数量过多,使用64位的指针将浪费大量内存,通过简单统计,64位的JVM将会比32位的IVM多耗费50%的内存。
-
为了节约内存可以使用选项
+UseCompressedOops
开启指针压缩。选项UseCompressedOops
中的Oop部分为Ordinary object pointer(普通对象指针)的缩写。-
如果开启UseCompressedOops选项,以下类型的指针将从64位压缩至32位:
Class对象的属性指针(即静态变量)。
Obiect对象的属性指针(即成员变量)。
普通对象数组的元素指针。
-
当然,也不是所有的指针都会压缩,一些特殊类型的指针不会压缩,比如指向PemmGen(永久代)的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
-
手动开启Oop对象指针压缩的Java指令为:
java -XX:+UseCompressed0ops mainclass
-
手动关闭Oop对象指针压缩的Java指令为:
java -XX:-UseCompressedOops mainclass
-
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度(Array Length字段)。Array Length字段的长度也随着JVM架构的不同而不同:在32位的JVM上,长度为32位;在64位JVM上,长度为64位。64位JVM如果开启了OOP对象的指针压缩,ArrayLength字段的长度也将由64位压缩至32位。
4. Mark Word 的结构信息
- Java内置锁的涉及很多重要信息,这些都存放在对象结构中,并且存放于对象头的Mark Word字段中。
- Mark Word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark Word为32位,64位JVM的Mark Word为64位。Mark Word的位长度不会受到OOP对象指针压缩选项的影响。
- Java内置锁的状态总共有4种,级别由低到高依次为:
无锁
、偏向锁
、轻量级锁
和重量级锁
。其实在JDK 1.6之前,Java内置锁还是一个重量级锁,是一个效率比较低下的锁,在JDK 1.6之后,JVM为了提高锁的获取与释放效率,对synchronized的实现进行了优化,引入了偏向锁、轻量级锁的实现,从此以后Java内置锁的状态就有了4种(无锁、偏向锁、轻量级锁和重量级锁),并且4种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别)。
4.1 不同锁状态下的 Mark Word 字段结构
- Mark Word字段的结构与Java内置锁的状态强相关。为了让Mark Word字段存储更多的信息,JVM将Mark Word的最低两个位设置为Java内置锁状态位,不同锁状态下的32位Mark Word结构。
64位的Mark Word与32位的Mark Word结构相似,具体如表2-2所示。
4.2 64 位 Mark Word 的构成
- 由于目前主流的JVM都是64位,使用64位的Mark Word,64位的Mark Word中各部分的内容。
- lock: 锁状态标记位,占两个二进制位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。
- biased lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁为0时表示对象没有偏向锁。
- lock和biased lock两个标记位组合在一起,共同表示Object实例处于什么样的锁状态。
-
age: 4 位的Java对象分代年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,因此最大值为15,这就是
-XX:MaxTenuringThreshold
选项最大值为15的原因。 -
identity_hashcode: 31位的对象标识HashCode(哈希码)采用延迟加载技术,当调用Object.hashCode()方法或者System.identityHashCode()方法计算对象的HashCode后,其结果将被写到该对象头中。当对象被锁定时,该值会移动到Monitor(监视器)中。
-
thread: 54位的线程ID值为持有偏向锁的线程ID。
-
epoch: 偏向时间戳。
-
ptr_to_lock_record: 占62位,在轻量级锁的状态下指向栈帧中锁记录的指针。
-
ptr_to_heavyweight_monitor:占62位,在重量级锁的状态下,指向对象监视器的指针。
32位的Mark Word与64位MarkWord结构相似。
4.3 无锁、偏向锁、轻量级锁和重量级锁
- 在JDK 1.6版本之前,所有的Java内置锁都是重量级锁。重量级锁会造成CPU在用户态和核心态之间频繁切换,所以代价高、效率低。
- JDK1.6版本为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁实现。所以,在JDK 1.6版本里内置锁一共有4种状态: 无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这些状态随着竞争情况逐渐升级。
- 内置锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种能升级却不能降级的策略,其目的是为了提高获得锁和释放锁的效率。
4.3.1 无锁状态
- Java对象刚创建时还没有任何线程来竞争,说明该对象处于无锁状态(无线程竞争它)这偏向锁标识位是
0
、锁状态01
。无锁状态下对象的Mark Word。
4.3.2 偏向锁状态
- 偏向锁是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
- 如果内置锁处于偏向状态,当有一个线程来竞争锁时,先用偏向锁,表示内置锁偏爱这个线程,这个线程要执行该锁关联的同步代码时,不需要再做任何检查和切换。
- 偏向锁在竞争不激烈的情况下效率非常高。
- 偏向锁状态的Mark Word会记录内置锁自己偏爱的线程ID,内置锁会将该线程当作自己的熟人。偏向锁状态下对象的Mark Word。
4.3.3 轻量级锁状态
- 当有两个线程开始竞争这个锁对象时,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录。
- 当锁处于偏向锁又被另一个线程所企图抢占时,偏向锁就会升级为轻量级锁。企图抢占的线程会通过自旋的形式尝试获取锁,不会阻塞抢锁线程,以便提高性能。轻量级锁状态下对象的Mark Word。
- 自旋原理: 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核切换的消耗。但是,线程自旋是需要消耗 CPU的,如果一直获取不到锁,那线程也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。JVM对于自旋周期的选择,JDK1.6之后引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不是固定的,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定的。线程如果自旋成功了,下次自旋的次数就会更多,如果自旋失败了,自旋的次数就会减少。如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,自旋不会一直持续下去,这时争用线程会停止自旋进入阻塞状态,该锁膨胀为重量级锁。
4.3.4 重量级锁状态
- 重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也就叫同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,该监视器对象用集合的形式来登记和管理排队的线程。重量级锁状态下对象的Mark Word。