提及 JVM 这个词,估计大家都能简单说两句,但是根据身边朋友以及诸多粉丝提出的疑问,能系统讲出来 JVM 的却真心很少。
网上讲解 JVM 这块的文章非常多,不过鱼龙混杂,鉴于 JVM 也是最考验 Java 程序员的基础功底啦,今天静下来,一起画画图,一起梳理梳理,好好填补一下这块,争取无论走到哪里,大家在脑海中都能有行走的 JVM 内存模型图。
1
JVM 初识
在讲解 JVM 之前,先来揭秘一下 Java 程序是如何实现一次编译到处运行的?
步骤一:用文本编辑器或者 IDE,快速编写 HelloWorld.java 的源代码文件;
步骤二:用 Java 编译器(javac)把源代码(*.java)编译成字节码文件(*.class);
步骤三:字节码文件(.class)便可以在任何安装了 JVM 的操作系统中运行,JVM 会将字节码翻译成可以被机器执行的本地机器码。
那么重点来了,Java 是如何实现一次编译到处运行的呢?通过上图应该很清晰找到解。
解一:Java 一次编译,到处运行,跨平台的特性是通过 JVM 来实现的,通过 JVM 来屏蔽底层操作系统的差异;
解二:Java 通过 JVM 来实现跨平台,但是 JVM 是不跨平台的,也就是说不同操作系统之上的 JVM 是不同的,Linux 系统上的 JVM 不能用在 Windows 系统上。
2
JVM 窥探
既然已经知晓 Java 程序可以通过 JVM 来实现一次编译,到处运行的跨平台特性,那么 JVM 到底是什么呢?
JVM 是 Java Virtual Machine(Java虚拟机)的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的 —— 百度百科。
上面是引了一段百度百科对 JVM 的解释,大意就是 Java 虚拟机是在计算机上虚构出来的一个计算机,既然是虚构的就意味着看不到,只在于内存之中。
(图片来源于网络)
如上图所示,计算机主要有运算器、控制器、内外存储器、输入和输出设备组成,那么 JVM 结构长啥样子呢?
还是以开篇的 HelloWorld 为例,窥探一下 JVM 的运行流程。
1. Java 源代码文件会被 Java 编译器编译为字节码文件(.class);
2. JVM 中的类加载器进行加载各个类的字节码文件,将所有类结构和方法变量放入运行时数据区;
3. 字节码文件加载完毕之后,交给 JVM 执行引擎进行执行。
JVM = 类加载器 ClassLoader + 执行引擎 Execution Engine + 运行时数据区域 Runtime Data Area。
在程序执行过程中,会分配内存空间来存储程序执行期间需要用到的数据相关信息,分配的内存空间被称作为 Runtime Data Area(运行时数据区),也就是常说的 JVM 内存,接下来就重点说一说 JVM 运行时数据区。
(JDK 1.7 内存模型)
如上图所示,JVM 运行时数据区主要分为程序计数器、虚拟机栈、本地方法栈、方法区、堆。
如上图所示,按照内存共享来划分 JVM 内存,主要划分为线程共享内存区域(堆、方法区)、线程私有内存区域(程序计数器、虚拟机栈、本地方法栈)、直接内存。
3
JVM 运行时数据区解剖
从上面介绍,可以清晰知道 JVM 运行时数据区,主要分为程序计数器、虚拟机栈、本地方法栈、方法区、堆。接下来就逐步解剖,简单了解一下。
1. 程序计数器
程序计数器(PC Register),也有人称它为 PC 寄存器。
主要是为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器。
2. 虚拟机栈
虚拟机栈(VM Stack),也有人称它为 Java 虚拟机栈、栈或者 JVM 栈,这块就是大家经常分析一段程序执行时要关注的区域。
JVM 栈是每个线程私有的,线程创建的同时都会创建对应的 JVM 栈。
JVM 栈中存放的是当前线程中局部基本类型的变量、返回结果以及 Stack Frame,而非基本类型的对象在 JVM 栈上仅存放一个指向堆上的地址。
3. 本地方法栈
本地方法栈(Native Method Stack)与 JVM 栈很相似,本地方法栈也是每个线程私有的,只不过是服务的对象不同,JVM 栈是为执行 Java 方法服务,而本地方法栈则是为执行本地方法(Native Method)服务。
4. 方法区
方法区(Method Area),也有人称它为永久代,是线程共享的区域。
在方法区中,主要存放静态变量、常量、类信息、运行时常量池以及所有的方法的信息。运行时常量池(Runtime Constant Pool)是方法区的重要一部分,用于存储编译器生成的常量和引用。
(JDK 1.8 内存模型)
如上图所示,值得注意的是 JDK 1.8 相比 JDK 1.7,JVM 运行时数据区划分中的方法区(持久代)从 JVM 运行时数据区拿掉了,而在本地内存加入了元数据区(Metadata Memory),简而言之在 JDK 1.8 中,元数据区替代了方法区(持久代)。
5. 堆
堆(Heap),所有 new 出来的对象实例都存储在该区域,这块区域也是线程共享的,也是分析 Java 程序时关注较多的一块。
6. 其它
直接内存(Direct Memory),有时也称作堆外内存,是不受 JVM控制的内存。
在 JDK 1.4 中加入了 NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。
避免了在 Java 堆和 Native 堆中来回复制数据,能在一些场景中显著提高性能。
4
寄语写最后
本次,主要让大家了解一下 JVM 的内存结构,希望通过本次分享,大家对 JVM 能有个梗概的认识,想要彻底掌握还需针对性的弥补,说句心里话,希望能够把这些图都记在脑子里,只要做到脑中有图,心就不慌。
好了,本次就谈到这里,一起聊技术、谈业务、喷架构,少走弯路,不踩大坑。会持续输出原创精彩分享,敬请期待!
推荐阅读: