JVM系列2-内存模型

栈和栈帧

上篇文章中类加载机制提到了java虚拟机栈以及栈帧. 每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。

栈帧数据

每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference tothe run-time constant pool)、方法返回地址(Return Address)和附加信息。
image

  • 局部变量表: 方法中定义的局部变量以及方法的参数存放在这张表中 局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。

  • 操作数栈: 以压栈和出栈的方式存储操作数的

  • 动态链接: 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态 连接(Dynamic Linking)。

  • 方法返回地址: 当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且 这个异常没有在方法体内得到处理。

Java虚拟机栈

image

运行时数据区

上篇文章中我们了解到运行时数据区被划分为了5大块:方法区、堆、Java虚拟机栈、本地方法栈、程序计数器。那这些区域是否能相互引用呢?

栈指向堆

Object obj=new Object(),

image

方法区指向堆

private static Object obj=new Object();

image

堆指向方法区

方法区中会包含类的信息,堆中会有对象,所有堆也会指向方法区找到对应类的信息

image

Java对象内存布局

image

一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充

  • 对象头

    1. Mark Word : 哈希码、分代年龄(垃圾回收时用来判断是否需要移动到老年代)、锁状态标志、持有锁的线程、偏向线程Id、偏向时间戳等

    2. Class Pointer : 指向对象对应的类的元数据在内存中的地址

    3. 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。

image

思考为什么要设计成这样

  1. 假设只有一块内存区域,内存不足触发GC后 内存区域会存在许多不连续的碎片空间。继续分配对象的时候空间是足够的但是由于不连续导致分配失败。

  2. 为了解决空间不连续的问题,将内存区域划分为老年代和新生代,垃圾回收的时候将新生代不连续的对象放入老年代,保证了新生代里面没有碎片空间。那什么时候数据才能放入老年代呢?这就要用到Java对象头里面的 分代年龄, 分代年龄达到15之后才放入老年代。但是只有一个新生代也会存在空间不连续的问题,导致发生GC的频率会变高,对象的分代年龄很快就会达到15而被放入老年代

  3. 为了解决频繁GC 的问题, 将新生代里面的区域划分为 Eden区 和 Survivor区,由于大多数对象的生命周期都比较短,将Eden区对象放入Survivor区,保证了Eden区内存相对连续,而不会因为少量的存活对象产生空间碎片

  4. 但是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”区被填满之后,会将所有对象移动到年老代中。

老年代什么时候有数据

  1. 分配的对象特别大 新生代发不下
    如果设置了 -XX:PretenureSizeThreshold 这个参数,那么如果你要创建的对象大于这个参数的值,比如分配一个超大的字节数组,此时就直接把这个大对象放入到老年代,不会经过新生代。
    这么做就可以避免大对象在新生代,屡次躲过GC,还得把他们来复制来复制去的,最后才进入老年代,这么大的对象来回复制,是很耗费时间的。

  2. 新生代数据分代年龄达到15
    一次Minor GC后,对象年龄就会+1,达到阈值的对象就移动到老年代,其他存活下来的对象会继续保留在新生代中

  3. 动态年龄判断
    根据对象年龄有另外一个策略也会让对象进入老年代,不用等待15次GC之后进入老年代,他的大致规则就是,假如当前放对象的Survivor,一批对象的总大小大于这块Survivor内存的50%,那么大于这批对象年龄的对象,就可以直接进入老年代了。

对象申请分配空间流程

image

垃圾回收类型

Minor GC Major GC Full GC
新生代垃圾回收 老年代垃圾回收 新生代 + 老年代垃圾回收

发生Old区的垃圾回收时都伴随着Minor GC,所有Major GC 也可以当作 Full GC

查看垃圾回收信息

使用jvisualvm查看,具体使用下篇文章介绍。

posted @ 2021-09-16 20:27  狻猊的主人  阅读(66)  评论(0编辑  收藏  举报