- java对象详解
- 内存布局
- 普通对象布局
- 数组的内存布局
- 内部类的内存布局
- 对象分解
- 对象头-mark word(8字节)
- 实例数据
- 对齐填充(可选)
- java锁分析
- 内存布局
java对象详解
HotSpot虚拟机中,对象在内存中存储的布局可以分为
对象头
,实例数据
,对齐填充
三个区域。本文所说环境均为HotSpot虚拟机。即输入java -version
返回的虚拟机版本:
java version "1.8.0_111"
Java(TM) SE Runtime Environment (build 1.8.0_111-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.111-b14, mixed mode)
内存布局
普通对象布局
- 在jvm中,任何对象都是8个字节为粒度进行对齐的,这是对象内存布局的第一个规则。
如果调用new Object(),由于Object类并没有其他没有其他可存储的成员,那么仅仅使用堆中的8个字节来保存两个字的头部即可。
除了上面所说的8个字节的头部(关于对象头,在下面会有详细解释),类属性紧随其后。属性通常根据其大小来排列。例如,整型(int)以4个字节为单位对齐,长整型(long)以8个字节为单位对齐。这里是出于性能考虑而这么设计的:通常情况下,如果数据以4字节为单位对齐,那么从内存中读4字节的数据并写入到处理器的4字节寄存器是性价比更高的。
为了节省内存,Sun VM并没有按照属性声明时的顺序来进行内存布局。实际上,属性在内存中按照下面的顺序来组织:
-
- 双精度型(doubles)和长整型(longs)
-
- 整型(ints)和浮点型(floats)
-
- 短整型(shorts)和字符型(chars)
-
- 布尔型(booleans)和字节型(bytes)
-
- 引用类型(references)
内存使用率会通过这个机制得到优化。例如,如下声明一个类:
class MyClass {
byte a;
int c;
boolean d;
long e;
Object f;
}
如果JVM并没有打乱属性的声明顺序,其对象内存布局将会是下面这个样子:
[HEADER: 8 bytes] 8
[a: 1 byte ] 9
[padding: 3 bytes] 12
[c: 4 bytes] 16
[d: 1 byte ] 17
[padding: 7 bytes] 24
[e: 8 bytes] 32
[f: 4 bytes] 36
[padding: 4 bytes] 40
此时,用于占位的14个字节是浪费的,这个对象一共使用了40个字节的内存空间。但是,如果用上面的规则对这些对象重新排序,其内存结果会变成下面这个样子:
[HEADER: 8 bytes] 8
[e: 8 bytes] 16
[c: 4 bytes] 20
[a: 1 byte ] 21
[d: 1 byte ] 22
[padding: 2 bytes] 24
[f: 4 bytes] 28
[padding: 4 bytes] 32
这次,用于占位的只有6个字节,这个对象使用了32个字节的内存空间。
- 规则2:类属性按照如下优先级进行排列:长整型和双精度类型;整型和浮点型;字符和短整型;字节类型和布尔类型,最后是引用类型。这些属性都按照各自的单位对齐。
现在我们知道如何计算一个继承了Object的类的实例的内存大小了。下面这个例子用来做下练习: java.lang.Boolean。这是其内存布局:
[HEADER: 8 bytes] 8
[value: 1 byte ] 9
[padding: 7 bytes] 16
Boolean类的实例占用16个字节的内存!惊讶吧?(别忘了最后用来占位的7个字节)。
- 规则3:不同类继承关系中的成员不能混合排列。首先按照规则2处理父类中的成员,接着才是子类的成员。
举例如下:
class A {
long a;
int b;
int c;
}
class B extends A {
long d;
}
类B的实例在内存中的存储如下:
[HEADER: 8 bytes] 8
[a: 8 bytes] 16
[b: 4 bytes] 20
[c: 4 bytes] 24
[d: 8 bytes] 32
如果父类中的成员的大小无法满足4个字节这个基本单位,那么下一条规则就会起作用:
- 规则4:当父类中最后一个成员和子类第一个成员的间隔如果不够4个字节的话,就必须扩展到4个字节的基本单位。
class A {
byte a;
}
class B {
byte b;
}
[HEADER: 8 bytes] 8
[a: 1 byte ] 9
[padding: 3 bytes] 12
[b: 1 byte ] 13
[padding: 3 bytes] 16
注意到成员a被扩充了3个字节以保证和成员b之间的间隔是4个字节。这个空间不能被类B使用,因此被浪费了。
- 规则5:如果子类第一个成员是一个双精度或者长整型,并且父类并没有用完8个字节,JVM会破坏规则2,按照整形(int),短整型(short),字节型(byte),引用类型(reference)的顺序,向未填满的空间填充。
class A {
byte a;
}
class B {
long b;
short c;
byte d;
}
[HEADER: 8 bytes] 8
[a: 1 byte ] 9
[padding: 3 bytes] 12
[c: 2 bytes] 14
[d: 1 byte ] 15
[padding: 1 byte ] 16
[b: 8 bytes] 24
在第12字节处,类A“结束”的地方,JVM没有遵守规则2,而是在长整型之前插入一个短整型和一个字节型成员,这样可以避免浪费3个字节。
数组的内存布局
数组有一个额外的头部成员,用来存放“长度”变量。数组元素以及数组本身,跟其他常规对象同样,都需要遵守8个字节的边界规则。
下面是一个有3个元素的字节数组的内存布局:
[HEADER: 12 bytes] 12
[[0]: 1 byte ] 13
[[1]: 1 byte ] 14
[[2]: 1 byte ] 15
[padding: 1 byte ] 16
下面是一个有3个元素的长整型数字的内存布局:
[HEADER: 12 bytes] 12
[padding: 4 bytes] 16
[[0]: 8 bytes] 24
[[1]: 8 bytes] 32
[[2]: 8 bytes] 40
内部类的内存布局
非静态内部类(Non-static inner classes)有一个额外的“隐藏”成员,这个成员是一个指向外部类的引用变量。这个成员是一个普通引用,因此遵守引用内存布局的规则。内部类因此有4个字节的额外开销。
对象分解
对象头-mark word(8字节)
对象头主要包含两部分信息,第一部分用于存储对象自身运行时数据,如哈希码,GC分代年龄(可以查看上一篇关于java内存回收分析的文章),锁状态标志,线程持有锁,偏向线程ID,偏向时间戳等。
如果对象是数组类型,则虚拟机用3个字宽存储对象头,如果是非数组类型,则用2个字宽存储对象头。下图是一个32位虚拟机mark部分占用内存分布情况
j1.jpeg
此图来源:http://blog.csdn.net/zhoufanyang_china/article/details/54601311
另一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,具体结构参考下图。
javao.png
在32位系统下,存放Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节。
在64位系统下,存放Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节。
64位开启指针压缩的情况下,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节。
数组长度4字节+数组对象头8字节(对象引用4字节(未开启指针压缩的64位为8字节)+数组markword为4字节(64位未开启指针压缩的为8字节))+对齐4=16字节。
静态属性不算在对象大小内。
实例数据
对象实际数据,大下为实际数据的大小
对齐填充(可选)
按8字节对齐,参照上面内存布局部分
java锁分析
synchronized到底锁的是对象还是代码片段?
例:
package com.startclan.thread;
/**
* Created by wongloong on 17-5-20.
*/
public class TestSync {
public synchronized void test() {
System.out.println("test1 start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test1 end");
}
public synchronized void test2() {
System.out.println("test2 start");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test2 end");
}
}
package com.startclan.thread;
/**
* Created by wongloong on 17-5-20.
*/
public class TestSyncStatic {
public static synchronized void test() {
System.out.println("test1 start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test1 end");
}
public static synchronized void test2() {
System.out.println("test2 start");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test2 end");
}
}
package com.startclan;
import com.startclan.thread.TestSync;
import com.startclan.thread.TestSyncStatic;
import org.junit.Test;
/**
* Created by wongloong on 17-5-18.
*/
public class TestWithThread {
@Test
public void testThread1() throws Exception {
final TestSync t1 = new TestSync();
/**
* 测试synchronized同步非static代码块
* 此处会先执行test方法然后执行test2方法,说明synchronized在同步非static方法时,
* 只能同步同一对象的同一实例进行同步
*/
new Thread(new Runnable() {
@Override
public void run() {
t1.test();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t1.test2();
}
}).start();
Thread.sleep(4000);
}
@Test
public void testThread2() throws Exception {
final TestSync t1 = new TestSync();
final TestSync t2 = new TestSync();
/**
* 测试synchronized同步非static代码块
* t1 t2不同对象,
* 不能同步方法
*/
new Thread(new Runnable() {
@Override
public void run() {
t1.test();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t2.test2();
}
}).start();
Thread.sleep(4000);
}
@Test
public void testThread3() throws Exception {
final TestSyncStatic tss1 = new TestSyncStatic();
final TestSyncStatic tss2 = new TestSyncStatic();
/**
* 测试synchronized 同步 static代码块
* 由于method1和method2都属于静态同步方法,
* 所以调用的时候需要获取同一个类上monitor(每个类只对应一个class对象),
* 所以也只能顺序的执行。
*/
new Thread(new Runnable() {
@Override
public void run() {
tss1.test();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
tss2.test2();
}
}).start();
Thread.sleep(4000);
}
}
此时输出结果为:
------------------------1-------------------------
test1 start
test1 end
test2 start
test2 end
-----------------------2-------------------------
test1 start
test2 start
test2 end
test1 end
-----------------------3-------------------------
test1 start
test1 end
test2 start
test2 end
结论:
- synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个实例的同步代码段(在第一段测试代码中,分别new了三个Mythread类,所以并不会执行同步)
- synchronized(xx.class)及static的synchronized方法,可以防止多个线程同时执行同一个对象的多个实例同步的代码段
- synchronize原理
每一个对象头信息都包含一个锁定状态,可以看上面的mark word的图解。当线程进入对象中,尝试获取锁的所有权,如果为锁的值为0,则该线程进入,并设置为1,该线程为锁的拥有者。如果线程已经占用该锁,只是重新进入,并且锁值+1.当线程退出时则-1.如果其他线程访问这个对象实例,则改线程堵塞。直到锁值为0的时候,在重新尝试取得锁的所有权。