美文网首页安卓
十九、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内存管理分析

    一、JVM的运行过程 JVM虚拟机我们可以把它当做一台虚拟出来的计算机,也有自己的内存管理如:堆、栈、方法区等。 ...

  • 初见JVM内存区域

    初见JVM内存区域 JVM一个重要的机制就是自动内存管理机制,为了深入理解JVM的内存管理机制,了解JVM的内存...

  • JVM内存结构分析

    JVM内存结构分析 对于Java程序员来说,内存是由JVM自动管理的,所以一旦出现内存泄漏或溢出的问题,不了解JV...

  • 【问答】补充

    Java JVM如何管理内存的? Java中内存管理是JVM自动进行的,创建对象或者变量时JVM会自动分配内存,当...

  • Android性能优化-内存泄漏的几个案例

    JVM内存管理 Java采用GC进行内存管理。深入的JVM内存管理知识,推荐《深入理解Java虚拟机》。 关于内存...

  • 【内存】

    性能优化->虚拟机原理 怎么分析堆内存 内存泄露和内存溢出的原因 怎么做内存管理 JVM类加载机制Java内存区域...

  • [JVM系列]JVM内存管理详解

    JVM内存管理详解

  • Java开发

    JVM 内存溢出实例 - 实战 JVM(二) 介绍 JVM 内存溢出产生情况分析Java - 注解详解 详细介绍 ...

  • 2020互联网Java后端面试专题解析—JVM21题

    前言 文章对 JVM 内存区域分布、JVM 内存溢出分析、JVM 垃圾回收算法/垃圾收集器、JVM 性能调优工具及...

  • 干货分享丨jvm系列:dump文件深度分析

    摘要:java内存dump是jvm运行时内存的一份快照,利用它可以分析是否存在内存浪费,可以检查内存管理是否合理,...

网友评论

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

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