欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

02-JVM虚拟机栈及内存模型

程序员文章站 2022-06-07 09:56:01
...

JVM虚拟机栈及内存模型

1、虚拟机栈

虚拟机栈就是一个线程里方法链执行的入栈出栈的一个过程,一个方法的调用对应一个栈帧。

栈帧中有些什么?

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

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

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

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

  • 看一段源码
class Person{
  private String name="Jack";
  private int age;
  private final double salary=100;
  private static String address;
  private final static String hobby="Programming";
  public void say(){
    System.out.println("person say..."); }
  public static int calc(int op1,int op2){
    op1=3;
    int result=op1+op2+1;
    return result;
  }
  public static void order(){
  }
  public static void main(String[] args){
    calc(1,2);
    order(); 
  }
}
  • 编译的字节码指令
Compiled from "Person.java" 
class Person {
	...
  public static int calc(int, int);
  	Code:
  	0: iconst_3		//将int类型常量3压入[操作数栈]
		1: istore_0		//将int类型值存入[局部变量0]
		2: iload_0		//从[局部变量0]中装载int类型值入栈
		3: iload_1		//从[局部变量1]中装载int类型值入栈
		4: iadd				//将栈顶元素弹出栈,执行int类型的加法,结果入栈
		...
		5: istore_2		//将栈顶int类型值保存到[局部变量2]中
		6: iload_2		//从[局部变量2]中装载int类型值入栈
		7: ireturn		//从方法中返回int类型的数据
}
  • 局部变量表

存放方法参数和方法内的局部变量,在编译期确定要分配的容量

局部变量表中存储的就是 int op1,int op2,int result 这三个局部变量

  • 操作数栈

方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入提取的操作就是入栈出栈的过程。

由字节码指令操作的,存放方法的操作数,例如iconst_3就是将int类型常量压入操作数栈,通过istore_0将操作栈元素3弹出并存入局部变量表0(opt1)

  • 方法出口

主函数压栈,calc压栈,代码层面需要继续执行order。

在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行。

一般来说,正常退出时,调用者的pc计数器可以作为返回地址,栈帧中会保存这个计数器值。而异常退出时,返回地址是异常处理器来确定,栈帧中不会保存这个地址

方法退出过程实际等同于把前栈帧出栈,恢复上层局部变量表和操作数栈,如果有返回值,则吧他压入调用者的操作数栈,调整pc计数器的值以指向后面一条指令,让后面方法入栈。

  • 动态链接

符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。

每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用就是为了支持方法调用过程中的动态链接;class文件的常量池存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号一部分会在类加载阶段或者第一次使用的时候转化为直接引用(静态解析)。另一部分将再每一次运行期间转化为直接引用,这部分称为动态链接。

  • 栈连的元素指向堆

Object obj = new Object();

方法里的局部变量表中的obj指向堆内存中的具体地址

  • 方法区元素指向堆

private static Object obj = new Object();静态成员变量存储在方法区,这个obj指向的就是堆

普通成员变量是随着对象存储在堆内存中
  • 堆指向方法区
对象头信息里class pointer记录了指向对象对应的类元数据的内存地址。

2、Java对象内存布局

02-JVM虚拟机栈及内存模型

Java对象的内存布局里包括三个部分
1、对象头
MarkWord:存储的哈希码,分代年龄、锁状态标志等(64位系统里,这个部分占8字节)
Class Pointer:指向对象对应的类元数据的内存地址(64位系统里,这个部分占8字节)
数组长度:如果是数组对象,这个部分将会记录数组的长度(占4字节)
2、实例数据
boolean:1字节
short or char:1字节
int or float:4字节
long or double:8字节
reference:8字节(64位系统)
3、对齐填充
这个部分是为了保证对象的大小为8字节的整数倍

3、内存模型

02-JVM虚拟机栈及内存模型

JVM的内存主要分为堆(heap)和非堆(no-heap,即方法区(metaspace))

(1)方法区(metaspace)

存储的是类的信息、静态变量、常量、即时编译后的代码

(2)堆

存储对象实例,分为两大部分Young区(新生代)和Old区(老年代)

a)、Young区
  • Eden区

一般情况下,新创建的对象会被分配到Eden区,一些特殊的大对象在Eden区存不下的情况会被直接分配到Old区

如果一直在Eden区分配对象,如果达到Eden区的最大容量,就会触发Minor GC,对一些垃圾对象进行清理回收。如果Eden区的对象经过一次垃圾回收仍然存活将会进入Survivor区,然后清空Eden区,这样做的目的就是让Eden的空间变得相对连续。

  • Survivor区

Survivor区分为两个部分S0和S1(也叫From、To)

在一个时间节点S0和S1只有一个区域有数据

在一次GC操作之后,Eden区中幸存的对象将会年龄+1并且这些存活对象进入S0区;当下一次GC的时候Eden区存活对象和S0区的对象年龄+1,并且Eden区中存活对象复制到S1区,S0中的对象也将复制到S1中,并且清空Eden区和S0;这样减少了内存碎片的产生,让Eden区相对连续。

当对象达到我们设置的年龄阈值(默认15)这个对象在GC回收的时候将会进入到Old区

Minor GC会一直重复这样的过程,直到To区被填满,然后会将所有对象复制到老年代中

默认的比例是 8:1:1

为什么需要Survivor?

主要是为了提高对象在Young区的停留时间,如果不存在S区,对象经过一次MinorGC就会进入到老年代,这样老年代很快就会被填满并且触发Major GC(因为MajorGC一般伴随着MinorGC,可以看做发生了FullGC).进行一次FullGC消耗的时间比MinorGC时间长很多

如果单纯的通过增加老年代的大小来解决FullGC发生的频率,但是随着Old区的变大,每次FullGC的时间也会变的更长。如果减小OLD区大小,则会增加FullGC频率。

所以S区存在的意义就是减少每次MinorGC后送到老年代的对象的数量,,进而减少FullGC的发生,按照默认的设置,一个对象最少要经过16次的MinorGC才会被送到老年代。

为什么需要两个Survivor区?

主要是解决了内存碎片的问题,如果只有一个S区,经过很多次MinorGC后,当下一次Eden区满的时候,经过了很多次GC后,这个S区的内存空间也变得不连续了,可能导致Eden区中的对象无法进入到S区(即使S区的理论大小大于Eden区中的存活对象的代大小)

所以有两个S区的时候,永远保证一个S区是空的无碎片的。

  • 分配担保机制

当新对象分配在Eden区出现无法分配的情况下,会触发一次Minor GC,这次GC之后,会将Eden区中存活对象放入S0中,并且扫描S1区,将S1区中的存活对象同样放入S0中,超过年龄阈值的将进入Old区,然后S0和S1进行交换。

当Young区发生YGC的时候,JVM首先会检查老年代的最大可用连续空间是否大于新生代所有对象的总和,如果大于那么这次YGC是安全的,如果不大于的话,JVM就需要判断HandlePromotionFailure是否允许空间分配担保。

允许担保的情况

JVM继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,

如果大于则进行一次YGC,尽管存在风险(风险在于这次晋升的对象可能比之前历次晋升对象的平均值大很多);

如果小于,或者HandlePromotionFailure不允许分配担保,这时就要进行一次FGC

一般情况下,需要将HandlerPromotionFailure的开关打开,避免FullGC过于频繁

b)、Old区

一般来说Old区的对象都是年龄超过我们设置阈值的对象(年龄较大的对象)

在Old区里发生的GC交Major GC ,每次GC过后还存活的对象的年龄也会+1,如果超过每个年龄阈值也会被回收。

4、一个对象的分配过程

02-JVM虚拟机栈及内存模型

新对象申请内存空间–》如果Eden有足够空间将会直接分配在Eden区,如果Eden没有足够的空间将会去进行一次MinorGC,GC之后如果Eden仍然没有足够的空间,将会判断S区是否有足够空间,如果S区有足够空间,将会将新生代活跃对象复制到S区,然后进行分配,如果S区也没有足够空间,将会分配到OLD区,如果OLD区也没有足够空间,进行一次FullGC,如果FullGC之后仍然没有足够空间,将会发生OOM。

5、栈上分配和TLAB

1)栈上分配

在对象分配的过程中,有很多的对象的作用域都是不会逃逸出方法之外的,也即是说该对象的生命周期会随着方法的调用而开始,随着方法调用的结束而结束,对于这种对象,我们可以考虑不将其分配在堆空间中。

因为一旦分配在堆空间中,当方法调用结束,没有引用的对象就会被gc回收,如果存在大量的这种情况,对gc来说是一种负担。

什么是栈上分配?

针对那些不会逃逸出方法之外的对象,在分配内存的时候不再将对象分配在堆空间中,而是将对象的属性打散后分配在栈(线程私有,属于栈内)上,这样随着方法调用的结束,栈空间的回收就会将分配在栈上的打散的对象也回收,不再给gc增加额外的负担,从而提高应用程序整体的性能。

开启栈上分配?

首先要开启逃逸分析(1.6默认开启)

-XX:+DoEscapeAnalysis

开启标量替换(-XX:+EliminateAllocations)

标量替换的作用是允许将对象根据属性打散后再分配在栈上,默认该配置为开启

2)TLAB(Thread Local Allocation Buffer)

从字面意思理解,线程本地分配缓存

我们知道,堆是线程共享的,当多个线程同一时刻操作堆空间时,就需要进行同步,而同步带来的结果就是导致对象分配的效率变差

TLAB就是为每个线程分配一个私有的堆空间(很小),每个线程分配对象到堆空间中时,先分配在属于自己的那块私有的堆空间中,避免同步带来的效率问题,从而提高对象分配的效率。

这块空间也是从eden区中划分出来的

开启TLAB

JDK默认开启了TLAB,可以使用-XX:+UseTLAB显示开启

-XX:PrintTLAB 打开跟踪TLAB的使用情况

-XX:TLABSize 通过这个参数指定分配给每个线程的TLAB空间的大小

一个对象分配的过程

栈上分配 --> TLAB --> old --> young

02-JVM虚拟机栈及内存模型

相关标签: Java