深入理解jvm——运行时数据区

JVM内存模型

java内存模型.png

程序计数器

  • 也称为PC寄存器,每个线程都有一个程序计数器,线程私有
  • 实际就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将执行的指令代码),由执行引擎读取下一条指令
  • 非常小的内存空间,几乎可以忽略不计.是唯一一个在JVM规范中没有OOM的区域

本地方法栈

  • 区别去java虚拟机的是,java虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到native方法服务
  • 也会有StackOverflowError和OutOfMemoryError异常

方法区

  • 方法区是所有线程共享的
  • 所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。
  • 所有定义的方法信息都保存在该区域,此区属于共享区间
  • 静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中
  • 实例变量存在堆中,和方法区无关

虚拟机栈

  • 线程私有,声明周期和线程一致
  • 每个方法在执行的时候都会创建一个栈帧
  • 每个栈帧都有局部变量表,操作数栈,动态链接,方法出口
局部变量表

局部变量表是一组变量值存储空间,用于存储方法参数和方法内部定义的局部变量

操作数栈
  • 也称为操作栈,是一个后进先出的栈
  • 默认方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈写入和提取内容,也就是出栈/入栈操作
  • 举例:整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了int类型的数值,当执行这个命令的时候,会将两个int值出栈并相加,然后将结果入栈
public class Test {
    public static void main(String[] args) {
       int a=0;
       int b=10;
       int c=a+b;
    }
}

javap -v Test.class查看字节码

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_0 //将0压入操作数栈
         1: istore_1 //将操作数栈中的0放到局部变量表下标1的位置
         2: bipush        10     //将10压入操作数栈
         4: istore_2 //将操作数栈栈顶元素10放到局部变量下标2的位置
         5: iload_1 //将局部变量表下标1位置的元素重新放到操作数栈栈顶
         6: iload_2 //将局部变量表下标2位置的元素重新放到操作数栈栈顶
         7: iadd //弹出操作数栈顶的两个元素进行相加并放到栈顶
         8: istore_3 //将局部变量表栈顶的元素放到局部变量下标3的位置
         9: return
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 5
        line 9: 9
      LocalVariableTable://局部变量表
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;
            2       8     1     a   I
            5       5     2     b   I
            9       1     3     c   I
  • 在概念模型中,两个栈帧作为虚拟机的元素,是完全相互独立的。但在大多数虚拟机实现里都会做一些处理,令两个栈帧出现一部分重叠


    image.png
动态链接
  • 每一个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态链接
  • 这个引用是一个符号引用,不是方法实际运行的入口地址,需要动态找到具体方法的入口
方法返回地址
  • 正常完成出口:方法正确执行,执行引擎遇到方法返回的指令,回到上层的方法调用者
  • 异常完成出口:方法执行过程中发生了异常,并且没有处理异常,这样就不会给上层调用者产生任何返回值
  • 方法正常退出,将会放回程序结束并将值给上层方法,经过调整之后以指向方法调用指令后面的一条指令,继续执行上层方法

  • 线程共享,主要存放对象实例和数组。
  • 从内存回收的角度来看,由于现在收集器基本上都采用分代收集算法,所以对空间还可以细分为:新生代(年轻代),老年代(年老代).再 细致一点,可以分为 Eden 空间,From Survivor 空间, To Survivor 空间.
  • GC主要就是管理堆空间,对分代GC来说,堆也是分代的(只是一种思想)
  • 堆的优点:运行期动态分配内存大小,自动进行垃圾回收
  • 堆的缺点:效率相对较慢
堆的结构
image.png
  • 新生代用来存放新分配的对象;新生代经过垃圾回收,没有回收掉的对象,被复制到老年代
  • 老年代存储对象比新生代存储对象的年龄大得多
  • 老年代存储一些大对象
  • 整个堆大小=新生代+老年代
  • 新生代=Eden+存活区
对象的内存布局
  • (以HotSpot虚拟机为例来说明),分为:对象头、实例数据和对齐填充
  • 对象头包含两个部分
    • Mark Word:存储对象自身的运行数据,如:HashCode、GC分代年龄、锁状态标志等
    • 类型指针:对象指向它的类元数据的指针
  • 实例数据
    • 真正存放对象实例数据的地方
  • 对齐填充
    • 这部分不一定存在,仅仅是占位符。

栈、堆方法区交互关系

image.png

字节码

栈帧概述
  • 栈帧是用于支持JVM进行方法调用和方法执行的数据结构
  • 栈帧随着方法调用而创建,随着方法结束而销毁
  • 栈帧里面存储了方法的局部变量表,操作数栈,动态链接,方法返回地址等信息

局部变量表

  • 用来存放方法参数和方法内部定义的局部变量的存储空间
    • 以变量槽slot为单位,目前一个slot存放32位以内的数据类型
    • 对64位的数据占2个slot
    • 对于实例方法,第0位slot存放的是this,然后从1到n,依次分配给参数列表(static没有this)
    • 然后根据方法体内部定义的变量顺序和作用域分配slot
public class Test {
   public  int add(int a,int b){
       int c=a+b;
       return a+b+c;
   }

    public static void main(String[] args) {
        new Test().add(1,3);
    }
}

字节码


image.png

将add方法改成static之后查看字节码


image.png
  • slot是复用的,以节省栈帧的空间,这种设计可能会影响到系统的垃圾收集行为
    public static void main(String[] args) {
        {
            byte[] bs = new byte[1024 * 1024 * 3];
        }
          int a=10;
    }
image.png
操作数栈
  • 用来存放方法运行期间,各个指令操作的数据
    • 操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配
public class Test {
    public int add(int a, int b) {
        int c = a + b;
        return a + b + c;
    }

    public static void main(String[] args) {
        new Test().add(1, 2);
    }
}

只看add和main方法的字节码

  public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=3
         0: iload_1 //局部变量表下标1的值加入操作数栈栈顶
         1: iload_2//局部变量表下标2的值加入操作数栈栈顶
         2: iadd//弹出操作数栈的2个元素相加并加值压入操作数栈顶
         3: istore_3//将返回的值赋予局部变量表下标3的位置
         4: iload_1//局部变量表下标1的值加入操作数栈栈顶
         5: iload_2//局部变量表下标2的值加入操作数栈栈顶
         6: iadd//弹出操作数栈的2个元素相加并加值压入操作数栈顶
         7: iload_3//局部变量表下标3的值加入操作数栈栈顶
         8: iadd//弹出操作数栈的2个元素相加并加值压入操作数栈顶
         9: ireturn//结果返回
      LineNumberTable:
        line 5: 0
        line 6: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/peakmain/jvm/Test;
            0      10     1     a   I
            0      10     2     b   I
            4       6     3     c   I
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: new           #2                  //新建一个 class com/peakmain/jvm/Test
         3: dup
         4: invokespecial #3                  // 调用初始化
         7: iconst_1//将常量1压入操作数栈
         8: iconst_2//将常量2压入操作数栈
         9: invokevirtual #4                  // 调用方法的add方法
        12: pop//弹出操作数栈栈顶元素
        13: return
      LineNumberTable:
        line 10: 0
        line 11: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  args   [Ljava/lang/String;

分析:
main函数

  • iconst_1:将常量1压入操作数栈


    image.png
  • iconst_2:将常量2压入操作数栈


    image.png

    add方法:

  • iload_1 :局部变量表下标1的值加入操作数栈栈顶
  • iload_2:局部变量表下标2的值加入操作数栈栈顶


    image.png
  • iadd:弹出操作数栈的2个元素相加并加值压入操作数栈顶


    image.png
  • istore_3:将返回的值赋予局部变量表下标3的位置


    image.png
  • iload_1:局部变量表下标1的值加入操作数栈栈顶


    image.png
  • iload_2:局部变量表下标2的值加入操作数栈栈顶


    image.png
  • iadd:弹出操作数栈的2个元素相加并加值压入操作数栈顶


    image.png
  • iload_3:局部变量表下标3的值加入操作数栈栈顶


    image.png
  • iadd//弹出操作数栈的2个元素相加并加值压入操作数栈顶


    image.png
  • ireturn:结果返回

分派

  • 分派:又分为静态分派和动态分派
    • 1:静态分派:所有依赖静态类型来定位方法执行版本的分派方式,比如:重载方法
    • 2:动态分派:根据运行期的实际类型来定位方法执行版本的分派方式,比如:覆盖方法

java中堆栈的应用

1.栈和堆都是用来存放数据的地方,与c++不同的是,java自动管理堆和栈,程序员不能直接地设置堆或栈
2.栈的优势:存取速度快,缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
堆的优势:可以动态地分配内存,java的垃圾回收器会自动收走这些不再使用的数据。缺点:由于要运行时动态地分配内存,存取速度较慢
3.java数据类型有两种,一种是基本数据类型还有一种是包装性数据类型

public class Test {

    public static void main(String[] args) {
        int a=10;
        Integer b=10;
        Integer c=Integer.valueOf(10);
        System.out.println(a==b);
    }
}

查看字节码

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=4, args_size=1
         0: bipush        10 //10压入操作数栈
         2: istore_1          //放到局部变量表1的位置
         3: bipush        10  //10压入操作数栈
         5: invokestatic  #2                  // 实际调用的Integer.valueof方法 Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         8: astore_2        //放到局部变量表2的位置
         9: bipush        10
        11: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 实际和字节码5一模一样
        14: astore_3
         //调用system.out.println
        15: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_1       //局部变量表下标1的位置重新压入操作数栈
        19: aload_2        //局部变量表下标2的位置重新压入操作数栈
        20: invokevirtual #4                  //调用的是Integer.intValue    Method java/lang/Integer.intValue:()I
        23: if_icmpne     30  //如果两个值不相等则进行跳转,跳转到字节码30的位置
        26: iconst_1         //相等则1压入操作数栈
        27: goto          31  //跳转到字节码31的位置
        30: iconst_0          //不相等0压入操作数栈
        31: invokevirtual #5                  // Method java/io/PrintStream.println:(Z)V
        34: return
      LineNumberTable:
        line 7: 0
        line 8: 3
        line 9: 9
        line 10: 15
        line 11: 34
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      35     0  args   [Ljava/lang/String;
            3      32     1     a   I
            9      26     2     b   Ljava/lang/Integer;
           15      20     3     c   Ljava/lang/Integer;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 30
          locals = [ class "[Ljava/lang/String;", int, class java/lang/Integer, class java/lang/Integer ]
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class "[Ljava/lang/String;", int, class java/lang/Integer, class java/lang/Integer ]
          stack = [ class java/io/PrintStream, int ]
  • 基本类型(primitive types), 共有 8 种,即 int, short, long, byte, float, double, boolean, char(注意, 并没有 string 的基本类型)。都存在于栈中
  • 另一种是包装类数据,如 Integer, String, Double 等将相应的基本数据类型包装起来的类。这些类数据全部 存在于堆中,Java 用 new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占 用更多的时间
  1. String 是一个特殊的包装类数据
public class Test {

    public static void main(String[] args) {
        String str = "abc";
    }
}

我们看下上面代码的字节码


image.png
  • ldc实际是将常量池的字符串变量abc压入操作数栈
  • astore将操作数栈栈顶放到局部变量表1的位置

这里我们就会发现String str="abc"并没有new()来创建实例,而是创建一个str对象指向常量池abc对象
所以我们很容易知道下面这行代码返回的是true

public class Test {

    public static void main(String[] args) {
        String str = "abc";
        String str1 = "abc";
        System.out.println(str==str1);
    }
}
equal和==的区别
public class Test {

    public static void main(String[] args) {
        String str1 = new String("abc");
        String str2 = "abc";
        System.out.println(str1 == str2);
        System.out.println(str1 .equals(str2));
    }
}

查看字节码


image.png
switch和if-else哪个性能高

switch

public class Test {
    public static void main(String args[]) {
        char grade = 'C';
        switch (grade) {
            case 'A':
                System.out.println("优秀");
                break;
            case 'B':
            case 'C':
                System.out.println("良好");
                break;
            case 'D':
                System.out.println("及格");
                break;
            case 'F':
                System.out.println("你需要再努力努力");
                break;
            default:
                System.out.println("未知等级");
        }
    }
}

转成对应的字节码


image.png

我们会发现一共9条字节码指令

再来看下if-else

public class Test {
    public static void main(String args[]) {
        char grade = 'C';
       if(grade=='A'){
           System.out.println("优秀");
       }else  if(grade=='B'||grade=='C'){
           System.out.println("良好");
       }else if(grade=='D'){
           System.out.println("及格");
       }else if(grade=='F'){
           System.out.println("你需要再努力努力");
       }else{
           System.out.println("未知等级");
       }
    }
}
image.png

我们发现一共需要13条字节码指令

a++和++a的区别

a++

public class Test {
    public static void main(String args[]) {
        int a=1;
        for (int j=0;j<10;j++){
            a=a++;
            System.out.println(a);
        }
        System.out.println(a);
    }
}

字节码分析

  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: iconst_1 //1压入操作数栈
         1: istore_1 //放到局部变量表1的位置
         2: iconst_0 //常量0压入操作数栈中
         3: istore_2//放到局部变量表2的位置
         4: iload_2//加载局部变量表2的值放到操作数栈
         5: bipush        10 //10压入操作数栈
         7: if_icmpge     21    //比较两个操作数栈的值 当大于等于0的时候进行跳转
        10: iload_1        //局部变量表1的数加载到操作数栈中(这里就是1)
        11: iinc          1, 1 //局部变量表1位置数据+1,注意只是局部变量表+1,操作数栈还是1
        14: istore_1           //操作数栈的值放到局部变量表1的位置   
        15: iinc          2, 1 //局部变量表2(也就是j)进行+1
        18: goto          4      //回到字节码4的位置
        21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: iload_1
        25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        28: return
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 10
        line 6: 15
        line 9: 21
        line 10: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            4      17     2     j   I
            0      29     0  args   [Ljava/lang/String;
            2      27     1     a   I

++a

public class Test {
    public static void main(String args[]) {
        int a=1;
        for (int j=0;j<10;j++){
            a=++a;
        }
        System.out.println(a);
    }
}

字节码分析

  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: iconst_1 //1压入操作数栈
         1: istore_1 //放到局部变量表1的位置
         2: iconst_0 //常量0压入操作数栈中
         3: istore_2//放到局部变量表2的位置
         4: iload_2//加载局部变量表2的值放到操作数栈
         5: bipush        10 //10压入操作数栈
         7: if_icmpge     21    //比较两个操作数栈的值 当大于等于0的时候进行跳转
        10: iinc          1, 1 //局部变量表1位置数据+1,注意只是局部变量表+1,操作数栈还是1
        13: iload_1     //局部变量表1位置的值也就是此时的2压入操作数栈
        14: istore_1     //再将操作数栈的值2设置给局部变量表下标为1位置
        15: iinc          2, 1 //j+1
        18: goto          4
        21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: iload_1
        25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        28: return
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 10
        line 6: 15
        line 9: 21
        line 10: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            4      17     2     j   I
            0      29     0  args   [Ljava/lang/String;
            2      27     1     a   I
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 4
          locals = [ int, int ]
        frame_type = 250 /* chop */
          offset_delta = 16

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容