安卓

十九、JVM内存管理分析

2021-06-14  本文已影响0人  大虾啊啊啊

一、JVM的运行过程

二、运行时数据区

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


image.png

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

1、线程共享数据区

1.1、程序计数器

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

1.2、虚拟机栈

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

1.2.1、栈帧

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


image.png

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


image.png
我们看到一个虚拟机栈会有多个栈帧,每一个栈帧入栈出栈的过程就是一个方法执行的过程。栈帧中的局部变量表、操作数栈、动态链接、完成出口存放了方法执行过程的数据。下面我们来演示一个方法的执行对应字节码指令的执行。
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
}

最后方法执行完毕之后,栈帧就从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、小结

栈、堆、方法区

线程独享和共享

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

空间大小

栈的内存要远远小于堆的内存,栈的深度也是有限制,可能发生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)
上一篇 下一篇

猜你喜欢

热点阅读