logo头像
ICQL

jvm_内存管理

jvm内存管理采用的是 自动管理 的机制



运行时内存区域

1)程序计数器(Program Counter Register)

线程私有,每个线程都有一个程序计数器

(1)异常:
唯一一个不会抛出 OutOfMemoryError 的区域

(2)用途:
可看作是当前线程所执行的字节码的行号指示器
记录正在执行的虚拟机字节码指令的地址(如果执行的是 native 方法,计数器的值置为空)
分支、循环、异常、调整、线程恢复等都依赖此区域

2)栈(Stack)

线程私有,HotSpot将 本地方法栈 和 虚拟机栈 合二为一

(1)异常:
StackOverflowError
OutOfMemoryError

(2)用途:
虚拟机栈:描述的是 java 方法执行的内存模型
本地方法栈:描述的是 native 方法执行的内存模型

3)堆(Heap)

进程私有

(1)异常:
OutOfMemoryError

(2)用途:
主要用于存放java对象实例,GC管理的主要区域,不需要连续的内存

4)方法区(Method Area)

进程私有

(1)异常:
OutOfMemoryError

(2)用途:
主要用于存放类型元数据信息等,jdk1.8 使用 Metaspace(元空间)实现方法区

5)直接内存(Direct Memory)

不属于jvm管理的内存区域

(1)异常:
OutOfMemoryError

(2)用途:
nio有相关缓冲区放在此区域



堆中java对象的内存布局

1)创建对象

(1)首先检查 new 指令参数 类型 是否已被加载,若没有执行类型的加载
(2)jvm为对象在堆中分配内存(TLAB详细介绍参见垃圾回收篇)
(3)对象的实例字段初始化为0值
(4)对象头设置(Object Header)
(5)执行实例构造方法(编译后的字节码,已将字段赋值添加到每个构造方法的最前面)

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
//类Aston
public class Aston {

private int num = 100;

public static int s_num = 100;

public static final int s_f_num = 1000;

static {
System.out.println("类初始化");
}

public Aston() {
}

public Aston(int num) {
this.num = num;
}

public static void test(){
Best best = new Best();
}
}

//类Aston.class字节码,javap -v -p Aston.class
//可以看到 2 个构造方法的最前面(第5、7行)都加入了字段 num 的赋值操作
public work.icql.jvm.classload.Aston();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 100
7: putfield #2 // Field num:I
10: return
LineNumberTable:
line 15: 0
line 5: 4
line 16: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lwork/icql/jvm/classload/Aston;

public work.icql.jvm.classload.Aston(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 100
7: putfield #2 // Field num:I
10: aload_0
11: iload_1
12: putfield #2 // Field num:I
15: return
LineNumberTable:
line 18: 0
line 5: 4
line 19: 10
line 20: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lwork/icql/jvm/classload/Aston;
0 16 1 num I

2)对象内存布局(64位虚拟机)

(1)对象头

Mark Word: 8个字节,主要用于存放 hashcode,GC分代年龄、锁状态标志、线程持有锁 等等(具体分析查看 jc_并发关键字篇 synchronized)
Klass Word: 8个字节,指向类元数据的指针
数组长度:8个字节,普通java对象可以通过类型元数据确定所需内存大小,但是数组不行,必须要记录数据元素的长度

(2)实例数据

存储的数据: 所有的实例字段的数据(包括父类的实例字段)
存储的顺序:
优先存储父类字段,默认按照源码中定义的顺序存储
还受以下两点的影响:
(1)jvm默认分配按类型顺序排序:longs/doubles,ints,shorts/chars,bytes/booleans,oops(对象的指针)
(2)jvm参数 UseCompressedOops 指针压缩,CompactFields 字段间隙填充,默认都是 true,https://juejin.im/post/6844903768077647880
存储的大小:
(1)基本类型:数据类型的大小
(2)引用类型:引用64位jvm占用8个字节(开启指针压缩后,默认开启,占用4个字节),32位jvm占用4个字节

(3)对齐填充

不一定存在,起占位符作用
因为 jvm要求对象的大小必须时8字节的倍数
所以当前面的部分不够时,用以对齐

3)对象访问定位

1)直接访问:栈中存储实例对象的实例指针(hotspot采用此种方法)
2)间接访问:栈中存储实例对象的句柄的指针–实例对象的句柄存储实例对象的指针–实例对象存储类型指针

比较:
直接访问速度较快,只有 1 次定位,而间接访问需要 2 次定位
当垃圾回收时,实例对象的地址会改变,需要同步变更指向此实例对象的所有的栈中存储的指针
而间接访问则不需要,只需要修改句柄中的指针



方法区中的运行时常量池(元数据空间)

https://segmentfault.com/a/1190000018792105

1)类型常量池(每个类型都有一个)

类型加载后,每一个类型的元数据 InstanceKlass 都会引用着一个自己的 ConstantPool 类型的运行时常量池
基本上和 class字节码数据的静态常量池 保持一致,将静态常量池转化为运行时常量池
只不过,其中的符号引用会在运行过程中逐渐被解析为对应的直接引用(指针)

2)符号常量池(SymbolTable,全局共享)

HashTable结构,不会自动扩容

全局的 SymbolTable 管理着 每个类型元数据的常量池 里的 CONSTANT_Utf8_info 类型的常量

3)字符串常量池(StringTable,全局共享)

HashTable结构,不会自动扩容,会进行垃圾回收(具体分析查看垃圾回收篇)

String对象本身还是存储在堆中,StringTable字符串常量池中保存的是其引用
-XX:+PrintStringTableStatistics ,进程关闭时打印 SymbolTable 和 StringTable 统计值
-XX:StringTableSize ,设置 StringTable 桶的个数,jdk8默认值为 60013

运行时 字符串对象的引用 入池的时机:
1)当遇到 ldc 指令,参数为类型元数据常量池中的字符串时,就会将此字符串的引用(putIfAbsent)放入常量池中
2)String对象调用 intern() 方法时,就会将其引用(putIfAbsent)放入常量池中

微信打赏

赞赏是不耍流氓的鼓励