美文网首页
深入理解jvm——运行时数据区

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

作者: Peakmain | 来源:发表于2021-06-07 10:07 被阅读0次

    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
    
    

    相关文章

      网友评论

          本文标题:深入理解jvm——运行时数据区

          本文链接:https://www.haomeiwen.com/subject/ikezsltx.html