前言

上一篇文章对JVM的运行时数据区域的内容进行了梳理,本篇文章对JVM中的对象和对象的内存布局进行深入解析。本文参考了《深入理解Java虚拟机》、《深入解析Java虚拟机HotSpot》、《HotSpot实战》三本书。

下面提到的虚拟机都特指JDK1.8版本的HotSpot VM,其他虚拟机的实现有可能不太一样。

类与对象

在编译时,通过Javac编译器为虚拟机规范的class文件格式。class文件格式是与操作系统和机器指令集无关的、平台中立的格式。其他语言编写的代码只需要实现指定语言的编译器编译位JVM规范标准的class文件就可以实现该语言运行在JVM之上,这就是JVM的语言无关性。

通过java命令运行class文件,首先会通过类加载器将class文件加载到内存中,加载class文件会为类生成一个klass实例。在klass包含了用于描述Java类的元数据,包括字段个数、大小、是否为数组、是否有父类、方法信息等。

对象类二分模型

HotSpot虚拟机是使用C++实现的, C++也是面向对象语言。可以采用java类一一映射到C++类,当创建Java对象就创建对应的C++类的对象。

但是由于如果C++的对象含有虚函数,则创建的对象会有虚方法表指针,指向虚方法表。如果采用这种直接一对一映射的方式,会导致含有虚方法的类创建的对象都包含虚方法指针。因此在HotSpot虚拟机中,通过对象类二分模型,将类描述信息和实例数据进行拆分。使用仅包含数据不包含方法的oop(Ordinary Object Pointer)对象描述Java的对象,使用klass描述java的类。oop的职责在于表示对象实例数据,没必要维护虚函数指针。通过oop对象头部的一个指针指向对应的klass对象进行关联。

在HotSpot虚拟机中,普通对象的类通过instanceKlass表示,对象实例则通过instanceOopDesc表示。

在JVM中引用类型可以分为对象,基本类型数组和对象类型数组。可以分别映射到Java中的对应的对象和类型。

对象
对象 instanceKlass instanceOopDesc
基本类型数组 typeArrayKlass typeArrayOopDesc
对象类型数组 objArrayKlass objArrayOopDesc

除了常用的3类引用对象外,还有一些其他JVM自己要用的java.lang.ClassLoaderInstanceClassLoaderKlass描述,java.lang.ClassInstanceMirrorKlass描述等。

对象

HotSpot VM使用oop描述对象,oop字面意思是“普通对象指针”。它是指向一片内存的指针,只是将这片内存‘视作’(强制类型转换)Java对象/数组。对象的本质就是用对象头和字段数据填充这片内存。

对象内存布局

JOL工具

在谈论具体对象布局时,推荐一个JOL工具,可以打印对象的内存布局。通过maven引入。

<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>

通过ClassLayout.parseInstance(new Object()).toPrintable()即可打印对象的内存布局。

对象头

普通对象的对象头包含2部分,第一部分被称为Mark Word,第二部分为类型指针。如果对象为数组,除了普通对象的两部分外对象头还包含数组长度。下图是64位虚拟机对象头。

32位虚拟机头部的Mark Word长度为4个字节。

Mark Word

Mark Word保存了对象运行时必要的信息,包括哈希码(HashCode)、GC分代年龄、偏向状态、锁状态标志、偏向线程ID、偏向时间戳等信息。通过类型指针,可以找到对象对应的类型信息。32位虚拟机和64位虚拟机的Mark Word长度分别为4字节和8字节。

不论是32位还是64位虚拟机的对象头部都使用了4比特记录分代年龄,每次GC时对象幸存年龄都会加1,因此对象在survivor区最多幸存15次,超过15次时,仍然有可达根的对象就会从survivor区被转移到老年代。可以通过-XX:MaxTenuringThreshold=15参数修改最大幸存年龄。

CMS垃圾回收器默认为6次。

类型句柄

相比32位对象头大小,64位对象头更大一些,64位虚拟机对象头的Mark Word类型指针地址都是8字节。而通常情况,我们的程序不需要占用那么大的内存。因此虚拟机通过压缩指针功能,将对象头的类型指针进行压缩。而Mark Word由于运行时需要保存的头部信息会大于4字节,仍然使用8字节。若配置开启了-XX:+UseCompressedOops,虚拟机会将类型指针地址压缩为32位。若配置开启了-XX:+UseCompressedClassPointers,则会压缩klass对象的地址为32位。

需要注意的是,当地址经过压缩后,寻址范围不可避免的会降低。对于64位CPU,由于目前内存一般到不了2^64,因此大多数64位CPU的地址总线实际会小于64位,比如48位。

开启-XX:+UseCompressedOops,默认也会开启-XX:+UseCompressedClassPointers。关闭-XX:+UseCompressedOops,默认也会关闭-XX:+UseCompressedClassPointers

如果开启-XX:+UseCompressedOops,但是关闭-XX:+UseCompressedClassPointers,启动虚拟机的时候会提示“Java HotSpot(TM) 64-Bit Server VM warning: UseCompressedClassPointers requires UseCompressedOops”。

普通对象内存布局(64位虚拟机指针压缩时)


System.out.println(ClassLayout.parseInstance(new Object()).toPrintable()); java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) Mark Word
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 类型指针
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

需要注意,由于内存按小端模式分布,因此显示的内容是反着的。上面实际对象头内容为 00000000 00000001 f80001e5

数组对象内存布局(64位虚拟机指针压缩时)

[Ljava.lang.Object; object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) Mark Word
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) f5 22 00 f8 (11110101 00100010 00000000 11111000) (-134208779) 类型指针
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 数组长度
16 4 java.lang.Object Object;.<elements> N/A 数组元素
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

对象头与锁膨胀

对象头中存储了锁的必要信息,不同的锁的对象头存储内容稍有不同。32位对象头存储格式如下

JVM底层对加锁进行了性能优化,默认虚拟机启动后大约4秒会开启偏向锁功能。当虚拟机未启用偏向锁时,锁的演化过程为无锁->轻量锁(自旋锁)->重量锁

当虚拟机启用了偏向锁时,锁的演化过程为无锁->偏向锁->轻量锁(自旋锁)->重量锁

本文不讨论JVM对加锁的具体优化逻辑,内容比较多,感兴趣的可以看同学可以参考《浅谈偏向锁、轻量级锁、重量级锁》。

无锁

当对象未加锁时,锁状态为01,32位虚拟机的对象头部如图所示


public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
/*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) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total*/
}

需要注意的是其中对象头保存的hashCode被称为identityHashCode,当我们调用对象的hashCode方法,返回的就是该值。若我们重写了hashCode的值,对象头的hashCode值仍然是内部的identityHashCode,而不是我们重写的hashCode值。可以通过System.identityHashCode打印identityHashCode,或者也可以通过toString直接打印对象输出16进制的identityHashCode


public static void main(String[] args){
Object obj1 = new Object();
System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
System.out.println(obj1.hashCode());
System.out.println(System.identityHashCode(obj1));
System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
System.out.println(obj1); /*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 1766822961
1766822961
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 31 94 4f (00000001 00110001 10010100 01001111) (1335111937)
4 4 (object header) 69 00 00 00 (01101001 00000000 00000000 00000000) (105)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total java.lang.Object@694f9431 十进制即为1766822961,二进制为01101001 01001111 10010100 00110001
*/
}
偏向锁

偏向锁中的“偏”,就是偏心的“偏”、偏袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

偏向锁的锁状态和未锁状态一样都是01,当对象处于偏向状态时,偏向标记为1;当对象处于未偏向时,偏向标记为0

32位虚拟机的偏向锁对象头部如图所示

偏向时间戳,它实际表示偏向的有效期。

无锁状态升级为偏向锁的条件:

  • 对象可偏向,对象未加锁时,执行CAS更新对象头部线程偏向线程ID为当前线程成功。
  • 对象可偏向,对象已加锁,但偏向线程ID为空,执行CAS更新对象头部线程偏向线程ID为当前线程成功。
  • 对象可偏向,对象已加锁,且偏向线程ID等于当前线程ID。
  • 对象可偏向,对象已加锁,且偏向线程ID不为空且不等于当前线程ID,执行CAS更新对象头部线程偏向线程ID为当前线程成功。

虚拟机启动时,会根据-XX:BiasedLockingStartupDelay配置延迟启动偏向,在JDK1.8中,默认为4秒。有需要时可以通过-XX:BiasedLockingStartupDelay=0关闭延时偏向。

//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args){
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 28 9c 02 (00000101 00101000 10011100 00000010) (43788293)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
}
}
轻量级锁

轻量级锁的锁状态为00,32位虚拟机的轻量级锁头部格式如下

升级为轻量级锁条件:

  • 对象不可偏向,跳过偏向锁直接使用轻量级锁。
  • 对象可偏向,但偏向加锁失败(存在线程竞争)。
  • 对象获取调用hashCode后加锁。
  • 对象已升级为重量级锁后,锁降级只能降级为轻量级锁,无法降级为偏向锁。

轻量级锁会在线程的栈帧中开辟一个锁记录区域,将当前对象的头部保存在锁记录区域中,将锁记录区域的地址保存到当前对象头部。

  • 对象不可偏向直接升级到轻量锁
public static void main(String[] args){
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 58 f4 dd 02 (01011000 11110100 11011101 00000010) (48100440)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
}
  • 偏向锁竞争升级为轻量锁
//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args){
final Object o = new Object();
Thread thread= new Thread(){
@Override
public void run() {
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable()); //偏向锁
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 e8 31 28 (00000101 11101000 00110001 00101000) (674359301)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
}
}
};
thread.start();
thread.join();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable()); //轻量锁
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 60 f4 b4 02 (01100000 11110100 10110100 00000010) (45413472)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
*/
}
}
  • 偏向后调用hashCode方法升级为轻量级锁
//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args){
final Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable()); //偏向锁
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
o.hashCode();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 18 f3 4a 02 (00011000 11110011 01001010 00000010) (38466328)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
*/
}
}
重量级锁

轻量级锁的锁状态为10,32位重量级锁头部如图所示

轻量级锁自循环一定次数后一致获取不到锁,则升级为重量级锁条件。自旋次数默认为10次,可以通过-XX:PreBlockSpin配置修改次数。

//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws InterruptedException {
final Object o = new Object();
Thread thread= new Thread(){
@Override
public void run() {
synchronized (o){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 37 c6 25 (00101010 00110111 11000110 00100101) (633747242)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
*/
}
}
};
thread.start();
synchronized (o){
Thread.sleep(1000);
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 37 c6 25 (00101010 00110111 11000110 00100101) (633747242)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
}
thread.join();
}
重量级锁降级

当重量级锁解锁后就会进行锁降级,锁降级只能降级为轻量锁,无法再使用偏向锁。

//-XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws InterruptedException {
final Object o = new Object();
Thread thread= new Thread(){
@Override
public void run() {
synchronized (o){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 37 c6 25 (00101010 00110111 11000110 00100101) (633747242)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
*/
}
}
};
thread.start();
synchronized (o){
Thread.sleep(1000);
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2a 37 c6 25 (00101010 00110111 11000110 00100101) (633747242)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
*/
}
thread.join();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
*/
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
/*
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 98 f6 8c 02 (10011000 11110110 10001100 00000010) (42792600)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
*/
}
}

实例数据

对象实例数据默认按照long、double、int、short、char、byte、boolean、reference顺序布局,相同字段宽度总是分配在一起。若有父对象,则父对象的实例字段在子对象前面。

另外如果HotSpot虚拟机的 +XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。

填充

JVM中,对象大小默认为8的整数倍,若对象大小不能被8整除,则会填充空字节来填充对象保证。

对象生命周期

在了解完对象头部后,我们看下对象的创建的时候发生了什么事情。当我们调用new Object()创建一个对象时,生成的字节码如下

0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>

首先通过new指令分配对象,并将对象地址入栈,通过dup指令复制一份栈顶元素。通过invokespecial指令调用对象的init进行初始化会消耗栈顶2个槽。由于init方法需要传入一个参数,该参数即为引用对象本身。在init初始化时会将this指针进行赋值。这样我们在代码中就可以通过this指向当前对象。

对象创建流程如下图所示。

  • 栈上分配

    通常对象都是在堆上创建的,若对象仅在当前作用域下使用,那么使用完很快就会被GC回收。JVM通过逃逸分析对对象作用域进行分析,如果对象仅在当前作用域下使用,则将对象的实例数据分配在栈上,从而提升对象创建速度的同时减少GC回收的对象数量。

  • 线程局部缓冲区(TLAB)

    如果无法在栈上分配,则对象会在堆上分配。对于JDK1.8来说,Java堆通常使用分代模型,(关于GC,垃圾回收算法等这里不做具体讨论)。经过统计,90%的对象在使用完成后都会被回收,因此默认新生代会分配10%的空间给幸存者区。



    对象先在eden区进行分配,但是我们知道,堆是所有线程共享的区域,会存在多线程并发问题。因此在堆上分配就需要进行线程同步。为了提高分配效率,JVM会为每个线程从eden区初始化一块堆内存,该内存是线程私有的。这样每次分配对象时就无需进行同步操作,从而提高对象分配效率。线程的这块局部内存区域被称为线程局部缓冲区(TLAB)。通常这块内存会小于eden区的1%。当这块内存用完时,就会重新通过CAS的方式为线程重新分配一块TLAB。

    通常对象分配有两种方式,一种是线性分配,当内存是规整时(大部分垃圾回收器新生代都是用标记清理算法,可以保证内存规整),通过一个指针向后移动对象大小,直接分配一块内存给对象,指针左边是已使用的内存,指针右边是未使用的内存,这种方式被称为指针碰撞。TLAB配合指针碰撞技术能够在线程安全的情况下移动一次指针直接就可以完成对象的内存分配。



    当内存不规整时(比如CMS垃圾回收器通常情况并不会每次GC后都压缩内存,会存在内存碎片),则需要一块额外的内存记录哪些内存是空闲的,这个缓存被称为空闲列表

  • eden区分配

    如果TLAB无法分配对象,那么对象只能在Eden区直接分配,前面说过,在堆上分配,必须采用同步策略避免有产生线程安全问题。如果分配内存时,对象的klass没有解析过,则需要先进行类加载过程,然后才能分配对象。这个过程被称为慢速分配,而如果klass已解析过则直接可以分配对象,这个过程被称为快速分配

  • 老年代分配

    当eden区放不下对象时(当然还有其他的判断策略,这里暂时不去关心),对象直接分配到老年代。

  • 对象实例初始化

    当对象完成内存分配时,就会初始化对象,将内存清零。需要注意,对象的静态变量在类初始化的初始化阶段已经完成设置。

  • 初始化对象头部

    当对象实例初始化完,就会设置对象头部,默认的对象头部存放在klass,如果启用了偏向,则设置的就是可偏向的对象头。

对象访问方式

现在我们了解了对象的内存布局和对象的创建逻辑,那么对象在运行时,如何通过栈的局部变量找到实际的对象呢?常用的对象访问方式有2种,直接指针访问句柄访问

直接指针访问

对象创建时,局部变量表只保存对象的地址,地址指向的是堆中的实际对象的markword地址,JVM中采用的就是这种方式访问对象。

句柄访问

通过句柄访问时局部变量保存的时句柄池的对象句柄,句柄池中,则会存储对象实例指针和对象类型指针。再通过这两个指针分别指向对象实例池中的对象和元数据的klass。

相比直接指针访问,这种访问方式由于需要2次访问,而直接指针只需要一次访问,因此句柄访问对象的速度相对较慢。但是对于垃圾回收器来说是比较友好的,因为对象移动无需更新栈中的局部变量表的内容,只需要更新句柄池中的对象实例指针的值。

HSDB

前面我们通过JOL工具可以很方便的输出对象的布局。JDK也提供了一些工具可以查看更详细的运行时数据。

HSDB(Hotspot Debugger) 是 JDK1.8 自带的工具,使用该工具可以连接到运行时的java进程,查看到JVM运行时的状态。

以该偏向锁代码为例

//-XX:BiasedLockingStartupDelay=0
public class BiasedLock {
public static void main(String[] args) {
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
/*
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 28 64 03 (00000101 00101000 01100100 00000011) (56895493)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
*/

为了能看到运行时状态,我们可以使用idea工具单笔调试,也可以使用jdb工具进行调试。jdb是Java的调试器,位于%JAVA_HOME%/bin下面。 通过jdb -classpath XXX class名 执行main方法。

执行后,我们可以将打断点,然后进行调试。

  • 通过stop in <class id>.<method>[(argument_type,...)]在方法中打断点,或者可以通过stop at <class id>:<line>在指定行打断点。
  • 通过stop in com.company.BiasedLock.main将断点打在main方法。
  • 通过run运行
  • 通过next进行调试。(可以使用step进行单步调试)
C:\Users\Dm_ca>jdb -classpath "D:\study\java\symbolreference\target\classes;D:\develop\mavenrepository\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar" com.company.lock.BiasedLock
正在初始化jdb...
> stop in com.company.lock.BiasedLock.main
正在延迟断点com.company.lock.BiasedLock.main。
将在加载类后设置。
> run
运行com.company.lock.BiasedLock
设置未捕获的java.lang.Throwable
设置延迟的未捕获的java.lang.Throwable
>
VM 已启动: 设置延迟的断点com.company.lock.BiasedLock.main 断点命中: "线程=main", com.company.lock.BiasedLock.main(), 行=8 bci=0 main[1] next
>
已完成的步骤: "线程=main", com.company.lock.BiasedLock.main(), 行=9 bci=8
...
未挂起任何对象。
> java.lang.Object o
bject internals:
OFFSET已完成的步骤: SIZE TYPE DESCRIPTION VALUE
"线程=main", com.company.lock.BiasedLock.main(), 行=11 bci=25
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

此时我们可以通过HSDB连接到进程。通过JPS 命令查看进程的pid

C:\Users\Dm_ca>jps
17072
22724 HSDB
23268 TTY
24548 Launcher
3828 BiasedLock

通过java -cp “.;%JAVA_HOME%/lib/sa-jdi.jar” sun.jvm.hotspot.HSDB 3828 启动HSDB(这种方式会阻塞我们的程序,不要直接在生产环境这样操作)

第一次启动可能会报错误无法找到sawindbg.dll,这时需要将%JAVA_HOME%/lib目录下面的sawindbg.dll文件拷贝到jre的/lib目录下即可。

启动后,在界面选中main线程,点击工具栏第二个图片打开线程栈。

HSDB工具在线程栈中已经标出我们的对象。在菜单找到内存查看器



输入栈局部变量表中的对象的地址,就可以显示出对象的内存,和JOL工具打印的对象头部是一样的。

参考文档

  1. HSDB - HotSpot debugger
  2. JOL:分析Java对象的内存布局
  3. [Java JVM] Hotspot GC研究- 开篇&对象内存布局
  4. 看了这篇文章,我搞懂了StringTable
  5. 盘一盘 synchronized (一)—— 从打印Java对象头说起
  6. 浅谈偏向锁、轻量级锁、重量级锁
  7. 源码解析-线程A请求偏向于线程B的偏向锁
  8. C++为什么要弄出虚表这个东西?
  9. 《深入理解Java虚拟机》
  10. 《Java虚拟机规范(Java SE 8版)》



微信扫一扫二维码关注订阅号杰哥技术分享

出处:https://www.cnblogs.com/Jack-Blog/p/14481982.html

作者:杰哥很忙

本文使用「CC BY 4.0」创作共享协议。欢迎转载,请在明显位置给出出处及链接。

JVM-对象及对象内存布局的更多相关文章

  1. JVM 专题十二:运行时数据区(七)对象的实例化内存布局与访问定位

    1. 对象的实例化 1.1 创建对象的方式 new 最常见的方式 变形1 : Xxx的静态方法 变形2 : XxBuilder/XxoxFactory的静态方法 Class的newInstance() ...

  2. JVM运行时数据区--纵向补充--对象的实例化内存布局与访问定位

    对象的实例化 创建对象的方式 1.new:最常见的方式(本质是构造器) 变形1 : Xxx的静态方法 变形2 : XxBuilder/XxoxFactory的静态方法 2.Class的newInsta ...

  3. 浅谈Java虚拟机内存中的对象创建,内存布局,访问定位

    参考于 深入理解Java虚拟机 这里介绍HotSpot虚拟机(自带的虚拟机) 1.对象的创建 对于程序员来说,创建对象的方法: User user1 = new User(); User user2 ...

  4. 深入理解java虚拟机(二)HotSpot Java对象创建,内存布局以及访问方式

    内存中对象的创建.对象的结构以及访问方式. 一.对象的创建 在语言层面上,对象的创建只不过是一个new关键字而已,那么在虚拟机中又是一个怎样的过程呢? (一)判断类是否加载.虚拟机遇到一条new指令的 ...

  5. HotSpot Java对象创建,内存布局以及访问方式

    内存中对象的创建.对象的结构以及访问方式. 一.对象的创建 在语言层面上,对象的创建只不过是一个new关键字而已,那么在虚拟机中又是一个怎样的过程呢? (一)判断类是否加载.虚拟机遇到一条new指令的 ...

  6. 一个Objective-C对象如何进行内存布局?(考虑有父类的情况)

    1.对象isa指向类对象,类对象的isa指向元类.元类isa指向根元类.根元类的isa指针指向自己,superclass指针指向NSObject类 2.实例对象结构体只有一个isa变量,指向实例对象所 ...

  7. JVM学习记录1--JVM内存布局

    先上个图 这是根据<Java虚拟机规范(第二版)>所画的jvm内存模型. 程序计数器:程序计数器是用来记录当前线程方法执行顺序的,对应的就是我们编程中一行行代码的执行顺序,如分支,跳转,循 ...

  8. 重磅硬核 | 一文聊透对象在 JVM 中的内存布局,以及内存对齐和压缩指针的原理及应用

    欢迎关注公众号:bin的技术小屋 大家好,我是bin,又到了每周我们见面的时刻了,我的公众号在1月10号那天发布了第一篇文章<从内核角度看IO模型的演变>,在这篇文章中我们通过图解的方式以 ...

  9. 关于Java的对象,锁和对象的内存布局、访问定位

    1. 对象的创建和分配 创建对象(如克隆.反序列化)通常仅仅一个new关键字,但在虚拟机中,对象的创建的过程需要如下步骤: 类加载检查 先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并 ...

  10. 理解Java对象:要从内存布局及底层机制说起,话说….

    前言 大家好,又见面了,今天是JVM专题的第二篇文章,在上一篇文章中我们说了Java的类和对象在JVM中的存储方式,并使用HSDB进行佐证,没有看过上一篇文章的小伙伴可以点这里:<类和对象在JV ...

随机推荐

  1. Codeforces Round #547 (Div. 3) F1/2. Same Sum Blocks (Easy/Hard) (贪心,模拟)

    题意:有一长度为\(n\)的数组,求最多的区间和相同的不相交的区间的个数. 题解:我们可以先求一个前缀和,然后第一层循环遍历区间的右端点,第二层循环枚举左端点,用前缀和来\(O(1)\)求出区间和,\ ...

  2. python-零基础入门-自学笔记

    目录 第一章:计算机基础 1.1 硬件组成 1.2 操作系统分类 1.3 解释型和编译型介绍 第二章:Python入门 2.1 介绍 2.2 python涉及领域 2.2.1 哪些公司有使用Pytho ...

  3. 【.NET 与树莓派】PWM 调节LED小灯的亮度

    在开始本文内容之前,老周先纠正一个错误.在上一篇中,提到过 Arduino 开发板的 Vin 引脚,文中老周说这个供电口的输入电压不能高于 5.5V.这里有错,被卖家给的使用说明忽悠了,上 Ardui ...

  4. shapefile 输出的地理处理注意事项(转载)

    来源:http://resources.arcgis.com/zh-cn/help/main/10.1/index.html#//005600000013000000 多年来,Esri 为存储地理信息 ...

  5. Python 3的f-Strings:增强的字符串格式语法(指南)

    最近也在一个视频网站的爬虫,项目已经完成,中间有不少需要总结的经验. 从Python 3.6开始,f-Strings是格式化字符串的一种很棒的新方法.与其他格式化方式相比,它们不仅更具可读性,更简洁且 ...

  6. .NET并发编程-数据结构不可变性

    本系列学习在.NET中的并发并行编程模式,实战技巧 内容目录 .NET不可变集合.NET并发集合函数式数据结构设计一个不可变类 作为程序员经常遇到产品上线后出现各种莫名其妙的问题,在我本地是好好的啊, ...

  7. Storybook 最新教程

    Storybook 最新教程 Storybook is the most popular UI component development tool for React, Vue, and Angul ...

  8. npm clear folder

    npm clear folder rm -rf rimraf rmrf & clear build / dist folder caches https://www.npmjs.com/pac ...

  9. Prettier All In One

    Prettier All In One .prettierrc.js / .prettierrc / .prettierrc.json module.exports = { singleQuote: ...

  10. .NET & C# & ASP.NET

    .NET && C# && ASP.NET https://docs.microsoft.com/zh-cn/dotnet/ .NET Documentation We ...