本文基于 JDK1.8 阐述分析
我们都知道 Java 源文件通过编译器编译后,能产生相应的 .Class 文件,也就是字节码文件。而字节码文件通过 Java 虚拟机中的解释器,编译成特定机器上的机器码。
Java 能跨平台的原因是因为:不同的平台有不同的 JVM 版本,一个 Java 源文件被编译成字节码文件,被不同平台的 JVM 翻译成特定平台下的机器码从而运行。
Java 虚拟机由三个子系统构成,分别是类加载子系统、JVM 运行时数据区和执行引擎,本文的重点是在 JVM 运行时数据区。
类加载子系统将硬盘上的字节码文件加载进内存,JVM 运行内存有一套自己的结构划分如图所示,最终程序要运行,需要操作系统分配相应的时间调度,由执行引擎去执行,才能得到最终结果。
线程共享数据:允许被所有线程共享访问的一块内存区域。
线程私有数据:本线程私有的一块内存区域
Java 虚拟机栈是线程私有的,它的生命周期与线程相同,线程启动而产生,线程结束而消亡。
Java 虚拟机栈是描述 Java 方法执行的内存模型,用于存储栈帧。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。
虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。
除了 native 方法,几乎所有的 Java 方法都是通虚拟机栈来实现方法的调用和执行(需要程序计数器、堆、方法区的配合)。
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
JVM 的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,在同一时刻一个处理器内核只会执行一条线程,处理器切换线程时并不会记录上一个线程执行到哪个位置,所以为了线程切换后依然能恢复到原位,每条线程都需要有各自独立的程序计数器。
程序计数器存储的是字节码文件的行号,而这个范围是可知晓的,在一开始分配内存时就可以分配一个绝对不会溢出的内存。
当执行 Java 方法时,程序计数器存放 Java 字节码的地址。实现上可能有两种形式,一种是相对该方法字节码开始处的偏移量,叫做 bytecode index(简称 bci)。另一种是该 Java 字节码指令在内存的地址,叫做 bytecode pointer(简称 bcp)。
Native 方法大多通过 C 实现,它的方法体不是由 Java 字节码构成,无法应用上述 Java 字节码地址的概念,也就不需要存储字节码文件的行号。
Java 线程总是需要以某种形式映射到 OS 线程上,HotSpot VM 目前在大多数平台上都使用 1:1 模型(原生线程模型),也就是每个 Java 线程直接映射到一个 OS 线程上执行。此时 native 方法由原生平台直接执行。
本地方法栈为虚拟机使用到的 Native 方法服务。Native 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用 C/C++ 方法。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
不同于虚拟机栈的入/出栈,当线程调用 native 方法时,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
如果某个虚拟机实现的本地方法接口是使用 C 连接模型的话,那个他的本地方法栈就是 C 栈,当一个 C 函数调用另一个 C 函数时,它的栈操作是确定的。如果本地方法接口需要回调JVM 中的 Java 方法,该线程会保存本地方法栈的状态并进入到另一个Java栈。
虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。常用的 HotSpot 虚拟机选择合并了虚拟机栈和本地方法栈。
堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。
参数 | 说明 |
---|---|
-Xms | 堆内存初始大小 |
-Xmx | 堆内存最大允许大小 |
-Xss | 每个线程的 Stack 大小 |
-XX:NewSize(-Xns) | 新生代初始大小 |
-XX:MaxNewSize(-Xmn) | 新生代最大允许大小 |
-XX:NewRatio | 设置新生代与老年代比值 |
-XX:SurvivorRatio | 设置 Survivor 与 Eden 比值 |
-XX:PermSize | 设置持久代初始内存大小(JDK8 以前) |
-XX:MaxPermSize | 设置持久代最大内存(JDK8 以前) |
-XX:MetaspaceSize | 设置元空间初始内存大小(JDK8 以后) |
-XX:MaxMetaspaceSize | 设置元空间最大内存(JDK8 以后) |
在堆中分配的内存,由 JVM 自动垃圾回收器来管理。关于 GC 详情,之后再补充。
方法区是一种规范,不同的虚拟机的实现也不一样。从 JDK 1.8 开始,元空间(Metaspace)取代了永久代(PermGen)成为 HotSpot VM 对方法区的实现。方法区存储加载进来的每一个类的结构信息,可以看做是将类(Class)的模板信息,保存在方法区里
JDK8 以前,永久代是堆的一部分,和新生代、老年代的地址是连续的。JDK8 以后,元空间属于本地内存,不再属于堆的一部分,它还有一个别名叫非堆(Non-Heap),所以元空间不存在 OOM 内存溢出的情况。
当多个线程用到同一个类,而这个类还未被加载,则应该只有一个线程去加载类,其他线程等待。