2、JVM运行时数据区

运行时数据区介绍

上节讲解了JVM的类加载机制,class字节码文件经类加载子系统加载后存放到运行时数据区的方法区中。而运行时数据区就是Java虚拟机在执行Java程序的过程中会把它管理的内存分为若干个不同的数据区域。这些区域有着各自的用途,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》中规定,jvm所管理的内存大致包括以下几个运行时数据区域(Runtime Data Area):方法区(JDK8中,方法区改为了元空间)、堆、程序计数器、本地方法栈、虚拟机栈。
启动一个JVM虚拟机对于计算机来说是运行了一个进程,JVM是支持多线程的,在JVM进程中可以同时执行多个线程。堆和方法区是线程之间共享的;程序计数器、本地方法栈和虚拟机栈是每个线程独有的。
在这里插入图片描述

1、程序计数器

JVM中的程序计数器(program counter register)不同于CPU中的寄存器,CPU中的寄存器是硬件层面的存储计算机指令和数据的,而java中的程序计数器是软件层面存储JVM指令地址的,只是存储执行指令地址,由执行引擎读取指令执行。
程序计数器所占内存非常小,仅仅是存放指令的地址。程序计数器的声明周期与线程的生命周期一致,每个线程有独自的程序计数器。
程序计数器是唯一一个不会发生OOM的区域。


下图为通过javap反编译class文件后的字节码内容,图中标红的即为指令地址或者内存偏移地址。
在这里插入图片描述

2、虚拟机栈

2.1、虚拟机栈介绍

JAVA虚拟机,英文名java virtual machine stack。每个线程在创建时都会创建一个虚拟机栈,虚拟机栈内部保存着一个个栈帧(stack frame),每一个栈帧代表一次方法调用,每一个栈帧内部保留了该方法的局部变量,虚拟机栈参与方法的调用和返回。虚拟机栈的生命周期是与线程保持一致的,线程结束,栈被回收。

不同平台的CPU架构不同,基于物理寄存器指令的程序在平台间移植会比较困难,而Java指令都是根据虚拟机栈设计的,在不同平台间移植java代码时,只需要在不同的平台上启动JVM即可,解耦了JAVA的指令代码。
JVM指令、或者栈指令、或者字节码指令都是一个意思,不同于JAVA代码指令,JAVA代码在编译期,把java代码编程class字节码,在运行时把class字节码加载到内存中翻译成字节码指令(字节码最后被运行时还会被翻译成机器指令)。
JVM指令有点就是跨平台,指令集小,编译期容易实现,缺点时性能下降,实现同样功能需要更多指令。

虚拟机栈采用先进后出形式,每执行一个方法,创建一个栈帧执行入栈操作,每执行完一个方法,对应的栈帧执行出栈操作。虚拟机栈的访问速度非常快,仅次于程序计数器,不存在垃圾回收问题。

2.2、虚拟机栈异常

JVM规范JVM栈的大小可以是固定的,也可以是动态的,不同的模式下会出现不同的异常。

  • JVM栈固定,每个线程在创建时的虚拟机栈是固定的(通过Xss设置),如果线程在执行过程中实际需要的栈容量大于Xss设置的值,虚拟机就会抛出StackOverflowError异常。
  • JVM栈动态扩展,线程在执行过程中,实际需求内存量增加时,虚拟机栈会进行扩容,当虚拟机栈在扩容过程中无法申请足够内存时,虚拟机就会抛出OOM(OutOfMemoryError)异常。

动态栈时视内存大小而定,固定栈时通过-Xss设置,示例如下:

public class StackDemo1 {

    public static long count = 0;

    public static void main(String[] args) {
        method1();
    }

    public static void method1(){
        count++;
        System.out.println(count + " : " + "execute method1");
        method1();
    }
}

在启动之前首先设置虚拟机栈参数,如下所示
在这里插入图片描述
由于设置的栈非常小,只有128k,无限递归调用method1方法时,最终导致栈内存溢出,执行结果抛StackOverflowError异常如下

1 : execute method1
...(中间省略)
812 : execute method1
813 : execute method1
Exception in thread "main" java.lang.StackOverflowError
	at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
	at sun.nio.cs.UTF_8.access$200(UTF_8.java:57)

2.3、虚拟机栈运行原理

虚拟机栈的生命周期与线程一致,栈中的数据都是以栈帧形式存在,每个方法对应栈中的一个栈帧。栈帧是内存中一块区域,是一个数据集,维系着方法执行过程中的各种数据信息。

栈的运行就是压栈和出栈操作,遵循后进先出原则,不同的线程中的栈帧是不能相互引用的,线程中的栈是隔离的。在线程执行过程中,同一时刻只会有一个活动的栈帧,即当前栈帧(current frame)。执行引擎执行的所有字节码只对当前栈帧操作。当在执行方法过程中,调用了新的方法,新的栈帧就会被创建并压栈进来,等被调用的方法执行结束后会被出栈,回到原来的栈帧继续执行。


java方法中返回有两种形式:一种使用return正常返回;另一种是方法抛出异常,非正常结束。两种方式都会执行出栈操作。
在这里插入图片描述

2.4、栈帧结构

每个栈帧就是内存中一块区域,每个栈帧大小不一定相等的,取决于栈帧内部数据量大小。每个栈帧由图中5部分组成:

  1. 局部变量表(Local Variables);
  2. 操作数栈(Operand Stack),又叫表达式栈;
  3. 动态链接(Dynamic Linking),又叫指向运行时常量池的方法引用;
  4. 方法返回地址(Return Address),又叫方法正常退出或者异常退出地址;
  5. 附加信息

在这里插入图片描述

2.4.1、局部变量表

局部变量表时栈帧中一个内存数组,主要用于存储方法参数以及定义在方法体内的局部变量,存储在局部变量表中的数据类型包括基本数据类型、对象的引用。局部变量表在栈帧内部,栈帧在栈内部,由于栈不存在数据安全问题,所以栈帧和局部变量表数据都是安全的。


局部变量表占的容量大小在编译期间确定的,并保存在方法属性maximum local variables中,在整个方法运行期间,局部变量表大小不会改变。

例如查看下面demo中add方法所属栈帧的局部变量表

package com.lzj.runtime;

public class LocalVariableDemo {
    public static void main(String[] args) {
        LocalVariableDemo demo = new LocalVariableDemo();
        demo.add(2, 3);
        demo.dec(10, 3);
    }
    
    public int add(int a, int b){
        int sum = a + b;
        System.out.println("sum = " + sum);
        return sum;
    }
    
    public int dec(int a, int b){
        int dec = a - b;
        System.out.println("dec = "+ dec);
        return dec;
    }
}

该代码编译后,局部变量表已确定,用反编译工具打开如下图所示,其中Maximum local variables: 4,表示add方法所属的栈帧中局部变量表长度为4
在这里插入图片描述
继续打开add方法所属栈帧的局部变量表,可以看到局部变量表数组中的内容,从0位到3位分别为:this对象变量、a、b、sum。
在这里插入图片描述
对于一个方法,它的参数和局部变量越多,局部变量表占用内存越大,栈帧就越大,占用的栈空间越大,如果栈空间容量有限,就容易导致栈溢出。
局部变量表中的变量只在当前方法中调用有效,当方法调用结束后,随着方法所属的栈帧消亡,局部变量表也随之消亡。

2.4.1.1 局部变量表中的槽(slot)

局部变量表是一个数组,局部变量表基本存储单元是槽(slot)。在java语法中整形数组可以表示为int[] a,字符串类型可以表示为String[] a,引用类型可以表示为Object[] a,而局部变量表可以表示为slot[] a。局部变量表的槽中可以存放java中8种基本数据类型以及引用类型,其中,32位以内的类型变量只占用局部变量表中的一个slot,64位类型变量占2个slot,即只有long和double类型变量占用2个slot,其它java的基本类型以及引用类型都只占用一个slot。byte、short、char在在局部变量表的slot中存储都是以int类型存储,boolean也是以int类型存储,0表示false,非0表示true。


JVM会为局部变量表中每一个slot分配一个访问索引,通过这个索引可以访问局部变量表中的局部变量。当一个实例方法(注意是实例方法,不能是static方法,后续会介绍)被调用时,该方法的入参、局部变量以及返回值按照顺序被复制到一个个slot中,如下图所示。如果被调用的方法为构造方法或者实例方法,那么局部变量表中的第一个slot必须为this对象,即slot[0] = this,其余的入参、局部变量以及返回值按出现的先后顺序复制到slot[1]、slot[2]……。如果局部变量为64位,存储在局部变量表中占用2个slot,访问该变量时只需要使用第一个索引即可。如下图所示,long m占了slot[1]、slot[2]两个槽,访问m变量时只需要使用slot[1]即可。
在这里插入图片描述

2.4.1.2 构造器实例方法在局部变量表中存储slot

构造器方法和实例方法都是以栈帧形式存储在内存中,构造器方法和实例方法的入参、局部变量以及返回值都是存储在局部变量表的slot中,第一个slot一定为this对象。以如下代码为例,只有一个构造器方法

public class SlotDemo1 {
    int id;
    String name;
    public SlotDemo1(int id, String name){
        this.id = id;
        this.name = name;
    }
}

对该代码编译后的class文件SlotDemo1.class通过Jclasslib工具翻译成java的字节码指令,其中< init >方法即为构造器方法,可见,构造器方法所属的栈帧中的局部变量表还有3个slot。
在这里插入图片描述
继续查看SlotDemo1构造器方法的局部变量表,如下所示,发现,第一个slot位置的参数为this对象,引用类型,第二个参数为id,int类型,第三个参数为name,应用类型。
在这里插入图片描述
实例方法和构造器一样,第一个slot参数一定为this对象的引用。

2.4.1.3 静态方法在局部变量表中存储slot

静态方法所属的栈帧中的局部变量表中第一个slot不为slot,其它均与构造器和实例方法一致。以如下代码所示,只还有一个add静态方法

public class SlotDemo2 {
    public static int add(){
        int a = 2;
        int b = 3;
        int c = a + b;
        System.out.println(c);
        return c;
    }
}

通过Jclasslib翻译后的字节码指令如下,发现局部变量表中第一个slot存储int类型的a变量,第二个slot存储int类型的b变量,第三个slot存储int类型的c变量。由此可见,static方法中没有this对象,所以在static静态方法中不能引用对象的成员变量,即不能调用this对象中的方法和属性,因为static方法拿不到this对象的引用。
在这里插入图片描述

2.4.1.4 long和double类型的变量在局部变量表中存储slot

上面已介绍,long或者double在局部变量表中占用2个slot,如下代码所示

public class SlotDemo3 {
    public void add(double a, double b){
        double sum = a + b;
        System.out.println(sum);
    }
}

对上述代码的SlotDemo3.class用Jclasslib工具打开,查看字节码
在这里插入图片描述
查看add方法的局部变量表,发现该局部变量表占用了7个slot,但add方法中实际只有2个入参a和b,1个局部变量sum,1个this对象,this对象占用第一个槽位slot[0],a占用两个槽位slot[1]和slot[2],但通过slot[1]进行访问,b占用slot[3]和slot[4]两个槽位,sum占slot[5]和slot[6]两个槽位。
在这里插入图片描述

2.4.1.5 slot重用

栈帧中的局部变量表中的槽位可以重复利用,如果一个局部变量超过了其作用域,那么在其作用域之后申明的新局部变量就很可能会重用过期的局部变量的槽位,从而达到节省资源目的。如下代码所示

public class SlotDemo4 {
    public void add(){
        {
            int a = 2;
            int b = a * 2;
        }
        int c = 5;
        System.out.println(c);
    }
}

把SlotDemo4.class文件通过Jclasslib工具查看该类的字节码,如下所示,字节码总长度为16,而局部变量表的长度为3,但add方法中应该有4个变量,this、a、b、c,但本案例实际上只分配了3个存储槽。
在这里插入图片描述
究其原因,发现c复用了a变量的槽位。Length表示变量作用字节码的长度,Start PC表示变量作用范围的起始偏移量(相对于add方法字节码初始位置),首先第一个槽位的变量slot[0]为this对象,作用字节码范围起始位置为0,作用长度为16,说明this对象作用域从add方法开始到结束。第二个槽位slot[1]为a,作用字节码范围起始位置为2,作用长度为4,说明a变量在add方法字节码的第2号位置到第6号位置有效,出了这个范围,a则失效。变量b在slot[2]位置,作用字节码范围起始位置为6,作用长度为0,说明b只在字节码的第6行有效。而变量c在slot[1]位置,作用字节码范围起始位置为8,作用长度为8,说明变量c作用字节码第8行一直到add方法字节码最后一行结束,c也被分配到了slot[1]中,说明a变量已经出了作用域,被c复用了slot[1]位置。

在这里插入图片描述

2.4.1.6、局部变量表对垃圾回收的影响

局部变量表中的变量是重要的垃圾回收根节点,当在堆上创建一个对象时,也会在栈帧的局部变量表中创建一个局部变量指向对象的堆地址,只要被局部变量直接引用或间接引用的对象都不会被回收。在栈帧中,与性能调优关系最密切就是局部变量表。

2.4.2 操作数栈

2.4.2.1 操作数栈介绍

操作数栈(operand stack),在方法执行过程中,根据字节码指令,往栈中写入数据(入栈)或从栈中读取数据(出栈)。操作数栈用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。
操作数栈是JVM执行引擎的一个工作区,当一个方法开始执行的时候,操作数栈也会被创建,操作数栈底层是用数组实现的,操作数栈被创建出的时候,其栈的深度,也即数组的长度就固定的,栈的深度是在编译期就确定好的,保存在方法到的code属性中,每一个方法开始执行的时候,操作数栈会随着栈帧创建出来。
操作数栈可以存储java的任意类型,long和double类型变量占用两个栈单位深度,其它类型占一个栈的深度。操作数栈虽是数组类型实现的,但操作数栈不可以通过数组索引访问栈中的元素,只能通过入栈和出栈存入或读取数据。
如下SlotDemo3代码所示,该段代码编译成SlotDemo3.class字节码文件后,通过javap工具翻译SlotDemo3.class,执行命令javap -v SlotDemo3.class,翻译后的字节码文件,在code属性中stack=4,表示栈的深度为4(数组长4),其中locals=7表示局部变量表的长度为7。

//java源文件
public class StackDemo1 {

    public static void main(String[] args) {
        StackDemo1 stackDemo1 = new StackDemo1();
        int sum = stackDemo1.add();
        System.out.println(sum);
    }

    public int add(){
        int a = 2;
        int b = 3;
        int sum = a + b;
        return sum;
    }

}

//add方法javap翻译后的字节码文件
  public int add();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_2
         1: istore_1
         2: iconst_3
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: istore_3
         8: iload_3
         9: ireturn
      LineNumberTable:
        line 12: 0
        line 13: 2
        line 14: 4
        line 15: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/lzj/runtime/StackDemo1;
            2       8     1     a   I
            4       6     2     b   I
            8       2     3   sum   I

2.4.2.1 栈顶缓存

基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作需要使用更多的入栈和出栈指令,如下add方法,方法体中只有一项a+b操作,但完成这一操作需要更多的JVM指令入栈出栈才能完成操作,如下所示add方法被翻译成JVM指令,通过JVM指令可以看出分别对c、d、s2执行入栈和出栈操作

    public int add(){
        int c = 5;
        int d = 6;
        int s2 = c + d;
        return s2;
    }

add方法被翻译成JVM指令如下

         0: iconst_5
         1: istore_1
         2: bipush        6
         4: istore_2
         5: iload_1
         6: iload_2
         7: iadd
         8: istore_3
         9: iload_3
        10: ireturn

因为栈中的操作数都是存储在内存中的,频繁的对内存执行读和写操作必然会影响执行速度,为了解决该问题,HotSpot虚拟机设计者提出了栈顶缓存(Tos, Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理的CPU寄存器中。例如执行到上述add方式时,必然会创建一个新的栈帧,该栈帧位于虚拟机栈的最顶部,即栈顶,利用Tos技术,会把add方法所属的栈顶元素全部缓存到寄存器中,避免了很多入栈和出栈操作,降低了对内存的读写次数,提升了执行引擎执行效率。

2.4.3 动态链接

每一个栈帧内部都包含一个指向运行时常量池中栈帧所属方法的引用。包含这个引用目的就是为支持当前方法的代码能都实现动态链接(Dynamic Linking)。动态链接也叫运行时常量池的引用,注意动态链接是对常量池中的变量或者方法引用。
java源文件被变异成字节码文件时,所有的变量和方法都作为符号引用保存在class文件的常量池中。而动态链接就是在常量池中找到当前栈帧方法内需要的变量以及方法的地址,并进行直接引用。比如:一个方法A调用了另一个方法B时,动态链接就是把方法A指向常量池中方法B的引用。如下图所示,栈帧所属方法内部需要调用的变量和方法,在字节码中存放的只是需要调用的变量和方法的引用地址,具体地址中内容要去运行时常量池中找。
在这里插入图片描述
以如下java源代码为例

public class StackDemo1 {

    public static void main(String[] args) {
        StackDemo1 stackDemo1 = new StackDemo1();
        int sum = stackDemo1.add();
        System.out.println(sum);
    }

    public int add(){
        int a = 2;
        int b = 3;
        int sum = a + b;
        return sum;
    }

}

StackDemo1 .java编译后胜场StackDemo1 .class字节码文件,通过javap -v StackDemo1 .class工具翻译成jvm指令如下,其中Constant pool即为运行时常量池,字节码解释运行时会把Constant pool放到运行时常量池中。

public class com.lzj.runtime.StackDemo1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#28         // java/lang/Object."<init>":()V
   #2 = Class              #29            // com/lzj/runtime/StackDemo1
   #3 = Methodref          #2.#28         // com/lzj/runtime/StackDemo1."<init>":()V
   #4 = Methodref          #2.#30         // com/lzj/runtime/StackDemo1.add:()I
   #5 = Fieldref           #31.#32        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = Methodref          #33.#34        // java/io/PrintStream.println:(I)V
   #7 = Class              #35            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/lzj/runtime/StackDemo1;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               stackDemo1
  #20 = Utf8               sum
  #21 = Utf8               I
  #22 = Utf8               add
  #23 = Utf8               ()I
  #24 = Utf8               a
  #25 = Utf8               b
  #26 = Utf8               SourceFile
  #27 = Utf8               StackDemo1.java
  #28 = NameAndType        #8:#9          // "<init>":()V
  #29 = Utf8               com/lzj/runtime/StackDemo1
  #30 = NameAndType        #22:#23        // add:()I
  #31 = Class              #36            // java/lang/System
  #32 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
  #33 = Class              #39            // java/io/PrintStream
  #34 = NameAndType        #40:#41        // println:(I)V
  #35 = Utf8               java/lang/Object
  #36 = Utf8               java/lang/System
  #37 = Utf8               out
  #38 = Utf8               Ljava/io/PrintStream;
  #39 = Utf8               java/io/PrintStream
  #40 = Utf8               println
  #41 = Utf8               (I)V
{
  public com.lzj.runtime.StackDemo1();
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class com/lzj/runtime/StackDemo1
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method add:()I
        12: istore_2
        13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: iload_2
        17: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
        20: return

  public int add();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_2
         1: istore_1
         2: iconst_3
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: istore_3
         8: iload_3
         9: ireturn
}

下面以main方法中下面一条指令为例

9: invokevirtual #4                  // Method add:()I

jvm执行到该指令后,表示需要调用方法,方法的地址指向#4,在Constant pool中查找,防线#4为

 #4 = Methodref          #2.#30         // com/lzj/runtime/StackDemo1.add:()I

#4表示方法引用,引用方法的地址为 #2.#30 ,继续查找Constant pool发现#2和#30分别为

#2 = Class              #29            // com/lzj/runtime/StackDemo1
#30 = NameAndType        #22:#23        // add:()I

#2表示类,类的地址指向#29,继续查找Constant pool,最终#29为com/lzj/runtime/StackDemo1类

 #29 = Utf8               com/lzj/runtime/StackDemo1

#30指向了地址#22:#23,继续查找Constant pool,返现#23和#22如下,#22表示add方法,#23表示返回int类型。到此为止,main方法调用add方法,在常量池中找到了引用的地址,然后执行。

  #22 = Utf8               add
  #23 = Utf8               ()I

2.4.4 方法返回地址

关于JVM中方法的介绍,参考JVM方法介绍
虚拟机栈中的方法返回地址就是一块内存结构,存放着调用该方法的PC寄存器的值,也即下一条指令的地址。例如A方法调用B方法,把B方法的栈帧压入虚拟机栈,当B方法执行结束后,B方法的栈帧中的方法返回地址存放的是A方法调用B方法指令的下一条地址,以便B方法调用结束后回到A方法继续向下执行。


一个方法的结束方式有两种:
  • 正常执行结束;
  • 抛出异常未处理,非正常结束。

不管哪种结束方式,方法退出后都回到方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法指令的下一条指令地址;异常退出时,返回地址是通过异常表确定,栈帧中不保存该部分信息。
方法正常结束和异常结束的区别也就在于:方法异常结束退出不会返回给调用者任何返回值信息。

方法正常结束时会使用返回指令,返回指令根据方法返回类型决定使用哪一个返回指令。在字节码指令中,当返回值类型为boolean、byte、char、short和int类型时,使用返回指令ireturn;当返回值类型为long时,使用返回指令lreturn;当返回值类型为floag时,使用返回指令freturn;当返回值类型为double类型时,使用返回指令dreturn;当返回值类型为引用类型时,使用返回指令areturn;最后当方法为void方法、实例初始化方法、类和接口的初始化方法,返回值类型为return。


方法异常结束时,当方法执行过程中遇到了Excetion,并且异常没有方法内进行try…cache处理,那么在方法异常表中就搜索不到匹配类型的异常处理器,导致方法异常退出。如果做了异常出来了,就会存储在异常处理表中,方便在发生异常时找到异常处理的代码。


如下方法为例,在main方法中调用了show盒divide方法,show方法正常退出,divide抛异常退出。
public class MethodDemo3 {
    public static void main(String[] args) {
        show();
        try {
            divide();
        }catch (ArithmeticException exception){
            exception.printStackTrace();
        }
        System.out.println("man execute end!");
    }
    
    //正常退出方法
    public static void show(){
        System.out.println("I'm show method");
    }
    
    //异常退出方法
    public static void divide(){
        BigDecimal a = new BigDecimal(5);
        BigDecimal b = new BigDecimal(3);
        System.out.println(a.divide(b));
    }
}

对上述java代码变异成字节码指令,截取部分如下所示

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: invokestatic  #2                  // Method show:()V
         3: invokestatic  #3                  // Method divide:()V
         6: goto          14
         9: astore_1
        10: aload_1
        11: invokevirtual #5                  // Method java/lang/ArithmeticException.printStackTrace:()V
        14: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        17: ldc           #7                  // String man execute end!
        19: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        22: return
      Exception table:
         from    to  target type
             3     6     9   Class java/lang/ArithmeticException

其中Exception table即为异常表。
0: invokestatic #2 为调用show方法,show方法的栈帧中的方法返回地址存放的是3,下一条执行的地址,即 3: invokestatic #3 ,即回到main方法后,pc计数器将要执行的下一条指令,即开始调用divide方法。
在调用方法divide过程中,有可能会出现异常,有可能除法除不尽,查看异常表如下

   Exception table:
     from    to  target type
         3     6     9   Class java/lang/ArithmeticException

异常表显示从地址3到地址6可能会出现ArithmeticException异常,如果出现了ArithmeticException异常,按指令地址9处方式来处理代码,如果没有异常发生就按指令地址14处方式继续执行代码。

     3: invokestatic  #3                  // Method divide:()V
     6: goto          14

2.4.5 附加信息

附加信息可有可无,根据不同的虚拟机设定的机制有关。附加信息一般栈帧中携带的与java虚拟机实现相关的一些附加信息,例如对程序调式提供的支持信息。

2.4.6 虚拟机栈中指令执行过程分析

1、字节码指令未执行之前如下所示,其中程序计数器、局部变量表,操作数栈均为空。
在这里插入图片描述
2、执行指令bipush 20,表示把byte类型值转为int类型并压入操作数栈。首先把指令地址0存入计数器,把数值20压入栈,栈顶上移。
在这里插入图片描述
3、执行指令istore_1,表示把栈顶元素存入局部变量表中第1号位置。计数器存入地址2,并把数值20出栈存入局部变量表中第一号位置,栈顶下移。注意第0号位置存入的this变量。
在这里插入图片描述
4、执行bipush 30,表示数值30压入操作数栈。计数器存入地址3,把数值30压入操作数栈,栈顶上移。
在这里插入图片描述
5、执行istore_2,表示把栈顶元素载入局部变量表中第2号位置。计数器存入地址5,并把30出栈存入局部变量中第2号位置,栈顶下移。
在这里插入图片描述
6、执行iload_1,表示加载局部变量表中第1号位置元素到操作数栈。计数器存入地址6,并把局部变量表中第1号位置的20加入到操作数栈中,栈顶上移。
在这里插入图片描述
7、执行iload_2,表示加载局部变量表中第2号位置元素到操作数栈。计数器存入地址7,并把局部变量表中第2号位置的值30加入到操作数栈中,栈顶上移。
在这里插入图片描述

8、执行iadd,表示两个int类型相加。计数器存入地址8,CPU从操作数栈最顶读取两个数进行相加,即20+30=50,操作数栈中的两个数30和20也伴随着出栈,因为只有出栈了才能在CPU中执行相加,相加的结果50压入操作数栈中。
在这里插入图片描述
9、执行istore_3,表示把栈顶值填入局部变量中第3号位置。计数器读取地址9,栈顶元素50出栈,填入了局部变量表的第3号位置。
在这里插入图片描述
10、执行iload_3,表示加载局部变量表中第3号位置的变量50并压入栈顶,栈顶上移。
在这里插入图片描述
11、执行ireturn,表示返回一个int类型数值。计数器存入地址11,该指令执行完毕后,会把栈顶元素50返给调用者,并且该栈帧被出虚拟机栈,从而消亡。

3、本地方法栈

3.1 本地方法

本地方法就是native method,一个native method就是一个java调用非java代码的接口。native method方法由非java语言实现,比如c/c++,在定义一个native method时,仅仅是一个接口,其实现方法由非java语言在外面实现。native method设立之初就是融合c/c++语言。

3.2 本地方法栈

java虚拟机栈用于管理java方法的调用,本地方法栈用于管理本地方法调用(本地方法是用C语言实现的)。
本地方法栈与虚拟机栈的原理基本相似,native method stack用于对native method interface进行入栈和出栈操作,执行引擎执行native method方法时加载native method libraries,如下图所示。
本地方法栈同虚拟机栈相同,都是线程私有的,栈可以被设定为固定大小,也可以是动态扩展大小。

如果线程请求的本地方法栈容量大于本地方法栈被允许的最大栈容量,java虚拟机抛出StackOverflowError异常;
如果本地方法栈动态扩展,在扩展的时候无法申请足够的内存或者创建的时候没有足够内存去创建本地方法栈,java虚拟机将抛出OutOfMemoryError异常。

在这里插入图片描述
当一个线程调用本地方法时,就进入了一个全新的不受虚拟机限制的世界,本地方法由于是C实现的,可以直接操纵底层的内存,可以处理很多与虚拟机相同的功能,与虚拟机有相同的权限。

本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区;
本地方法可以直接使用本地处理器中的寄存器;
本地方法可以直接从本地内存中的堆中分配任意数量的内存。


不同虚拟机厂家对JVM支持的力度不同,本地方法栈可有可无,hotspot虚拟机中支持本地方法栈。

4、堆

4.1 堆介绍

在启动应用后,一个进程就是一个JVM实例,一个JVM实例只有一个运行时数据区(Runtime Data Area),一个Runtime中只有一个方法区和堆区。一个进程可以包括多个线程,每个线程有独自的程序计数器、本地方法栈和虚拟机栈,所有的线程共享一个Runtime,共享一个方法区,共享一个堆区。但是不同的JVM厂商,在堆里可以划分一块缓冲区(Thread Local Allocation Buffer, TLAB)供每个线程私有。
堆是java内存管理的核心区域,堆在JVM启动时创建,创建后大小即确定。堆可以是物理上不连续的内存空间,但逻辑上被视为连续的。
所有的对象实例和数组都分配在堆上,但对象或者数组的引用变量分配在虚拟机栈栈帧中的操作数栈中。例如下面SimpleHeap类,初始化的对象分配在对上,对象引用的变量分配在栈上,SimpleHeap类以及类方法存储在方法区。在这里插入图片描述
其中对象实例用字节码指令为new,数组实例化用的字节码指令为newarray,如下代码所示

public class SimpleHeap {
    public void sayHello(){
        System.out.println("hello SimpleHello");
    }
    public void show(int array[]){
        int n = array.length;
        for (int i=0; i<n; i++){
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        SimpleHeap simpleHeap = new SimpleHeap();
        simpleHeap.sayHello();
        int array[] = new int[3];
        array[0] = 0;
        array[1] = 1;
        array[2] = 2;
        simpleHeap.show(array);
    }
}

编译成字节码指令后,截取片段如下,可见实例化SimpleHeal用的new,实例化array用的newarray指令。

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: new           #6                  // class com/lzj/runtime/heap/SimpleHeap
         3: dup
         4: invokespecial #7                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #8                  // Method sayHello:()V
        12: iconst_3
        13: newarray       int
        15: astore_2
        16: aload_2
  		.
  		.
  		.

堆内存分类

当前绝大多数垃圾收集器都是基于分带收集理论设计,堆是垃圾收集的最重要区域。在JDK7以及之前版本,堆内存主要分为新生代(yong generation space)、老年代(tenure generation space)以及永久代(permanent space)。


在JDK8以及之后版本,堆内存主要分为了新生代(yong generation space)、老年代(tenure generation space)以及元空间(meta space),其中元空间的作用类似于永久代作用,但在JDK8以及之后的版本,元空间已经从堆内存划出去了,属于运行时内存结构,因此堆内存只包括了新生代和老年代。


新生代区又分为了Eden 、Survivor1以及Survivor2区
在这里插入图片描述

还是以上述SimpleHeap为例,只修改main方法如下

public static void main(String[] args) throws InterruptedException {
    SimpleHeap simpleHeap = new SimpleHeap();
    simpleHeap.sayHello();
    int array[] = new int[3];
    array[0] = 0;
    array[1] = 1;
    array[2] = 2;
    simpleHeap.show(array);
    Thread.sleep(6000000);
    System.out.println("main execute end");
}

设置堆大小-Xms20m -Xmx20m,再次运行main方法,通过JDK自带的jvisualvm工具可以查看堆内存细分情况,如下图所示,从图中可以看出,Eden 、Survivor0、Survivor1以及老年代,4个区内存加起来正好等于20m,说明JDK8中堆内存只包括该4部分。
在这里插入图片描述

4.2 堆空间大小设置

前面章节已介绍设置堆的固定大小可以用-Xms和-Xmx进行设置

-Xms用于设置堆的起始内存,等价于-XX:InitialHeapSize,其中-X表示参数,m表示memory,s表示start
-Xmx用于设置堆的最大内存,等价于-XX:MaxHeapSize,其中x表示max

如不为堆设置固定大小,堆默认情况下,堆的初始内存大小为物理内存大小的 1/64;最大内存默认为物理内存大小的 1/4。
如若设置堆固定大小时,一般将-Xms和-Xmx两个参数配置成相同的值,目的是为了能够在java垃圾回收机制清理完堆后不需要重新分割计算堆区的大小,从而提高性能。例如初始内存设置为10M,最大内存设置为100M,当堆分配的对象增多,超过10M时,堆就会扩展堆内存,比如扩展到了50M,当堆上分配的对象继续增加时超过50M时,堆内存会继续扩容,比如扩到了80M,此时发生了GC,堆上的对象大部分被GC掉,需要的堆内存瞬间变小,堆内存会从新计算进行缩容,因此在堆不断的扩容和缩容时严重影响了系统的性能,导致性能下降,因此建议堆初始内存和最大内存保持一致,就不会涉及到了扩缩容。
一旦堆区中内存大小超过上限-Xmx设置的最大内存时,系统将抛出OutOfMemoryError异常。


分析堆空间时,如若查看堆空间大小,可以通过如下两种方式查看,还是以上述simpleHeap代码为例:
方式一
在命令行中,首先用jps查出当前应用进程,然后用jstat分析当前进程,如下图所示,其中8084为SimpleHeap的应用进程。其中S0C为Survivor0的容量,S1C为Survivor1的容量,EC为Eden的容量,OC为老年代容量,4个内存容量加起来等于20M,即为JVM进程中堆的总容量。
在这里插入图片描述
方式二
在启动应用时,添加参数*-XX:+PrintGCDetails*,然后启动应用,运行结果如下所示
在这里插入图片描述
从运行结果看,堆中内存容量为4个方框中标记总和 5632k + 512k + 512k + 1824k = 20m,其中新生代内存容量为5632k + 512k = 6144k。在实际应用中Survivor0和Survivor1只会用到其中一个,所以新生代内存容量只统计其中的一个。

4.3 新生代与老年代内存分配参数设置

1、设置新生代与老年代堆内存所占比例
在JVM中java对象有的生命周期很短,有的很长,甚至与JVM的生命周期相同,因此有必要设置新生代与老年代堆内存所占比重,如果生命周期短的对象居多,就要设置新生代堆内存大一点,如果生命周期长的对象居多,就要设置老年代堆内存比重更多一点。


-XX: NewRatio参数
通过-XX: NewRatio参数可以设置新生代与老年代堆内存的比重,默认-XX: NewRatio=2,表示 老年代 / 新生代=2,其中新生代 = Eden + Survivor0 + Survivor0=1。所以默认 老年代占整个堆内存的2/3,新生代占整个堆内存的1/3。
还是以上述的SimpleHeap为例,启动时设置VM参数为:

-Xms60m -Xmx60m -XX:+PrintGCDetails -XX:NewRatio=1

表示堆内存起始和最大内存都为60m,设置打印堆信息,并且设置新生代与老年代堆空间一样大,通过运行结果分析如下,23552+3584+3584 = 30720,表示 -XX:NewRatio=1结果已生效。
在这里插入图片描述
2、设置新生代中Eden与Survivor的堆空间比例

默认情况下,Eden与Survivor0和Survivor1之间的比例为8:1:1,但实际默认情况下并没有按严格的8:1:1进行设置,可以显示的设置-XX:SurvivorRatio参数来分配新生代内部堆空间比例,比如-XX:SurvivorRatio=4,表示Eden:Survivor0:Survivor1=4:1:1。继续以上述SimpleHeap为例,设置vm启动参数为:

-Xms60m -Xmx60m -XX:+PrintGCDetails -XX:NewRatio=1 -XX:SurvivorRatio=4

堆空间为60m,-XX:NewRatio=1表示新生代占30m,-XX:SurvivorRatio=4表示Eden:Survivor0:Survivor1=4:1:1,运行结果如下
在这里插入图片描述
其中Eden占20m,Survivor0占5m,Survivor1占5m,结果为4:1:1

3、设置新生代内存大小
-Xmn可以设置新生代内存大小,该参数一般不用,与-XX: NewRatio功能类似,继续以SimpleHeap为例,设置VM启动参数如下,表示堆栈为40m,新生代占20m,Eden与Survivor占比为2:1:1

-Xms40m -Xmx40m -Xmn20m -XX:+PrintGCDetails -XX:SurvivorRatio=2

在这里插入图片描述
如图所示,Eden占10m,Survivor0和Survivor1分别占5m,新生代总内存为20m

4、查看设置的JVM参数
在调试过程中,如果要查看JVM设置的参数,比如NewRatio、SurvivorRatio等,可以用jinfo命令,如下所示
在这里插入图片描述

几乎所有的java对象都在Eden区创建;
绝大部分的java对象的销毁都在新生代;

4.4 对象分配过程

下面以图解方式演示对象分配过程
1、 几乎所有对象创建都是在Eden区,当Eden区内存不足时就会发生minor GC,如下所示,Eden区内存已满
在这里插入图片描述

当Eden内存满了之后,发生minor GC进行垃圾回收,垃圾对象被回收,有用的对象会被放置到S0区,如下所示
在这里插入图片描述

minor GC后堆内存如下所示,经过第一次GC之后,幸存下来的对象age标记为1。此时S1为to区,S0为from区,幸存者区可以分为Survivor0和Survivor1区,也可以叫from和to区,to区为每次minor GC后Eden区的对象放置的位置,to区每次都是空的。
在这里插入图片描述
2、 程序继续运行,创建的对象依然首选放在Eden区,如下所示
在这里插入图片描述
当Eden区又满了后,再次发生minor GC,由于Eden区和S0区都有对象,GC会对这两个区都进行垃圾回收,Eden中幸存的对象放置到S1中,对象的年纪为1,垃圾对象被回收;S0中的垃圾对象会被回收,幸存者被复制到S1中,年纪在原有的年纪上都分别加1,如下所示。此时S0转化为to区,S1转化为from区。
在这里插入图片描述
3、当进过多次minor GC后,幸存者区中的对象已经经过很多次回收都没有被回收掉,比如达到了下面状态,此时程序继续执行,创建对象首先放在Eden区
在这里插入图片描述
当Eden区又满了后,再次发生minor GC,Eden区会发生GC,幸存者对象放到S1区,标记年纪为1;S0区也会发生GC,此时有对象的年纪为15,经过15轮GC都没被GC调,该对象复制到Old老年代区,对象年纪不大于15的分别加1,复制到S1区。最终堆内存状态如下所示,如果最终老年区也满了会发生full GC,后续再分析。
在这里插入图片描述

通过以上图解分析,总结如下;

垃圾回收频繁在新生区收集,很少在老年区收集,几乎不在元空间收集;
新生区中对象被GC 15次都没被回收的进入老年区,JVM默认是15次,如果要修改进入老年代的次数,可以通过参数 -XX:MaxTenuringThreshold=<n>进行设置

以上为对象分配一般过程,但对象分配还涉及到一些特殊步骤,比如对于超大对象,Minor GC后Eden还是放不下,就会向老年代分配,如果老年代也放不下就进行Full GC;
比如Eden区被Minor GC后,幸存对象在Survivor区放不下,直接晋升到老年代。
在这里插入图片描述

4.5 GC种类

对于HotSpot虚拟机,按照回收的区域GC可以分为两大类:部分收集(Partial GC)和整堆收集(Full GC)。

Partial GC并不收集整个堆,可以分为如下收集:

  1. 新生代收集(Minor GC / Young GC):只收集新生代;
  2. 老年代收集(Major GC / Old GC):只收集老年代,只有CMS的concurrent collection是这个模式;
  3. Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式

Full GC收集整个java堆空间和元空间。Full GC相对比较慢,开发中应减少Full GC的次数。

1、Minor GC

年轻代空间不足时,就会触发Minor GC,这里年轻代指的是Eden代,Survivor空间不足时不会触发Minor GC。当发生Minor GC时,Eden幸存下来的对象会晋升到Survivor区,如果Survivor区空间不足就会晋升到老年代。

一般java对象都具有朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也快。

Minor GC会引发STW( stop the world),暂停其它用户线程,等垃圾回收结束,用户线程才恢复运行。

2、Major GC

老年代发生GC时,一般是Major GC或者Full GC。
发生Major GC经常会伴随至少一次的Minor GC,(但非绝对,Parallel Scavenge收集器就是直接进入Major GCj进行回收)。首先老年代空间不足时,尝试触发Minor GC,如果空间还是不足则触发Major GC。

Major GC的速度一般会比Minor GC慢10倍以上,STW时间更长。
如果Major GC后,内存还不足,抛异常OOM。
.

Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。

3、Full GC

以下5种情况触发Full GC:

  1. 调用System.gc(),只是系统建议执行Full GC,大师不必然执行;
  2. 老年代空间不足;
  3. 方法区空间不足;
  4. 通过Minor GC后进入老年代的平均大小大于老年代可用内存;
  5. 由Eden区、Survivor0(也叫From space)区向Survivor1(也叫To space)区复制时,对象大于Survivor1可用内存,则把该对象转存到老年代,且老年代可用内存小于该对象大小。

4.6 对象在堆内存中分配策略

一般情况下,如果对象在Eden创建,经过第一次Minor GC仍然存活,并且Survivor区可用内存足够容纳的情况下,该对象会被转移到Survivor区,并将年龄设置为1岁。对象在Survivor区每经过一次Minor GC,年龄就会加1,当该对象的年龄增加到一定程度(默认为15岁,可以通过选项-XX:MaxTenuringThreshold来设置)时,就会被晋升到老年代。

对象分配策略如下:

  1. 优先分配到Eden区;
  2. 大对象直接分配到老年代,比如一个大对象大于Eden的可用内存,经过Minor GC后,Eden仍然放不下该对象,那么该对象就会直接分配到老年代中,所以应该尽量避免程序中出现过多的大对象;
  3. 如无设置-XX:MaxTenuringThreshold参数,对象经过15次Minor GC后仍存活,对于这种长期存活的对象转移分配到老年代;
  4. 动态对象年龄判断:如果Survivor区中相同年龄的所有对象大小总和大于Survivor区空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。
  5. 空间分配担保

空间分配担保:
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续内存空间是否大于新生代所有对象的空间总和。
  如果大于,则此次Minor GC是安全的;
  如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置的值是否允许担保失败;
    如果XX:HandlePromotionFailure=true,那么会继续检查老念叨最大可用连续内存是否大于历次晋升到老年代的对象的平均大小;
      如果大于,则尝试进行一次Minor GC,但Minor GC依然有风险;
      如果小于,则进行一次Full GC;
    如果XX:HandlePromotionFailure=false,则进行一次FULL GC;

从上述情况可知,只要老年大连续内存空间大于新生代对象总大小,或者设置XX:HandlePromotionFailure=true时,老年代连续内存空间大于历次晋升到老年代对象的平均大小,JVM会进行Minor GC,否则将进行FULL GC。
在JDK6之前均需要设置XX:HandlePromotionFailure参数才能使空间分配担保,在JDK7之后,已取消XX:HandlePromotionFailure参数设置,默认JVM源码就直接支持空间分配担保,相当于在JDK7之后,XX:HandlePromotionFailure默认为true。

4.7 TLAB对对象分配策略的影响

TLAB全名为Thread Local Allocation Buffer。众所周知,堆空间是线程共享的,线程访问堆空间中的对象是线程不安全的,为避免多个线程同时操作一个对象内存,需要对该对象内存采用加锁机制,由于对象在JVM中创建和销毁是非常频繁的,就需要对内存频繁的加锁和解锁,影响堆内存对象的分配速度,因此诞生了TLAB技术。
TLAB: 在Eden区,JVM为每个线程分配了一个私有的缓存区域,在该缓存区域中创建的对象是线程安全的。
例如多个线程同时在Eden区创建对象分配内存时,优先对象内存分配到TLAB区,这样就可以避免非线程安全问题。如果没有TLAB,比如有三个线程线程A、线程B、线程C,线程A需要创建A对象,线程B需要创建B对象,线程C需要创建线程C对象,三个线程需要同一时刻在Eden区创建3个对象,对于Eden区就要先对第一块区域加锁并比如分给了线程A创建的A对象,其它两个线程就不能在该内存再继续分配对象了,在第二块区域加锁并分配给了线程B创建的B对象,那么C就不可能在该区域分配内存了,因此如果没有TLAB技术,多个线程同时申请分配对象内存时,需要对内存区域采用加锁机制。有了TLAB技术,线程ABC就可以同一时刻分别在各自的私有缓存区创建对象了,避免了对内存加锁解锁操作。
TLAB技术不仅解决了非线程安全问题,还同时提升了内存分配的吞吐量,因此将这种内存分配方式称为快速分配策略
目前OpenJDK衍生出来的JVM都提供了TLAB设计。


如下图所示,Thread1、Thread2、Thread3、Thread4分别拥各自线程的TLAB区域,等TLAB内存区域用完了,就会在TLAB以外的Eden区创建对象并分配内存。
在这里插入图片描述
TLAB对象分配过程
JVM将TLAB作为内存分配的首选,但并不是所有对象都能在TLAB分配成功。默认TLAB是开启状态的,可以显示的开启TLAB,需设置参数 -XX:+UseTLAB,也可以关闭TLAB,需设置参数 -XX:-UseTLAB
默认情况下,TLAB空间非常小,只占整个Eden区的1%,也可以通过设置 -XX:TLABWasteTargetPercent 来设置TLAB占用Eden空间的百分比。
注意:一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间分配内存。TLAB内存分配如下图所示
在这里插入图片描述

4.8 对象逃逸对对象分配策略的影响

4.8.1 对象逃逸分析

众所周知,对象分配在堆内存上。但有一种特殊情况,如果经过对象逃逸分析(Escape Analysis)后,一个对象并没有逃逸出方法的话,那么该对象就有可能会在栈上创建,这样就不会在对上创建对象,当然也不会对该对象进行GC,该对象只会随着出栈进行消亡,这是最常见的对外存储技术。

基于Open JDK深度定制的TaoBal JVM,其中创新的GCIH(GC invisible heap)技术十点了off-heap,将生命周期较长的对象从堆中移到堆外,并且GC不会回收GCIH中的对象,因而会降低GC的回收频率和提升GC效率的目的。


**逃逸分析概述**

上面介绍过,如果对象在栈上创建就会减少堆的压力,减小GC的压力,而决定是否将对象分配在栈上的依据就是对象的逃逸分析。虚拟机栈一节中已介绍过,虚拟机栈是由一个个方法的栈帧组成,如果分析出对象只在方法中使用,当方法出栈后该对象就会消亡,这样的对象是未逃逸的对象,可以在栈上分配对象,不通过GC通过方法栈出栈回收对象空间,反之就是逃逸的对象,需要在堆上分配空间。

逃逸分析的判定准则

当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸;
当一个对象在方法中被定义后,它还被外部方法引用,则认为发生了逃逸分析。

示例分析如下:

public class HeapDemo2 {

    private Persion persion;

    /*
    * 案例一:方法返回HeapDemo2的实例对象demo,在方法外可能会继续使用对象demo,发生逃逸
    * */
    public HeapDemo2 getInstance(){
        HeapDemo2 demo = new HeapDemo2()
        return demo;
    }

    /*
    * 案例二:方法为成员变量persion赋值,赋值后的persion对象可能在外部会继续使用,发生逃逸
    * */
    public void setPersion(Persion persion){
        this.persion = persion;
    }

    /*
    * 案例三:p对象是调用getPesion获取的成员变量对象persion
    * 注意一个概念,分析对象是否逃逸,不是分析的对象的变量,因为对象的变量只是存储在栈上,而是分析的对象本身内容
    * 所以p只是栈上一个指向Persion实例对象的变量,而p指向的对象实际内容就是成员对象persion的内容,因为persion在外部可能会被引用,因此也发生了逃逸
    * */
    public void showPersion(){
        Persion p = getPersion();
        System.out.println(p);
    }

    /*
    * 案例四:由于创建的p1对象只是在createPersion内部使用,当createPersion调用结束后,p1对象也就消亡了,因此未发生逃逸
    * */
    public void createPersion(){
        Persion p1 = new Persion("lzj", 18);
        System.out.println(p1);
    }
    
    public Persion getPersion(){
        return persion;
    }
}

**逃逸分析的场景:** 经过上述代码分析,以下场景中对象发生逃逸:给成员变量赋值、方法返回值、实例引用传递。

逃逸分析的意义:
通过使用逃逸分析技术,可以分析出未逃逸的对象,对这些未逃逸的对象,在分配内存时,不再直接分配在堆上而是直接分配栈上,栈上的对象不需要GC,随着方法执行结束就自动出栈回收了。通过使用逃逸分析,JVM可以对代码进行优化,例如:栈上分配、同步省略、分离对象。

4.8.2 逃逸分析之栈上分配

前面一节已介绍,未逃逸的对象可以不必分配在堆上,而是分配在栈上。对象在栈上分配的场景即为上节介绍的逃逸分析的场景。
以如下代码为例,创建一百万个Persion对象,在堆设置不大的情况下,如果不打开逃逸分析,对象分配的堆上,当Eden满了后,肯定要发生Minor GC。

public class HeapDemo3 {
    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        for (int i=0; i<1000000; i++){
            createPersion();
        }
        long endTime = System.currentTimeMillis();
        long takeTime = endTime - startTime;
        System.out.println("分配对象花费时间:" + takeTime + "ms");
        Thread.sleep(600000);
    }
    public static void createPersion(){
        Persion persion = new Persion();
    }
}

设置虚机运行参数为 -Xms512M -Xmn512M -XX:-DoEscapeAnalysis -XX:+PrintGC,-XX:-DoEscapeAnalysis表示关闭逃逸分析。
运行HeapDemo3程序后,查看堆中persion对象个数,通过java visualVM中抽样器查看堆中对象个数如下,发现堆内存中有1000000个对象,说明关闭逃逸分析后,1000000个person对象全部分配到堆上了,当然如果堆设置的小的话,会发生GC。
运行HeapDemo3会输出分配对象花费时间:20ms,说明在堆上分配1000000个persion对象花费了20ms。
在这里插入图片描述


同样上述HeapDemo3代码,虚拟机启动时,打开逃逸分析,查看Persion对象分配情况。首先设置JVM参数为-Xms512M -Xmn512M -XX:+DoEscapeAnalysis -XX:+PrintGC,其中-XX:+DoEscapeAnalysis表示开启逃逸分析,如不设置,也是默认开启逃逸分析,执行HeapDemo3后,查看persion对象如下所示,发现内存中persion对象数量为89033个(是动态变化的),远远少于1000000个,并且还没有发生GC,因此可以说明persion对象有部分被优化到栈上,并随着栈的入栈和出栈消亡了。注意尽管所有persion对象都是未逃逸的,并不是所有persion对象都会优化到栈上,堆中也会分配部分。
执行HeapDemo3时,输出分配对象花费时间:9ms,表示创建1000000个persion对象花费了9ms,上面在堆 上分配1000000个persion对象时却需要20ms时间,因此栈上分配对象会更快。
在这里插入图片描述

4.8.3 逃逸分析之同步省略

例如一个被同步的对象经过分析只能被一个线程访问到,那么这个被同步的对象就可以考虑不同步,进而优化掉同步锁。由于线程同步大开销是相当大的,线程同步导致降低了并发性和性能。

在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步代码块所使用的对象锁是否只能够被一个线程访问而其他线程不能访问,对于这种情况,JIT编译器在编译该同步块的时候就会取消这部分代码块的同步。这样就能大大提高并发性和性能,这个取消同步的过程就叫做同步省略,也叫锁消除。

例如下面syncronizedDemo一段代码,在方法内部创建了persion对象,persion对象是未逃逸的,是线程私有的,因此此地加锁对程序执行没有任何意义,加锁去锁反而会降低并发性。JVM开启逃逸分析后,在运行期,JIT会去掉persion对象上锁,即锁消除,把字节码文件编译成机器指令,CPU在执行这段代码时是没有锁的。

public void syncronizedDemo(){
    Persion persion = new Persion();
    synchronized (persion){
        System.out.println("execute synchronized persion");
    }
}

注意:同步省略是发生在运行期,在编译期时仍会存在锁,就如上面的syncronizedDemo代码,在编译成class字节码文件时,锁对象还是存在的,后面在运行时才会锁消除。

4.8.4 逃逸分析之标量替换

标量替换也叫分离对象。堆是一块逻辑内存,在堆中分配的对象,逻辑内存都是联系的,但对应的物理内存不一定是连续的。因此如果一个对象分配在堆上时,就需要一个堆内存与物理内存的映射表,读取堆中对象时,才能从不连续的物理内存中中获取到存储的对象。
在java中有的对象不需要作为一个连续的内存结构存在也可以被访问,那么对象的这部分或者全部不需要作为连续内存结构访问的,也可以不存储的堆中,而是存储在栈中。

标量(Scalar)是指一个无法再分解成更小数据的数据。java中原始的数据类型就是标量(原始数据类型包括byte、short、int、long、boolean、char、float、double,注意不能是原始数据类型的包装类型,java中String也可以作为一个标量)。相对的,可以再分解的数据叫做聚合量(Aggregate),java中的对象就是聚合量,可以再分解成其它标量和聚合量。

标量替换就是,在JIT动态编译阶段,如果经过逃逸分析后,判断该对象不会被外界访问的话,那么经过JIT编译优化,就会把这个对象拆解成若干个成员变量来代替。

以如下代码为例,创建一百万个persion对象,persion对象没有逃逸,对比看对象是创建堆上还是栈上

public class HeapDemo4 {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i=0; i<1000000; i++){
            createPersion();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("分配对象花费时间:" + (endTime - startTime) + "ms");
    }
    public static void createPersion(){
        Persion persion = new Persion();
        persion.setAge(18);
        persion.setName("lzj");
    }
}

在启动该demo之前,首先设置JVM参数为:-Xms15M -Xmn15M -XX:+DoEscapeAnalysis -XX:-EliminateAllocations -XX:+PrintGC,其中-XX:+DoEscapeAnalysis表示开启逃逸分析,-XX:-EliminateAllocations表示不开启变量替换,启动测试,输出如下,发生了两次GC,说明persion对象分配到了堆上

[GC (Allocation Failure)  12288K->864K(14848K), 0.0023692 secs]
[GC (Allocation Failure)  13152K->896K(14848K), 0.0013879 secs]
分配对象花费时间:25ms

下面修改一下JVM参数为`-Xms15M -Xmn15M -XX:+DoEscapeAnalysis -XX:+EliminateAllocations -XX:+PrintGC`,其中`-XX:+EliminateAllocations`为打开标量替换,启动测试,输出如下,未发生GC,说明发生了标量替换,persion对象没有分配到堆上,而是persion中age和name分配到了栈上。创建同样数量的对象,分配到堆上用了25ms,而分配到了栈上却只用了12ms,说明标量替换对象分配在栈上速度更优。
分配对象花费时间:12ms

其实一旦打开了标量替换,上述createPersion方法就相当于如下形式,persion对象被分解成了name和age两个方法的局部变量,分配在了栈帧中的局部变量表中。

public static void createPersion(){
    String name = "lzj";
    int age = 18;
}

注意:标量替换同样时发生在JIT动态编译时期,如上述案例,即使打开标量替换并不是一百万个persion对象都会分配在栈上,只是部分优化到了栈上,依然会有部分分配到了堆上。

5. 方法区

5.1 方法区与堆栈之间的关系

方法区主要存放对象的一些特性,方法区在JDK8之后改名为元空间。方法区与堆都属于线程共享的,栈属于线程私有的。如下图所示,简单的创建一个persion对象,首先要加载Persion类的信息,类的信息存储在方法区中,而创建的对象Persion的对象在堆上开辟空间,对象的变量persion存储在栈上。
在这里插入图片描述
方法区(Method Area)在JVM启动的时候创建,方法区的逻辑内存是连续的,其物理内存与堆一样可以是不连续的。方法区的大小跟堆空间一样,可以选择固定大小,也可以选择扩展大小。
方法区的大小决定了可以加载多少类,如果JVM加载了太多的类导致方法溢出,JVM就会抛出OOM内存溢出错误。在JDK8以后抛出的错误为java.lang.OutOfMemoryError: Metaspace,而在JDK8以前抛出的错误为java.lang.OutOfMemoryError: PermGen space,永久代OOM,因为JDK8以前,方法区又叫永久代,是堆中的一部分,JDK8以后改为了元空间,并且从堆中独立出来,所以元空间不再属于堆中的一部分。


java 虚拟机中的方法区只是一种标准,不同的虚拟机在这种标准下的实现形式不一样。比如JRockit、J9虚拟机通过元空间实现方法区,Hot Spot虚拟机在JDK7之前(包括JDK7)都是通过永久代实现的元空间,而在JDK8之后都是通过元空间方式实现的方法区,元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用的本地内存,永久代则是使用虚拟机的内存,存在于堆中。当然永久代与元空间不只是名字不一致,二者的内部结构也不一致。

5.2 方法区参数设置

在JDK7以及JDK7以前

通过-XX:PermSize设置永久代的初始内存,默认是20.75M;
通过-XX:MaxPermSize设置永久代最大可分配的内存,32位机器默认是64M,64位机器默认为82M。

JDK8以及之后版本,使用-XX:MetaspaceSize-X:MaxMetspaceSize代替上述两参数。如不指定,这两个参数取默认值依赖于平台,在Window情况下,-XX:MetaspaceSize默认为21M,-X:MaxMetspaceSize默认为-1,没有上限,直到占满整个本地内存。


-XX:MetaspaceSize设置元空间的初始内存大小,即为初始高水位线,一旦元空间中加载的类越来越多,超过了这个初始高水位线,就会触发FULL GC卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线被重置。重置后的新的高水位线的大小取决于GC后释放了多少元空间,如果释放的空间不足,则会提高这个高水位线,但不会超过-X:MaxMetspaceSize,如果释放的空间过多,就会适当降低这个高水位线,即低于21M。

如果初始的高水位线设置过低,高水位线会发生多次调整情况,通过垃圾回收可以看多多次FULL GC,导致频繁的GC,一般建议-XX:MetaspaceSize设置一个相对较高的值。

5.3 方法区内部结构

如下图所示,字节码文件经过类的加载子系统加到到运行时数据区的方法区中,方法区主要存放类的信息以及运行时常量池(存放字符串常量)。而类的信息主要包括:类型信息、静态变量、JIT代码缓存、域信息(类变量)、方法信息(类方法)等。
在这里插入图片描述
1、类型信息
加载到方法区中的类包括:class类、interface接口、enum枚举、annotaiton注解。其中interface接口、enum枚举、annotaiton注解和clss类统称为java中的class类。方法区存放class类的类型信息主要包括:

类的全路径名;
类的直接父类的全路径名(对于interface或者object都没有直接父类);
类的修饰符,包括public、abstract、final等;
类实现的接口的列表。

2、域信息
类加载到方法区后,方法区中必须保存类的所有属性的相关信息以及声明顺序。域的相关信息即属性的相关信息,主要包括:属性的名称、属性类型、属性的修饰符(public、static、final、valatile、transient等)。

3、方法信息
方法区保存了类的所有方法信息以及方法声明的顺序,方法信息主要包括如下:

方法名称;
方法返回类型;
方法的参数数量以及类型;
方法的修饰符,包括public、private、protected、static、final、synchronized、native、abstract等。
方法的字节码、操作数栈、局部变量表及大小(abstract和native方法除外);
异常表(abstract和native方法除外),异常表信息包括每个异常处理的开始位置、结束位置、代码处理在程序计数器中的便宜地址、被捕获的异常类的常量池索引。

下面通过反编译java编译后的二进制文件,观察方法区的内部结构,以java代码如下所示

public class Persion {
    String name;
    int age;
    public Persion(String name, int age){
        this.name = name;
        this.age = age;
    }

    public void divide(Persion p){
        int age1 = this.age;
        int age2 = p.age;
        int division = 0;
        try {
            division = age1 / age2;
        } catch (ArithmeticException exception){
            System.out.println("除数不能为0");
        }
        int mod = age1 % age2;
        System.out.println("p1是p2年纪的" + division + "倍,并且还多" + mod + "岁");
    }

    public static void main(String[] args) {
        Persion p1 = new Persion("Tom", 25);
        Persion p2 = new Persion("Bob", 10);
        p1.divide(p2);
    }
}

该java代码编译后,生成Persion.class二进制文件,通过javap -v -p Persion.class反编译后的方法区结构如下所示,方法区中各结构已在文档中注释。

//1. 类信息
Classfile /C:/Users/lzj/IdeaProjects/MyDemo/out/production/MyDemo/com/lzj/runtime/method/Persion.class
  Last modified 2021-7-13; size 1505 bytes
  MD5 checksum 6259a081e58300706cda16c24274a2fc
  Compiled from "Persion.java"

//1.1 类的全路径名
public class com.lzj.runtime.method.Persion
  minor version: 0
  major version: 52
//1.2 类的修饰符
  flags: ACC_PUBLIC, ACC_SUPER

//2. 常量池  
Constant pool:
   #1 = Methodref          #21.#53        // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#54        // com/lzj/runtime/method/Persion.name:Ljava/lang/String;
   #3 = Fieldref           #16.#55        // com/lzj/runtime/method/Persion.age:I
   #4 = Class              #56            // java/lang/ArithmeticException
   #5 = Fieldref           #57.#58        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = String             #59            // 除数不能为0
   #7 = Methodref          #60.#61        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #8 = Class              #62            // java/lang/StringBuilder
   #9 = Methodref          #8.#53         // java/lang/StringBuilder."<init>":()V
  #10 = String             #63            // p1是p2年纪的
  #11 = Methodref          #8.#64         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #12 = Methodref          #8.#65         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #13 = String             #66            // 倍,并且还多
  #14 = String             #67            // 岁
  #15 = Methodref          #8.#68         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #16 = Class              #69            // com/lzj/runtime/method/Persion
  #17 = String             #70            // Tom
  #18 = Methodref          #16.#71        // com/lzj/runtime/method/Persion."<init>":(Ljava/lang/String;I)V
  #19 = String             #72            // Bob
  #20 = Methodref          #16.#73        // com/lzj/runtime/method/Persion.divide:(Lcom/lzj/runtime/method/Persion;)V
  #21 = Class              #74            // java/lang/Object
  #22 = Utf8               name
  #23 = Utf8               Ljava/lang/String;
  #24 = Utf8               age
  #25 = Utf8               I
  #26 = Utf8               <init>
  #27 = Utf8               (Ljava/lang/String;I)V
  #28 = Utf8               Code
  #29 = Utf8               LineNumberTable
  #30 = Utf8               LocalVariableTable
  #31 = Utf8               this
  #32 = Utf8               Lcom/lzj/runtime/method/Persion;
  #33 = Utf8               divide
  #34 = Utf8               (Lcom/lzj/runtime/method/Persion;)V
  #35 = Utf8               exception
  #36 = Utf8               Ljava/lang/ArithmeticException;
  #37 = Utf8               p
  #38 = Utf8               age1
  #39 = Utf8               age2
  #40 = Utf8               division
  #41 = Utf8               mod
  #42 = Utf8               StackMapTable
  #43 = Class              #69            // com/lzj/runtime/method/Persion
  #44 = Class              #56            // java/lang/ArithmeticException
  #45 = Utf8               main
  #46 = Utf8               ([Ljava/lang/String;)V
  #47 = Utf8               args
  #48 = Utf8               [Ljava/lang/String;
  #49 = Utf8               p1
  #50 = Utf8               p2
  #51 = Utf8               SourceFile
  #52 = Utf8               Persion.java
  #53 = NameAndType        #26:#75        // "<init>":()V
  #54 = NameAndType        #22:#23        // name:Ljava/lang/String;
  #55 = NameAndType        #24:#25        // age:I
  #56 = Utf8               java/lang/ArithmeticException
  #57 = Class              #76            // java/lang/System
  #58 = NameAndType        #77:#78        // out:Ljava/io/PrintStream;
  #59 = Utf8               除数不能为0
  #60 = Class              #79            // java/io/PrintStream
  #61 = NameAndType        #80:#81        // println:(Ljava/lang/String;)V
  #62 = Utf8               java/lang/StringBuilder
  #63 = Utf8               p1是p2年纪的
  #64 = NameAndType        #82:#83        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #65 = NameAndType        #82:#84        // append:(I)Ljava/lang/StringBuilder;
  #66 = Utf8               倍,并且还多
  #67 = Utf8               岁
  #68 = NameAndType        #85:#86        // toString:()Ljava/lang/String;
  #69 = Utf8               com/lzj/runtime/method/Persion
  #70 = Utf8               Tom
  #71 = NameAndType        #26:#27        // "<init>":(Ljava/lang/String;I)V
  #72 = Utf8               Bob
  #73 = NameAndType        #33:#34        // divide:(Lcom/lzj/runtime/method/Persion;)V
  #74 = Utf8               java/lang/Object
  #75 = Utf8               ()V
  #76 = Utf8               java/lang/System
  #77 = Utf8               out
  #78 = Utf8               Ljava/io/PrintStream;
  #79 = Utf8               java/io/PrintStream
  #80 = Utf8               println
  #81 = Utf8               (Ljava/lang/String;)V
  #82 = Utf8               append
  #83 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #84 = Utf8               (I)Ljava/lang/StringBuilder;
  #85 = Utf8               toString
  #86 = Utf8               ()Ljava/lang/String;
{
  //3. 域信息
  //3.1 属性的名称、类型、修饰符
  java.lang.String name;
    descriptor: Ljava/lang/String;
    flags:

  int age;
    descriptor: I
    flags:

  //4. 方法信息(构造器)
  //方法名称Persion,返回类型void,参数类型分别为String和int
  //修饰符为public,stack=2表示栈深度为2,locals表示局部变量表长度为3,args_size=3表示有3个入参(this, String, int)
  public com.lzj.runtime.method.Persion(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aload_1
         6: putfield      #2                  // Field name:Ljava/lang/String;
         9: aload_0
        10: iload_2
        11: putfield      #3                  // Field age:I
        14: return
      //字节码行号与java代码行号对应关系
      LineNumberTable:
        line 6: 0
        line 7: 4
        line 8: 9
        line 9: 14
      //局部变量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lcom/lzj/runtime/method/Persion;
            0      15     1  name   Ljava/lang/String;
            0      15     2   age   I

  public void divide(com.lzj.runtime.method.Persion);
    descriptor: (Lcom/lzj/runtime/method/Persion;)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=6, args_size=2
         0: aload_0
         1: getfield      #3                  // Field age:I
         4: istore_2
         5: aload_1
         6: getfield      #3                  // Field age:I
         9: istore_3
        10: iconst_0
        11: istore        4
        13: iload_2
        14: iload_3
        15: idiv
        16: istore        4
        18: goto          31
        21: astore        5
        23: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        26: ldc           #6                  // String 除数不能为0
        28: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        31: iload_2
        32: iload_3
        33: irem
        34: istore        5
        36: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        39: new           #8                  // class java/lang/StringBuilder
        42: dup
        43: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
        46: ldc           #10                 // String p1是p2年纪的
        48: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        51: iload         4
        53: invokevirtual #12                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        56: ldc           #13                 // String 倍,并且还多
        58: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        61: iload         5
        63: invokevirtual #12                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        66: ldc           #14                 // String 岁
        68: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        71: invokevirtual #15                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        74: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        77: return
      //异常表
      Exception table:
         from    to  target type
            13    18    21   Class java/lang/ArithmeticException
      LineNumberTable:
        line 12: 0
        line 13: 5
        line 14: 10
        line 16: 13
        line 19: 18
        line 17: 21
        line 18: 23
        line 20: 31
        line 21: 36
        line 22: 77
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           23       8     5 exception   Ljava/lang/ArithmeticException;
            0      78     0  this   Lcom/lzj/runtime/method/Persion;
            0      78     1     p   Lcom/lzj/runtime/method/Persion;
            5      73     2  age1   I
           10      68     3  age2   I
           13      65     4 division   I
           36      42     5   mod   I
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 21
          locals = [ class com/lzj/runtime/method/Persion, class com/lzj/runtime/method/Persion, int, int, int ]
          stack = [ class java/lang/ArithmeticException ]
        frame_type = 9 /* same */

  //方法信息,同上所述
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=3, args_size=1
         0: new           #16                 // class com/lzj/runtime/method/Persion
         3: dup
         4: ldc           #17                 // String Tom
         6: bipush        25
         8: invokespecial #18                 // Method "<init>":(Ljava/lang/String;I)V
        11: astore_1
        12: new           #16                 // class com/lzj/runtime/method/Persion
        15: dup
        16: ldc           #19                 // String Bob
        18: bipush        10
        20: invokespecial #18                 // Method "<init>":(Ljava/lang/String;I)V
        23: astore_2
        24: aload_1
        25: aload_2
        26: invokevirtual #20                 // Method divide:(Lcom/lzj/runtime/method/Persion;)V
        29: return
      LineNumberTable:
        line 25: 0
        line 26: 12
        line 27: 24
        line 28: 29
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      30     0  args   [Ljava/lang/String;
           12      18     1    p1   Lcom/lzj/runtime/method/Persion;
           24       6     2    p2   Lcom/lzj/runtime/method/Persion;
}

方法区与堆栈、程序计数器之间执行过程见2.4.6 虚拟机栈中指令执行过程分析

5.4 运行时常量池

1、常量池与运行时常量池关系
字节码文件中的Constant pool是常量池,在编译期确定的,字节码文件经过类的加载子系统加载到方法区中后就变成了运行时常量池。常量池中存放的都是符号的引用,而运行时常量池则是把符号引用转化为了具体地址的引用。比如5.3节常量池中的打印字符串类#79 = Utf8 java/io/PrintStream,在常量池中存放的是该形式,用#79符号表示 java/io/PrintStream的引用,等到了方法区中后,就会把符号转化为具体的java/io/PrintStream在内存中的地址供调用。

总结:常量池和运行时常量池中存放的内容都是一致的,只不过常量池是静态存放的,运行时常量池是动态存放的。但是运行时常量池可以比常量池存放更多的内容,程序运行时,动态的向运行时常量池中添加内容,而常量池是静态的,在编译期就确定的,动态添加的内容不会体现在常量池中。

2、为什么需要常量池或者运行时常量池
比如一个类A的正常运行需要import很多JDK的类或者用户自定义的类,假如A在编译的时候,把依赖的所有类都编译到A的字节码文件中,就会导致A的字节码非常庞大臃肿,那么对于一个工程中每一个字节码文件都会非常大。因此通过常量池,把A依赖的类通过符号进行引用,在运行时常量池中,再指向依赖类的具体地址,精简了字节码文件大小。

3、常量池
常量池中存放了各种字面量,还存放了对类型、域(字段属性)和方法的符号引用。那么对于运行时常量池则存放的是对这些内容的具体引用。
常量池可以看做一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

4、运行时常量池
常量池是字节码文件中一部分,运行时常量池(Runtime Constant Pool)是方法区的一部分。
JVM在加载类和接口到虚拟机后,就会创建对应类的运行时常量池,JVM会为每个加载的类维护一个运行时常量池,池中的数据像数组一样通过索引访问。
运行时常量池是动态的,不仅可以存储编译期就确定的数值字面量,也包括了运行时通过解析符号引用转化为对应类或者方法或者字段的引用,此时常量池中的符号地址就会转化为具体的真实地址。
如果运行时常量池所需的内存空间超过了方法区所能提供的最大值,JVM就会抛出OOM(OutOfMemoryError)异常。

5.5 方法区的演变

JDK8之后,HotSpot虚拟机取消了永久代,改为元空间。也只有HotSpot虚拟机曾经存在过永久代,其它虚拟机比如JRockit、J9等都不存在永久代的概念,只有元空间,只是每家的虚拟机对方法区的实现不一致而已。


对于HotSpot:

JDK6之前有永久代(permanent generation),静态变量存放在永久代上;
JDK7有永久代,但已经逐步去永久代 ,字符串常量池、静态变量从永久代移除,放到了堆空间中;
JDK8及之后,无永久代,类元信息保存在本地的元空间中,但字符串常量池、静态变量仍在堆空间中。

1. 永久代替换为元空间的好处

  • 永久代在堆内存中,占用虚拟机的内存,为永久代设置空间大小很难确定,因此当加载的类足够多时就容器发生permanent OOM。而元空间占用的是本地内存,不在占用虚拟机的内存,默认情况,元空间大小上限就是整个本地内存,因此可以加载更多的类;
  • 对永久代调优比较困难。

2. 字符串常量池从永久代调整到堆空间的好处
字符串常量池或者也叫字符串常量在JDK7之后,从永久代转移到了堆空间。因为永久代的回收效率很低,在执行FULL GC时才会回收永久代,而触发FULL GC的条件是老年代空间不足或者永久代空间不足时才会触发。如果字符串常量还是继续留在永久代的话,由于FULL GC的频率比较低,导致字符串常量回收的效率就比较低,但是在开发过程中一般会创建大量的字符串,大量的字符串就会占据永久代很大空间,导致FULL GC的频率增加,FULL GC会发生STD,暂停用户线程,导致程序执行效率低下,因此字符串常量放到堆中,通过minor GC可以及时回收内存。由此可见,字符串常量存放在堆中更优,所以到了JDK8,永久代变成了元空间后,字符串常量依然在堆中。

3. 静态变量从永久代调整到堆空间的好处
静态变量在类的整个生命周期都有效,为类所拥有,即使类没有初始化,也可以通过类调用静态变量。静态变量从永久代移到堆空间好处同字符串常量移到堆空间好处一致,及时内存回收可以收回不用的静态变量,因此静态变量更适合放到堆上。当然静态变量也不可以转移到栈上存储,因为栈是由每个栈帧组成,随着每个方法执行结束对应的栈帧就会回收,不适合存放长期有效的静态变量。因此静态变量在元空间时代继续存放到堆上。


例如如下代码,静态变量bytes在JDK8就是分配在堆中的,但是不管什么版本的虚拟机,new出来的byte的数组对象的内容是存储在堆上的,只是对象的变量bytes在JDK8是分配在堆上,在JDK6之前都是存储在永久代上的。

public class Demo1 {
    public static byte[] bytes = new byte[1024*1024*50];    //50M
}

5.6 方法区的垃圾回收

不同的JVM虚拟机厂商对方法区的实现不同,有的虚拟机在方法区进行垃圾回收,有的虚拟机不在方法区进行垃圾回收。一般方法区的垃圾回收效果比较差,尤其是类型的卸载,条件苛刻,但是方法区的回收又有必要,否则严重会导致内存泄漏。
方法区的垃圾回收主要集中在两方面:运行时常量池中废弃常量、不再使用的类型。

对于运行时常量池中废弃常量回收:只要运行时常量池中的常量没有在任何地方引用,都可以被回收;
对于类型信息回收:需要同时满足下面3个条件才会进行类型信息的回收

  1. 该类的所有实例均已被回收,java堆中不存在该类以及任何派生子类的实例;
  2. 加载该类的加载器已经被回收,这个条件是经过精心设计的可替换类加载器场景,如OSGI、JSP的重加载等,否则该条件很难满足;
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    java虚拟机对满足上述3个条件的类允许卸载,但是否真的被卸载还需要虚拟机设置-Xnoclassgc参数进行控制,还可以通过-verbose:class以及-XX:+TraceClass-Loading, -XX:+TraceClassUnLoading查看类的加载信息。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值