【Java虚拟机】JVM 主要组成部分与内存区域
JVM主要组成部分
JVM包含两个子系统和两个组件,分别为:
- Class loader(类装载子系统):根据给定的全限定名类名来装载class文件到运行时数据区的方法区中
- Execution engine(执行引擎子系统):执行引擎也叫解释器,负责解释class的指令,再提交给操作系统执行
- Runtime data area(运行时数据区组件):即我们常说的JVM的内存
- Native Interface(本地接口组件):与native lib交互,它的作用是融合不同的编程语言为Java所用,是其它编程语言交互的接口
首先通过编译器把 Java源代码转换成字节码,Class loader(类装载)再把字节码加载到内存中,将其放在运行时数据区的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
JVM 内存区域
JVM 在执行 Java 程序时,会将内存划分为若干个不同的数据区域,不同的区域用途不同,创建和销毁时间也不相同。在 JDK1.8 版本之后对运行时数据区域做了些修改,下面我们分别来看看修改前后的内存区域是怎么样的。
JDK1.8之前的JVM内存区域
JDK8之后的JVM内存区域
各区域的作用
程序计数器
当前线程执行的字节码的行号指示器,记录当前线程执行到程序的哪个位置,通过改变计数器的值,可以选取下一条需要执行的字节码指令。该区域是线程私有,是唯一一个不会发生OOM的区域。
Java虚拟机栈
描述 Java 方法执行的内存模型,每个方法执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成,就对应着一个栈帧在虚拟机中入栈到出栈的过程。该区域线程私有,生命周期与线程的生命周期相同。
本地方法栈
本地方法栈的作用和虚拟机栈的作用非常相似,区别是本地方法栈则为Native方法服务,而虚拟机栈为执行java方法服务,该区域也是线程私有。
Java堆
用于存储对象实例,是占用内存最大的区域,可划分为新生代和老年代,新生代又可细分为 Eden区、From Survivor区、To Survivor区。
在 HotSpot 中,对象在堆内存布局分成三部分:对象头,实例数据,对齐填充。
① 对象头:包括两部分的信息:
- 运行时数据:用于存储对象自身的运行时数据,如哈希码,GC代年龄,锁状态标志、线程持有的锁、偏向线程ID等。
- 类型指针:即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个Java数组,那对象头中还必须有一块用于记录数组长度的数据。
② 实例数据:是对象真正存储的有效信息,是在程序代码中所定义的各种类型的字段内容,相同宽度的字段会被分配到一起。
③ 对齐填充:并不是必然存在的,仅起着占位符的作用。
方法区
用于存储类信息,包括运行时常量池、静态变量、常量、即时编译后的代码(即class文件)等数据。与Java堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败会抛出 OOM 异常,该区域被所有线程共享。对这块区域进行垃圾回收的主要目标是对常量池的回收和对类型的卸载,但是一般比较难实现。
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。JDK8 之前,Hotspot 中方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),以前永久代的静态变量和常量池移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。那为什么要使用元空间取代永久代的实现?主要是为了方便管理方法区:
① 永久代的方法区,和堆使用的物理内存是连续的。对于永久代,由于类及方法的信息等比较难确定其大小,所以指定永久代的大小比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出,并且每次 Full GC 之后永久代的大小都会改变,如果动态生成很多 class 的话,就很可能出现 OOM,毕竟永久代的空间配置有限。
② JDK8之后,方法区存在于元空间,物理内存不再与堆连续,而是直接存在于本地内存中,理论上机器内存有多大,元空间就有多大。
③ 字符串存在永久代中,容易出现性能问题和内存溢出。
④ 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
参考: |