JVM 一直都是面试的必考点,大家都知道,但是要把它搞清楚又好像不是特别容易。JVM 的知识点太散,不系统,所以不便于归纳总结,今天楠哥今天就来帮大家解决这个问题,用一篇文章把 JVM 的结构讲清楚。
JVM 可分为 5 个部分,分别是:
1、类加载器(Class Loader)
2、运行时数据区(Runtime Data Area)
3、执行引擎(Execution Engine)
4、本地库接口(Native Interface)
5、本地方法库(Native Libraies)
这其中最复杂的是运行时数据区,又可分为方法区、虚拟机栈、本地方法栈、堆、程序计数器,并且方法区和堆是线程共享的,虚拟机栈、本地方法栈、程序计数器是线程隔离的,JVM 的结构如下图所示。
搞清楚了 JVM 虚拟机的结构,接下来我们详细讲解它的每一部分。
类加载器:加载字节码文件到内存。
执行引擎:对 JVM 指令进行解析,翻译成机器码,解析完成后提交到操作系统中。
本地库接口:供 Java 调用的融合了不同开发语言的原生库。
本地方法库:Java 本地方法的具体实现。
运行时数据区:JVM 核心内存空间结构模型。
运行时数据区是 JVM 内存结构最重要的部分,接下来我们详细讲解运行时数据区的各个组成部分。
1、方法区
方法区存储虚拟机加载的类信息、常量、静态变量,即时编译器编译后的代码等数据。方法区是一种规范,永久代是方法区的一种实现,这里有个常考的面试题:JDK 7 以前的版本字符串常量池是放在永久代中的,JDK 7 将字符串常量池移动到了堆中,JDK 8 直接删除了永久代,改用元空间替代永久代。
2、本地方法栈
本地方法栈与 Java 栈的作用和原理基本相同,都可以用来执行方法,不同点在于 Java 栈执行的是 Java 方法,本地方法栈执行的是本地方法。
什么是 Java 的本地方法?Java 是基于应用层的高级编程语言,无法访问操作系统底层信息,如底层硬件设备等,这个时候就需要使用其他语言来完成功能了,比如 C 语言,本地方法的使用原理如下所示:
1、在 Java 程序中声明 native 修饰的方法,只有方法定义,没有方法实现,将该 Java 文件编译成字节码文件。
2、用 javah 编译字节码文件,生成一个 .h 文件。
3、写一个 .cpp 文件实现 .h 文件中的方法。
4、将 .cpp 文件编译成动态链接库文件 .dll 。
5、使用 System.loadLibrary() 加载动态连接库文件。
这样就可以实现本地方法的调用,用 Java 调用非 Java 编写的接口,基本原理是利用反射机制,在运行的时候找到 .dll 文件并且解析,根据动态链接库中的文件名称创建出对象和方法,然后我们就可以利用对象调用方法了。
常见的本地方法有:public final native Class<?> getClass()、public native int hashCode()、protected native Object clone()。
3、程序计数器
程序计数器占用的内存空间较小,是当前线程所执行的字节码行号指示器,通过改变这个计数器的值来选取下一条需要执行的字节码指令。多个线程之间的程序计数器相互独立,互不影响,为了保证每个线程都恢复后都可以找到具体的执行位置。
4、Java 堆
Java 堆用来存放实例化对象,Java 堆被所有线程共享,在虚拟机启动时创建,用来存放对象实例,是 Java 内存结构中的大头,占用大部分的空间,是 GC 的主要管理区域,又可分为年轻代、老年代、永久代,JDK 8 及以后去掉了永久代。
年轻代
年轻代又可分为 Eden,from Survivor,to Survivor。
Eden区:对象刚被创建的时候,存放在 Eden 区,如果 Eden 区放不下,则放在 Survivor 区,甚至老年代中。
Survivor 区:Survivor 又可分为 Survivor From 和 Survivor To,GC 回收时使用,将 Eden 中存活的对象存入 Survior From 中,下一次回收时,将 Survior From 中的对象存入 Survior To 中,清除 Survior From ,下一次回收时重复次步骤,Survior From 变成 Survior To,Survivor To 变成 Survivor From,依次循环,同时每次回收,对象的年龄都 +1,年龄增加到一定程度的对象,移动到老年代中。
老年代
存放生命周期较长的对象。JDK 8 之后改用元空间替代永久代。
元空间
Java 8 之后开始将类的元数据放在堆内存中,这块区域叫做元空间,在 Java 7 及以前,元空间是放在永久代中的,Java 8 之后分离出来了。
元空间和永久代是方法区的实现,方法区只是一种规范,在 Java 7 之后,原先位于方法区永久代里的字符串常量池已被移动到了 Java 堆中,因为永久代的内存空间极为有限,如果频繁调用 inter 方法,内存无法存储这么多数据。在 Java 8 之后将永久代完全删除了,使用元空间替代了永久代。
元空间使用本地内存,永久代使用 JVM 内存,所以使用元空间的好处在于程序的内存不在受限于 JVM 内存,本地内存剩余多少空间,元空间就可以有多大,解决了空间不足的问题。
5、虚拟机栈
Java 方法执行的内存模型,Java 栈中存放的是多个栈帧,每个栈帧对应一个被调用的方法,主要包括局部变量表、操作数栈、动态链接、方法返回地址(方法出口)。每一个方法的执行,JVM 都会创建一个栈帧,并且将栈帧压入 Java 栈,方法执行完毕,该栈帧出栈。
局部变量表:存储方法执行过程中的所有变量,包括方法中声明的局部变量和形参。
操作数栈:方法中的计算过程都是借助于操作数栈来完成的,将参与计算的数据压入操作数栈,
栈的具体运算方式是这样的,编译器是通过两个栈来实现的,一个是保存操作数的栈,另一个是保存运算符的栈。我们从左向右遍历表达式,当遇到数字,直接压入操作数栈。当遇到运算符,先与运算符栈的栈顶元素进行比较,如果高于当前栈顶元素的优先级,直接压入,否则取出当前栈顶的运算符,同时取出操作数栈的前两个数据进行运算,并将结果压入操作数栈。再次重复上述步骤,直到当前的运算符被压入栈中,当没有新的运算符需要入栈的时候,取出当前的栈顶元素以及操作数栈的两个运算,进行运算,将结果压入操作数栈,如果方法定义时需要返回值,直接将操作数栈栈顶元素返回即可。
方法返回地址:一个方法调用结束之后要返回到调用它的地方,所以栈帧中要保持能够返回到方法调用处的地址。
每个线程都有自己的 Java 栈,相互独立,可以同时执行各种的方法,每个方法的执行都是一个栈帧的入栈和出栈过程,Java 虚拟机栈用来存储栈帧,方法调用结束之后,帧会被销毁。
以上就是 JVM 的内存结构