Java内存机制

内存原理

参数拷贝传值


基本类型是拷贝传值,内部拷贝间交换不会影响外部值

Sample
1
2
3
4
5
void swapNoChange(int v1,int v2){
int tmp = v1;
v1 = v2;
v2 = tmp;
}

引用也是传值,传的是引用的地址的拷贝,内部拷贝间交换同样不会影响外部值

Sample
1
2
3
4
5
void swapNoChange(Person p1,Person p2){
Person tmp = p1;
p1 = p2;
p2 = tmp;
}

修改对象内部时,外部对象真的改变了,因为指向的内存是同一位置,在函数内修改了内存,影响了外部

Sample
1
2
3
void set(Person p){
p.setName("foo");
}

对象底层结构


JVM底层C++实现oop-klass模型,即对象实例和元数据分离存储的二分模型

  • oop: Ordinary Object Pointer 对象指针,描述实例
  • klass: 元数据及方法信息,描述类

JVM运行时加载一个类时,会在JVM内部创建一个instanceKlass对象,表示这个类的运行时元数据
创建一个这个Class的实例时,会在JVM内部创建一个instanceOop来表示这个Java对象
instanceKlass对象放在了方法区/元空间,instanceOop放在了堆,instanceOop的引用放在了JVM栈

新对象创建过程

  1. 检查加载
  2. 分配内存
  3. 成员默认初始化
  4. 对象头填充

对象空间大小


Java的对象大小是平台无关的,不像C++。

基本类型大小

Java规定了每种类型的大小:

  • 8字节 double/long
  • 4字节 int/float
  • 2字节 short/char
  • 1字节 byte

除了布尔类型,在每种类型下都有一个SIZE常量表示占用比特位数。布尔类型在规范中没明确定义,取决于虚拟机实现,可能1字节也可能采取整型4字节。布尔型可采用int,布尔数组采用byte数组

Sample
1
2
3
4
5
6
7
System.out.println(Double.SIZE); // 64
System.out.println(Long.SIZE); // 64
System.out.println(Float.SIZE); // 32
System.out.println(Integer.SIZE); // 32
System.out.println(Short.SIZE); // 16
System.out.println(Character.SIZE); // 16
System.out.println(Byte.SIZE); // 8

引用大小

引用可以看成地址,跟寻址空间有关
32位JVM就是4字节,64位就是8字节

对象大小

没规定具体的内存结构,不同的JVM结构有所不同

https://www.pianshen.com/article/2382167638/

2~3个字宽存储对象头,字宽由32/64位决定

  • Mark Word:存储内容根据不同的状态而不同并非同时全部存在,比如无锁时包含哈希码,偏向锁时包含线程id,轻量级锁时包含锁记录指针,重量级锁时包含monitor指针等,GC标记
  • Class Meta Address:类元信息指针
  • Array Length: 数组专有

HotSpot为例

  • 对象头
    • 对象标记(Mark Word),32位下4字节,64位下8字节
    • 类元信息,类地址
  • 实例信息
    • 类成员,本类及所有可见父类成员
    • 数组对象还有额外4字节储存长度信息
  • 对象填充,以8字节为粒度对齐,这样不会跨缓存行

32位与64位小结

  • 32位处理器一次处理32位的指令,64位则一次处理64位,在相同工作频率下64位更高效
  • 32位寻址空间2的32次方最大4GB,64位则是上千万个TB,32位JVM堆大小理论上限4G,实际不能达到,不同操作系统不同,64位JVM堆理论大小非常大,通常只受限于物理机
  • 无论是32位还是64位,Java的基本类型大小都是固定的,比如int就是32位
  • 并非64位就比32位性能好,如果32位即可满足,移植到64位性能反而可能下降,因为64位提供了不必要的大空间

UseCompressedOops

在64位环境下,如果仍需要32位结构,可以开启压缩指针选项
纯32位环境下,寻址空间只有4G
开启压缩指针后,由于8字节对齐,隐含相当于末尾3个0,即全部地址物理上只32位,但是效果上可以35位,多出的3位使得寻址空间扩大8倍到32G
超过32G只能采用纯64位指针

实例

32位环境下的Integer对象,共16字节

  • MarkWord 4字节
  • Class类指针 4字节
  • 类成员(int value) 4字节
  • 对齐 4字节

32位环境下的Integer[3]数组对象

  • MarkWord 4字节
  • Class类指针 4字节
  • 长度(int length) 4字节
  • 数组元素3个引用 4字节*3
  • 对齐 0字节

测试


利用Agent机制获取Instrumentation

1
java -javaagent:test.jar -XX:-UseCompressedOops -jar test.jar

利用Instrumentation的getObjectSize方法获取对象大小

Sample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class SizeOfObject {
static Instrumentation inst;

public static void premain(String args, Instrumentation instP) {
inst = instP;
}
/**
* 直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、<br></br>
* 引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小;<br></br>
* 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小 <br></br>
*/
public static long sizeOf(Object obj) {
return inst.getObjectSize(obj);
}
/**
* 递归计算当前对象占用空间总大小,包括当前类和超类的实例字段大小以及实例字段引用对象大小
*/
public static long fullSizeOf(Object objP) throws IllegalAccessException {
Set<Object> visited = new HashSet<Object>();
Deque<Object> toBeQueue = new ArrayDeque<Object>();
toBeQueue.add(objP);
long size = 0L;
while (toBeQueue.size() > 0) {
Object obj = toBeQueue.poll();
//sizeOf的时候已经计基本类型和引用的长度,包括数组
size += skipObject(visited, obj) ? 0L : sizeOf(obj);
Class<?> tmpObjClass = obj.getClass();
if (tmpObjClass.isArray()) {
//[I , [F 基本类型名字长度是2
if (tmpObjClass.getName().length() > 2) {
for (int i = 0, len = Array.getLength(obj); i < len; i++) {
Object tmp = Array.get(obj, i);
if (tmp != null) {
//非基本类型需要深度遍历其对象
toBeQueue.add(Array.get(obj, i));
}
}
}
} else {
while (tmpObjClass != null) {
Field[] fields = tmpObjClass.getDeclaredFields();
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers()) //静态不计
|| field.getType().isPrimitive()) { //基本类型不重复计
continue;
}
field.setAccessible(true);
Object fieldValue = field.get(obj);
if (fieldValue == null) {
continue;
}
toBeQueue.add(fieldValue);
}
tmpObjClass = tmpObjClass.getSuperclass();
}
}
}
return size;
}

/**
* String.intern的对象不计;计算过的不计,也避免死循环
*/
static boolean skipObject(Set<Object> visited, Object obj) {
if (obj instanceof String && obj == ((String) obj).intern()) {
return true;
}
return visited.contains(obj);
}
}