美文网首页安卓
十九、JVM内存管理分析

十九、JVM内存管理分析

作者: 大虾啊啊啊 | 来源:发表于2021-06-14 11:46 被阅读0次

    一、JVM的运行过程

    • JVM虚拟机我们可以把它当做一台虚拟出来的计算机,也有自己的内存管理如:堆、栈、方法区等。
    • JVM的作用是将字节码翻译成不同操作系统可以识别的机器码执行。
      运行过程如下图:


      image.png

      (1)我们写的一个JAVA程序首先通过JDK的中JAVAC工具进行编译,编译成了.class文件,也就是我们说的字节码。
      (2)JAVA类加载器(ClassLoader)将字节码载入到JVM的运行时数据区,也就是JVM中的一块内存区域。
      (3)通过执行引擎调用操作系统接口解释执行或者JIT执行。

    • 解释执行指的是JVM通过加载到的字节码进行翻译执行
    • JIT指的是将热点代码直接翻译成机器码保存下来,方便下次直接执行。
      区别是:前者启动快,但是运行速度慢,因为每次都要边解释边执行。而后者启动速度慢,但是执行速度快,因为需要将热点代码翻译成机器码保存下来,但是下次执行到热点代码的时候直接通过执行之前保存的机器码,因此速度比较快。
    • 一般虚拟机是通过两种方式混合使用。

    二、运行时数据区

    字节码通过类加载器加载到了JVM运行时数据区,而运行时数据区又将内存划分了不同的区域。不同的身份将进入到不同的区域。如下图:


    image.png

    运行时数据区主要分为两大块:线程共享数据区和线程隔离数据区
    其中右边白色部分包括虚拟机栈、本地方法栈、程序计数器是线程隔离数据区。左边灰色区域方法区、堆是线程共享数据区。

    • 线程隔离数据区
      指的是每一个线程都拥有自己的一块内存区域,就比如有两个线程A和B,他们各自拥有自己的虚拟机栈、本地方法栈、程序计数器。
    • 线程共享区
      指的是线程共享的区域,每个线程都共享了方法区和堆

    1、线程共享数据区

    1.1、程序计数器

    指向了当前线程正在执行的字节码指令地址,换句话说就是记录当前字节码指令执行的位置。因为在操作系统中由于时间片轮转机制,当前的线程可能指令还没执行完就被切出去了,因此要通过程序计数器记录正在执行的位置,当线程重新获得时间片之后,在原来的位置继续执行。

    1.2、虚拟机栈

    存储当前线程运行方法所需要的数据、指令、返回地址。我们可以理解成一个线程就拥有一个虚拟机栈,而线程中的每一个方法就是一个栈帧。

    1.2.1、栈帧

    栈帧中又包含了:局部变量表、操作数栈、动态链接、 完成出口(返回地址)
    如下图


    image.png

    线程中一个方法的执行就是在这些身份相互配合执行。我们将线程隔离数据区进行更细的划分如下图


    image.png
    我们看到一个虚拟机栈会有多个栈帧,每一个栈帧入栈出栈的过程就是一个方法执行的过程。栈帧中的局部变量表、操作数栈、动态链接、完成出口存放了方法执行过程的数据。下面我们来演示一个方法的执行对应字节码指令的执行。
    • JAVA源代码
    package com.it.test;
    
    public class Test {
        public static void main(String[] args) {
            Test test = new Test();
            test.function();
        }
    
        public int function() {
            int a = 1;
            int b = 3;
            int c = (a + b) * 10;
            return c;
        }
    }
    
    
    • 反汇编后的字节码
    
    D:\app_work_space\JavaHighSets\src\com\it\test>javap -c Test.class
    Compiled from "Test.java"
    public class com.it.test.Test {
      public com.it.test.Test();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: new           #2                  // class com/it/test/Test
           3: dup
           4: invokespecial #3                  // Method "<init>":()V
           7: astore_1
           8: aload_1
           9: invokevirtual #4                  // Method function:()I
          12: pop
          13: return
    
      public int function();
        Code:
           0: iconst_1
           1: istore_1
           2: iconst_3
           3: istore_2
           4: iload_1
           5: iload_2
           6: iadd
           7: bipush        10
           9: imul
          10: istore_3
          11: iload_3
          12: ireturn
    }
    
    
    

    我们将源代码和反汇编后的代码合在一起进行对比,然后分析function方法的执行如何在虚拟机栈和程序计数器中体现的。

    //源代码
      public int function() {
            int a = 1;
            int b = 3;
            int c = (a + b) * 10;
            return c;
        }
    //反汇编后的代码
      public int function();
        Code:
           0: iconst_1
           1: istore_1
           2: iconst_3
           3: istore_2
           4: iload_1
           5: iload_2
           6: iadd
           7: bipush        10
           9: imul
          10: istore_3
          11: iload_3
          12: ireturn
    }
    
    • code就是代码的意思
    • 0、1、2...等等行号,我们可以认为是字节码指令执行到的位置,程序计数器存放的就是这些数据的地址,记录当前方法执行到的位置,因此程序计数器存放的数据较小,不会因为内存不足产生OOM异常。
      0: iconst_1
      将int 类型 1压入到操作数栈
      1:istore_1
      将操作数栈栈顶出栈,存入局部变量表下标为1的位置
      以上两个步骤完成了int a =1;
      2: iconst_3
      将int类型 3压入到操作数栈
      3:istore_2
      将操作数栈栈顶出栈,存入局部变量表下标为2的位置
      以上两个步骤完成了int b = 3;
      4:iload_1
      将局部变量表下标为1的位置的数据压入操作数栈
      5:iload_2、
      将局部变量表下标为2的位置的数据压入操作数栈
      6: iadd
      三部曲,
      (1)将栈顶的两个数据出栈
      (2)相加
      (3)将结果压入到操作数栈
      7: bipush
      将int类型10压入到操作数栈
      9:
      imul三部曲
      (1)将栈顶两个数据出栈
      (2)相乘
      (3)结果压入到操作数栈
      10:istore_3
      将栈顶数据存入到局部变量表下标为3的位置
      11: iload_3
      将局部变量表下标为3的位置的数据存到操作数栈,作为返回值
      以上就完成了int c = (a + b) * 10
      12: ireturn
      将操作数栈栈顶的数据出栈返回
      完成了rerturn z

    最后方法执行完毕之后,栈帧就从Java虚拟机栈中出栈

    1.3、Java虚拟机栈小结

    Java虚拟机栈存放的是一个方法的执行过程、而每一个方法对应一个栈帧,每一个方法的执行过程就是栈帧入栈出栈的过程,而栈帧中又包含了局部变量表、操作数栈、动态链接、完成出口(返回地址)。这四个角色用于存放执行过程的数据。

    (1)局部变量表

    存方法内部的局部变量基本数据类型、以及局部对象类型变量的引用

    (2)操作数栈

    完成方法中数据的操作

    (3)动态链接

    Java语言中会有多肽,例如以下代码,Student和Teacher都继承了User,执行方法eat,在编译期间是没法知道是执行Student的eat方法还是Teacher的eat方法,所以在方法运行期间,Java虚拟机栈的栈帧中存放一个动态链接来确定执行谁的eat方法

    public class User  {
    
        public void eat(){
        }
        public static void main(String[] args) {
            User user = new Student();
            user.eat();
            user = new Teacher();
            user.eat();
        }
    }
    
    
    (4) 完成出口(返回地址)

    例如以下代码, 在main方法中执行了function方法,当function放执行完毕之后,要回到main方法中继续执行,那具体要回到的地方是哪里呢?就是我们 System.out.println("你好。。。");这一行这里作为出口,所以这个出口就是存放在栈帧中的“完成出口”区域。

    package com.it.test;
    
    public class Test {
        public static void main(String[] args) {
            Test test = new Test();
            test.function();
            System.out.println("你好。。。");
            System.out.println("哈哈。。。");
        }
    
        public int function() {
            int a = 1;
            int b = 3;
            int c = (a + b) * 10;
            return c;
        }
    }
    
    

    1.4、本地方法栈

    以上我们说到了线程隔离区中的程序计数器、虚拟机栈。下面我们来了解一下最后一个本地方法栈。
    我们知道虚拟机栈存的是Java方法的执行过程所需的指令、数据、返回地址等,每一个方法的执行就是一个栈帧入栈出栈的过程。而本地方法的执行则对应了我们的本地方法栈。例如我们的hashCode方法就是本地方法。

       public native int hashCode();
    

    当JVM创建的线程调用了native方法之后,JVM不会为其在虚拟机栈中创建栈帧,而是简单的动态链接并直接调用native方法。
    一般的虚拟机,Java虚拟机栈和本地方法栈都是合在一块区域。例如我们的HotSpot。

    2、线程隔离数据区

    在JVM运行时数据区中的线程隔离数据区主要包含了方法区、堆。其中我们说的常量池也是在方法区中。

    2.1、方法区

    方法区主要用于存放以下数据
    (1)类的信息
    我们知道当我们的Java源代码被编译成class之后,通过类加载器ClassLoader将class加载我们的运行时数据区中的方法区。
    (2)常量
    (3)静态变量
    (4)即时编译后的代码

    2.2、堆

    堆中则存放了对象实例(几乎所有)、数组。
    线程数据共享数据区之所以要分为方法区和堆,是因为方法区主要存放一些类、静态变量、常量等这些是比较难回收的。而在堆中存放的是对象、经常要动态创建,这样方便于回收。

    2.2.1、Java堆大小参数的设置

    -Xmx 堆内存可被分配的最大上限
    -Xms 堆内存初始化分配的大大小

    2.2.2、深刻理解运行时数据区

    下面我们通过代码例子来深刻理解JVM运行时数据区

    • 代码例子
    package cn.enjoyedu.concurrent.cas;
    
    public class JVMObject {
        private final static String MAN_TYPE = "man";
        private  static String WOMAN_TYPE = "woman";
    
        public static void main(String[] args) {
            Teacher t1 = new Teacher();
            t1.setName("小明");
            t1.setSex(MAN_TYPE);
            //15次垃圾回收
            for (int i = 0; i <15 ; i++) {
                System.gc();
            }
            Teacher t2 = new Teacher();
            t2.setName("小红");
            t2.setSex(WOMAN_TYPE);
            try {
                Thread.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
        }
    
        static class Teacher {
            String name;
            String sex;
    
            public String getSex() {
                return sex;
            }
    
            public void setSex(String sex) {
                this.sex = sex;
            }
    
            public String getName() {
                return name;
            }
            public void setName(String name) {
                this.name = name;
            }
        }
    
    
    }
    
    
    
    • 运行时数据区结构图
    image.png

    我们通过代码分析图中数据的走向:
    当我们以上的代码运行的时候,
    (1)JVM向操作系统申请内存,然后根据堆、栈等各自设置的参数给它们分配内存
    (2)ClassLoader类加载器将我们的Java源代码编译后的JVMObject.class和Teacher.class加载到我们的运行时数据区的方法区


    image.png

    (3)运行时数据区的数据再进行拆分,将我们静态变量、常量存放到方法区


    image.png
    (4)在main线程执行main方法,为main线程创建虚拟机栈、将main方法栈帧压入到虚拟机栈
    image.png

    (5)main方法创建t1对象实例,存放到堆中。它的引用存放到栈帧中的局部变量表


    image.png

    (6)t1对象设置属性,这里是调用了方法,就是栈帧的不断入栈,出栈的过程
    (7)for循环15次进行GC
    垃圾回收器在堆中回收15次,t1对象从堆中的新生代eden区,转到了老年代(Tenued)
    (8)创建t2对象,和t1一样,一开始也存放到了堆中的新生代,引用存放到了局部变量表。
    最后我们的运行时数据区的图如下:

    image.png

    3、小结

    栈、堆、方法区

    • 栈主要分为虚拟机栈和本地方法栈。本地方法栈主要用于动态连接我们的native方法,而Java虚拟机栈以栈帧的方式存储方法的调用过程,并存储了方法中的基本数据类型变量、对象的引用变量。当变量出了方法的作用域就会自动释放。一般的Java虚拟机中Java虚拟机栈和本地方法栈都是合二为一,我们简称为栈。

    • 而堆主要用来存放Java的对象实例,无论是成员变量、局部变量还是类变量他们的对象实例都是存放在堆中

    • 方法区主要用于存放class类信息、静态变量、常量。我们说的常量池也就是在方法区中。

    线程独享和共享

    栈和程序计数器是属于线程独享数据区,每一个线程都拥有自己的一个栈和程序计数器。
    堆和方法区是属于线程共享数据区,线程可以共享访问这些数据区域。

    空间大小

    栈的内存要远远小于堆的内存,栈的深度也是有限制,可能发生StackOverFlowError。

    栈溢出

    例如下面的代码:
    我们写了一个死的递归执行方法,由于一直递归执行方法,栈帧就会一直入栈。最终导致栈溢出java.lang.StackOverflowError。栈的具体深度是根据默认的配置以及自定义配置栈的大小。

    package cn.enjoyedu.concurrent.cas;
    
    public class MyTest {
        public static void main(String[] args) {
            test();
        }
    
        private static void test() {
            test();
        }
    }
    
    
    Exception in thread "main" java.lang.StackOverflowError
        at cn.enjoyedu.concurrent.cas.MyTest.test(MyTest.java:9)
    

    堆溢出

    堆内存溢出值的是申请的内存超出了堆中的最大可分配内存,例如下面代码,申请的内存超出了堆最大分配内存,所以抛出了内存溢出异常OutOfMemoryError。

    package cn.enjoyedu.concurrent.cas;
    
    public class MyTest {
        public static void main(String[] args) {
            String[] s = new String[Integer.MAX_VALUE];
        }
    }
    
    
    
    
    
    Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
        at cn.enjoyedu.concurrent.cas.MyTest.main(MyTest.java:5)
    

    相关文章

      网友评论

        本文标题:十九、JVM内存管理分析

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