咱们编写一个Java
类,编译后会生成.class
文件,当类加载器将class
文件加载到jvm
时,会生成一个Klass
类型的对象(c++
),称为类描述元数据,存储在方法区中,即jdk1.8
以后的元数据区。当使用new
建立对象时,就是根据类描述元数据Klass
建立的对象oop
,存储在堆中。每一个java
对象都有相同的组成部分,称为对象头。java
在学习并发编程知识synchronized
时,咱们老是难以理解其实现原理,由于偏向锁、轻量级锁、重量级锁都涉及到对象头,因此了解java
对象头是咱们深刻了解synchronized
的前提条件。ios
介绍一款能够在代码中计算java
对象的大小以及查看java
对象内存布局的工具包:jol-core
,jol
为java object layout
的缩写,即java对象布局。使用只须要到maven
仓库http://mvnrepository.com
搜索java object layout
,选择想要使用的版本,将依赖添加到项目中便可。c++
使用jol
计算对象的大小(单位为字节):编程
ClassLayout.parseInstance(obj).instanceSize()
复制代码
使用jol
查看对象的内存布局:数组
ClassLayout.parseInstance(obj).toPrintable()
复制代码
网络搜索了不少资料,对64
位jvm
的Java
对象头的布局讲解的都很模糊,不少资料都是讲的32
位,并且不少都是从一些书上摘抄下来的,不免会存在错误的地方,因此最好的学习方法就是本身去验证,看jvm
源码。本篇将详细介绍64
位jvm
的Java
对象头。bash
以User
类为例网络
public class User {
private String name;
private Integer age;
private boolean sex;
}
复制代码
经过jol
查看User
对象的内存布局并发
User user = new User()
System.out.println(ClassLayout.parseInstance(user).toPrintable());
复制代码
输出内容以下 jvm
object header
为对象头;从图中能够看到,对象头所占用的内存大小为16*8bit=128bit
。若是你们本身动手去打印输出,可能获得的结果是96bit
,这是由于我关闭了指针压缩。jdk8
版本是默认开启指针压缩的,能够经过配置vm
参数关闭指针压缩。maven
-XX:-UseCompressedOops
复制代码
如今取消关闭指针压缩的配置,开启指针压缩以后,再看User
对象的内存布局。
开启指针压缩能够减小对象的内存使用。从两次打印的User
对象布局信息来看,关闭指针压缩时,name
字段和age
字段因为是引用类型,所以分别占8
个字节,而开启指针压缩以后,这两个字段只分别占用4
个字节。所以,开启指针压缩,理论上来说,大约能节省百分之五十的内存。jdk8
及之后版本已经默认开启指针压缩,无需配置。
从两次打印的User
对象的内存布局,还能够看出,bool
类型的age
字段只占用1
个字节,但后面会跟随几个字节的浪费,即内存对齐。开启指针压缩状况下,age
字段的内存对齐须要3
个字节,而关闭指针压缩状况下,则须要7
个字节。
以默认开启指针压缩状况下的User
对象的内存布局来看,对象头占用12个字节
,那么这12
个字节存储的是什么信息,咱们不看网上的资料,而是看jdk
的源码。
我当前使用的jdk
版本是jdk1.8
,可经过命令行java -version
查看,也可经过下面方式查看,这个你们应该都很熟悉了。
System.out.println(System.getProperties());
复制代码
打开官网后点击左侧菜单栏的Groups
找到HotSpot
,在打开的页面的Source code
选择Browsable souce
,以后会跳转到hg.openjdk.java.net/,选择jdk8u
,跳转后的页面中继续选择jdk8u
下面的hotspot
。传送连接:hg.openjdk.java.net/jdk8u/jdk8u…。
zip
可将源码打包下载;browse
可在线查看源码;在开启指针压缩的状况下,User
对象的对象头占用12
个字节,本节咱们经过源码了解对象头都存储了哪些信息。
在Java
程序运行的过程当中,每建立一个新的对象,JVM
就会相应地建立一个对应类型的oop
对象,存储在堆中。如new User()
,则会建立一个instanceOopDesc
,基类为oopDesc
。
[instanceOop.hpp文件:hotspot/src/share/vm/oops/instanceOop.hpp]
class instanceOopDesc : public oopDesc {
}
复制代码
instanceOopDesc
只提供了几个静态方法,如获取对象头大小。所以重点看其父类oopDesc
。
[oop.hpp文件:hotspot/src/share/vm/oops/oop.hpp]
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
........
}
复制代码
咱们只关心对象头,普通对象(如User
对象,本篇不讲数组类型)的对象头由一个markOop
和一个联合体组成,markOop
就是MarkWord
。这个联合体是指向类的元数据指针,未开启指针压缩时使用_klass
,开启指针压缩时使用_compressed_klass
。
markOop
与narrowKlass
的类型定义在/hotspot/src/share/vm/oops/oopsHierarchy.hpp
头文件中:
[oopsHierarchy.hpp头文件:/hotspot/src/share/vm/oops/oopsHierarchy.hpp]
typedef juint narrowKlass;
typedef class markOopDesc* markOop;
复制代码
所以,narrowKlass
是一个juint
,junit
是在globalDefinitions_visCPP.hpp
头文件中定义的,这是一个无符号整数,即4
个字节。因此开启指针压缩以后,指向Klass
对象的指针大小为4
字节。
[/hotspot/src/share/vm/utilties/globalDefinitions_visCPP.hpp]
typedef unsigned int juint;
复制代码
而markOop
则是markOopDesc
类型指针,markOopDesc
就是MarkWord
。不知道大家有没有感受到奇怪,在64
位jvm
中,markOopDesc
指针是8
字节,即64bit
,确实恰好是MarkWord
的大小,可是指针指向的不是一个对象吗?咱们先看markOopDesc
类。
[markOop.hpp文件:hotspot/src/share/vm/oops/markOop.hpp]
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
class markOopDesc: public oopDesc {
......
}
复制代码
markOop.hpp
头文件中给出了64bit
的MarkWord
存储的信息说明。markOopDesc
类也继承oopDesc
。若是单纯的看markOopDesc
类的源码,根本找不出来,markOopDesc
是用那个字段存储MarkWord
的。并且,根据从各类来源的资料中,咱们所知道的是,对象头的前8
个字节存储的就是是否偏向锁、轻量级锁等等信息(全文都是以64
位为例),因此不该该是个指针啊。
为了解答这个疑惑,我是先从markOopDesc
类的源码中,找一个方法,好比,获取gc
对象年龄的方法,看下jvm
是从哪里获取的数据。
class markOopDesc: public oopDesc {
public:
// 获取对象年龄
uint age() const {
return mask_bits(value() >> age_shift, age_mask);
}
// 更新对象年龄
markOop set_age(uint v) const {
return markOop((value() & ~age_mask_in_place) | (((uintptr_t)v & age_mask) << age_shift));
}
// 自增对象年龄
markOop incr_age() const {
return age() == max_age ? markOop(this) : set_age(age() + 1);
}
}
复制代码
那么,value()
这个方法返回的就是64bit
的MarkWord
了。
class markOopDesc: public oopDesc {
private:
// Conversion
uintptr_t value() const { return (uintptr_t) this; }
}
复制代码
value
方法返回的是一个指针,就是this
。从set_age
和incr_age
方法中也能够看出,只要修改MarkWord
,就会返回一个新的markOop
(markOopDesc*
)。难怪会将markOopDesc*
定义为markOop
,就是将markOopDesc*
当成一个8
字节的整数来使用。想要理解这个,咱们须要先补充点c++
知识,所以我写了个demo
。
自定义一个类叫oopDesc
,而且除构造函数和析构函数以外,只提供一个Show
方法。
[.hpp文件]
#ifndef oopDesc_hpp
#define oopDesc_hpp
#include <stdio.h>
#include <iostream>
using namespace std;
// 将oopDesc* 定义为 oop
typedef class oopDesc* oop;
class oopDesc{
public:
void Show();
};
#endif /* oopDesc_hpp */
[.cpp文件]
#include "oopDesc.hpp"
void oopDesc::Show(){
cout << "oopDesc by wujiuye" <<endl;
}
复制代码
使用oop(指针)
建立一个oopDesc*
,并调用show
方法。
#include <iostream>
#include "oopDesc.hpp"
using namespace std;
int main(int argc, const char * argv[]) {
//
oopDesc* o = oop(0x200);
cout << o << endl;
o->Show();
//
oopDesc* o1;
o1->Show();
return 0;
}
复制代码
测试输出
0x200
oopDesc by wujiuye
oopDesc by wujiuye
Program ended with exit code: 0
复制代码
所以,经过类名(value)
能够建立一个野指针对象,将指针赋值为value
,这样就可使用this
做为MarkWord
了。若是在oopDesc
中添加一个字段,并提供一个方法访问,程序运行就会报错,所以,这样建立的对象只能调用方法,不能访问字段。
锁状态/gc | Mark Word (64 bits) | - | - | - | 是否偏向锁 | 锁标志位 |
---|---|---|---|---|---|---|
无锁 | unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |
偏向锁 | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 |
轻量级锁 | ptr_to_lock_record:62 | - | - | - | - | lock:2 |
重量级锁 | ptr_to_heavyweight_monitor:62 | - | - | - | - | lock:2 |
gc标志 | - | - | - | - | - | lock:2 |
经过倒数三位判断当前MarkWord
的状态,就能够判断出其他位存储的是什么。
[markOop.hpp文件]
enum { locked_value = 0, // 0 00 轻量级锁
unlocked_value = 1,// 0 01 无锁
monitor_value = 2,// 0 10 重量级锁
marked_value = 3,// 0 11 gc标志
biased_lock_pattern = 5 // 1 01 偏向锁
};
复制代码
如今,咱们再看下User
对象打印的内存布局。
64
位是
MarkWord
,后
32
位是类的元数据指针(开启指针压缩)。
从图中能够看出,在无锁状态下,该User
对象的hashcode
为0x7a46a697
。因为MarkWord
实际上是一个指针,在64
位jvm
下占8
字节。所以MarkWordk
是0x0000007a46a69701
,跟你从图中看到的正好相反。这里涉及到一个知识点“大端存储与小端存储”。
学过汇编语言的朋友,这个知识点应该都还记得。本篇不详细介绍,不是很明白的朋友能够网上找下资料看。
接着,咱们再看一下,使用synchronized
加锁状况下的User
对象的内存信息,经过对象头分析锁状态。
public class MarkwordMain {
private static final String SPLITE_STR = "===========================================";
private static User USER = new User();
private static void printf() {
System.out.println(SPLITE_STR);
System.out.println(ClassLayout.parseInstance(USER).toPrintable());
System.out.println(SPLITE_STR);
}
private static Runnable RUNNABLE = () -> {
while (!Thread.interrupted()) {
synchronized (USER) {
printf();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(RUNNABLE).start();
Thread.sleep(1000);
}
Thread.sleep(Integer.MAX_VALUE);
}
}
复制代码
从该对象头中分析加锁信息,MarkWordk
为0x0000700009b96910
,二进制为0xb00000000 00000000 01110000 00000000 00001001 10111001 01101001 00010000
。
倒数第三位为"0"
,说明不是偏向锁状态,倒数两位为"00"
,所以,是轻量级锁状态,那么前面62
位就是指向栈中锁记录的指针。
public class MarkwordMain {
private static final String SPLITE_STR = "===========================================";
private static User USER = new User();
private static void printf() {
System.out.println(SPLITE_STR);
System.out.println(ClassLayout.parseInstance(USER).toPrintable());
System.out.println(SPLITE_STR);
}
private static Runnable RUNNABLE = () -> {
while (!Thread.interrupted()) {
synchronized (USER) {
printf();
}
}
};
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(RUNNABLE).start();
}
Thread.sleep(Integer.MAX_VALUE);
}
}
复制代码
从该对象头中分析加锁信息,MarkWordk
为0x0000700009b96910
,二进制为0xb00000000 00000000 01111111 11110000 11001000 00000000 01010011 11101010
。
倒数第三位为"0"
,说明不是偏向锁状态,倒数两位为"10"
,所以,是重量级锁状态,那么前面62
位就是指向互斥量的指针。