JVM系列2-内存模型
栈和栈帧
上篇文章中类加载机制提到了java虚拟机栈以及栈帧. 每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。
栈帧数据
每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference tothe run-time constant pool)、方法返回地址(Return Address)和附加信息。
-
局部变量表
: 方法中定义的局部变量以及方法的参数存放在这张表中 局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。 -
操作数栈
: 以压栈和出栈的方式存储操作数的 -
动态链接
: 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态 连接(Dynamic Linking)。 -
方法返回地址
: 当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且 这个异常没有在方法体内得到处理。
Java虚拟机栈
运行时数据区
上篇文章中我们了解到运行时数据区被划分为了5大块:方法区、堆、Java虚拟机栈、本地方法栈、程序计数器。那这些区域是否能相互引用呢?
栈指向堆
Object obj=new Object(),
方法区指向堆
private static Object obj=new Object();
堆指向方法区
方法区中会包含类的信息,堆中会有对象,所有堆也会指向方法区找到对应类的信息
Java对象内存布局
一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充
-
对象头
-
Mark Word : 哈希码、分代年龄(垃圾回收时用来判断是否需要移动到老年代)、锁状态标志、持有锁的线程、偏向线程Id、偏向时间戳等
-
Class Pointer : 指向对象对应的类的元数据在内存中的地址
-
Length :数组对象特有的,表示数组长度
-
-
实例数据
包含了对象的所有成员变量,大小由各个变量决定
boolean | byte | short | char | int | float | long | double | refence |
---|---|---|---|---|---|---|---|---|
1字节 | 1字节 | 2字节 | 2字节 | 4字节 | 4字节 | 8字节 | 8字节 | 8字节 |
- 对齐填充
保证对象的长度是8字节的倍数
内存模型
Java堆内存模型分为两个区域 一个是Old区(老年代),一个是Young区(新生代)。
Young区分为两大块,一个是Survivor区(存活区,大小为:S0+S1),一块是Eden区(伊甸区)。 初始比例为:Eden:S0:S1=8:1:1;S0和S1一样大,也可以叫From和To。
思考为什么要设计成这样
-
假设只有一块内存区域,内存不足触发GC后 内存区域会存在许多不连续的碎片空间。继续分配对象的时候空间是足够的但是由于不连续导致分配失败。
-
为了
解决空间不连续
的问题,将内存区域划分为老年代和新生代,垃圾回收的时候将新生代不连续的对象放入老年代,保证了新生代里面没有碎片空间。那什么时候数据才能放入老年代呢?这就要用到Java对象头里面的分代年龄
, 分代年龄达到15之后才放入老年代。但是只有一个新生代也会存在空间不连续的问题,导致发生GC的频率会变高,对象的分代年龄很快就会达到15而被放入老年代 -
为了
解决频繁GC
的问题, 将新生代里面的区域划分为 Eden区 和 Survivor区,由于大多数对象的生命周期都比较短,将Eden区对象放入Survivor区,保证了Eden区内存相对连续,而不会因为少量的存活对象产生空间碎片 -
但是Survivor区 也会有空间碎片的问题,于是将 Survivor区划分为From 、To两块区域。在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过
-XX:MaxTenuringThreshold
来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
老年代什么时候有数据
-
分配的对象特别大 新生代发不下
如果设置了-XX:PretenureSizeThreshold
这个参数,那么如果你要创建的对象大于这个参数的值,比如分配一个超大的字节数组,此时就直接把这个大对象放入到老年代,不会经过新生代。
这么做就可以避免大对象在新生代,屡次躲过GC,还得把他们来复制来复制去的,最后才进入老年代,这么大的对象来回复制,是很耗费时间的。 -
新生代数据分代年龄达到15
一次Minor GC后,对象年龄就会+1,达到阈值的对象就移动到老年代,其他存活下来的对象会继续保留在新生代中 -
动态年龄判断
根据对象年龄有另外一个策略也会让对象进入老年代,不用等待15次GC之后进入老年代,他的大致规则就是,假如当前放对象的Survivor,一批对象的总大小大于这块Survivor内存的50%,那么大于这批对象年龄的对象,就可以直接进入老年代了。
对象申请分配空间流程
垃圾回收类型
Minor GC | Major GC | Full GC |
---|---|---|
新生代垃圾回收 | 老年代垃圾回收 | 新生代 + 老年代垃圾回收 |
发生Old区的垃圾回收时都伴随着Minor GC,所有Major GC 也可以当作 Full GC
查看垃圾回收信息
使用jvisualvm查看,具体使用下篇文章介绍。