【面试】JVM内存结构

Java跨平台运行

我们都知道Java语言一次编译到处运行,可以在windows上运行也可以在Liunx上运行,属于跨平台语言,Java其实就是依赖JVM实现的跨平台性,但是我们的JVM本身是不存在跨平台的。

通过Javac.exe编译.java原代码文件生成.class文件,然后再通过类加载器,将class文件加载到JVM中,交由JVM运行,最后输出结果。

我们来记一张简洁的运行图:

image.png

JVM的组成

JVM由4大部分组成:ClassLoader(类加载器),Runtime Data Area(运行时数据区域),Execution Engine(执行引擎),Native Interface(本地接口)。

image.png

  • ClassLoader: 负责加载字节码文件,即是java编译后的.class文件。
  • Runtime Date Area: 存放.class文件和分配内存。
  • Native Interface: 负责调用本地接口,即是调用不同的语言接口给java使用。
  • Execution Engine: 当.class字节码文件被加载后,会把指令和数据信息存放在内存中,此时执行引擎负责把这些命令解释给操作系统。

类加载器

1 类加载器的过程

image.png

  • 加载:将字节码文件加载到内存
  • 校验:检验字节码文件的正确性
  • 准备:给类的静态变量分配内存,并赋予默认值
  • 解析:类装载器装入类所引用的其他对象
  • 初始化:对类的静态变量初始化为指定值,执行静态代码块

2 类加载的种类

  1. 启动类加载器:负责加载JRE的核心类库

  2. 扩展类加载器:负责加载JRE扩展的ext中的JAR类包

  3. 系统类加载器:负责加载ClassPath路径下的类包

  4. 用户自定义加载器:负责加载用户自定义路径下的类包

3 类加载机制

  1. 全盘负责委托机制:当类加载器加载一个类时,除非显示的是另一个加载器,该类锁依赖的和应用的类也由这个类加载器载入

  2. 双亲委派机制:当一个类加载器收到了类加载的请求的时候,他不会直接去加载目标类,首先委派父类加载器去寻找目标类,只有父加载器无法加载这个类的时候,才会在自己路径中查找并载入目标类。

Java虚拟机采用的是双亲委派模式,双亲委派机制的优势:避免类的重复加载,保护程序安全,防止核心API被随意篡改

运行时数据区域

运行时数据区域总共分为五部分:分别是Java虚拟机栈、本地方法栈、程序计数器、堆、方法区。

方法区: 负责存储.class文件,并且这块有一个运行常量池,就是存储一些变量或者常量信息的。

堆: 分配内存给对象,比如我们new的对象,就存在堆里面。

java虚拟机栈: 也可称为线程栈,每个线程独享的内存空间。

本地方法栈: 本地native方法独享的内存空间。

程序计数器: 记录线程执行的位置,方便线程切换后再次执行。

Java虚拟机栈

比如我们的main方法,调用sum函数,执行一个和的运算,此时我们的Java虚拟机栈就会为期分配栈帧内存区域。

public class MainDemo {

    // 一个方法对应一块栈帧内存区域
    public static Integer sum() {
        int a = 1;
        int b = 2;
        return a + b;
    }

    // main方法也对应一块栈帧内存区域
    public static void main(String[] args) {
        Integer sum = sum();
        System.out.println(sum);
    }
}
复制代码

首先我们来看一下他的执行顺序是怎样的?首先是先调用main函数,随后再去调用sum函数,sum运算结束之后,再销毁栈内存,其次再返回到main函数,等到main结束之后,再销毁main的栈内存空间,这个过程main先执行了,却是最后退出,即栈帧内部的数据结构即是先进后出(FILO)。

image.png

我们将上面的demo进行反汇编,翻译成JVM虚拟机的汇编代码:

javap -c MainDemo.class > MainDemo.txt
复制代码

image.png

然后我们打开MainDemo.txt文件,里面就是一堆的JVM运行的汇编代码。

Compiled from "MainDemo.java"
public class com.dt.thread.java.MainDemo {
  public com.dt.thread.java.MainDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static java.lang.Integer sum();
    Code:
       0: iconst_1
       1: istore_0
       2: iconst_2
       3: istore_1
       4: iload_0
       5: iload_1
       6: iadd
       7: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      10: areturn

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #3                  // Method sum:()Ljava/lang/Integer;
       3: astore_1
       4: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: aload_1
       8: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      11: return
}
复制代码

这一堆的代码,怎么来解读呢?其实Oracle官方有专门的指令码文档来解读。这里我们就来简单来解读一下

iconst_1 将int类型常量1压入栈

istore_0 将int类型值存入局部变量0

iconst_2 将int类型常量2压入栈

istore_1 将int类型值存入局部变量1

iload_0 从局部变量0中装载int类型值

iload_1 从局部变量1中装载int类型值

iadd 执行int类型的加法

invokestatic 调用类(静态)方法

areturn 从方法中返回引用类型的数据

我们栈帧内部存放的是一些局部变量,操作数栈,动态链表,方法出口。

image.png

这里当我们的栈中的局部变量是对象的时候,那么此时我们存储的是堆内存空间中对象的地址。

image.png

Java虚拟机启动时创建,用于存放对象实例,几乎所有的对象包括常量池都在堆上分配内存,当对象无法在内存申请内存时,就会抛出OOM(OutOfMemoryError)异常。

image.png

所有的类都是在Eden Space(伊甸区)new出来的,当伊甸区空间用完了,程序又需要创建对象,JVM的垃圾回收器将对伊甸区进行垃圾回收(Minor GC),将伊甸区中不再被其它对象所引用的对象销毁,然后被引用的剩余对象移到幸存者0区,当0区空间不够用,再次进行GC,然后移动到1区,如果1区也满了,将会转移到0区,幸存者0区和1区中反复存在,经过多次GC,超过15次的存活对象,最后将会进入到老年区,如果老年区内存空间也满了,将会产生MajorGC,进行老年区的内存清理,如果老年代执行了MajorGC之后,任然无法进行对象的保存,也会产生OOM(OutOfMemoryError)异常。

总结

GC是垃圾回收机制,java中申请的内存可以被垃圾回收装置进行回收,GC可以一定程度的避免内存泄漏,但是会引入一些额外的开销。 GC中主要回收的是堆和方法区中的内存,栈中内存的释放要等到线程结束或者是栈帧被销毁,而程序计数器中存储的是地址不需要进行释放。

猜你喜欢

转载自juejin.im/post/7110551447720198174