本文来源于公众号:勾勾的Java宇宙(微信号:Javagogo),莫得推广,全是干货!
原文链接:mp.weixin.qq.com/s/J3SiqlJlq… 作者:王磊
JVM 是 Java 程序能够运行的根本,因此掌握 JVM 也已经成了一个合格 Java 程序员必备的技能。而 JVM 的内存布局是一道必考面试题,一般会作为 JVM 相关的第一道面试题出现,这也是中高级工程师必须掌握的一个知识点。
所以我们本篇文章的主题是,说一说 JVM 的内存布局和运行原理。
内存布局
JVM 的种类有很多,比如 HotSpot 虚拟机,它是 Sun/OracleJDK 和 OpenJDK 中的默认 JVM,也是目前使用范围最广的 JVM。
我们常说的 JVM 其实泛指的是 HotSpot 虚拟机,还有曾经与 HotSpot 齐名为“三大商业 JVM”的 JRockit 和 IBM J9 虚拟机。
但无论是什么类型的虚拟机都必须遵守 Oracle 官方发布的「Java虚拟机规范」,它是 Java 领域最权威最重要的著作之一,用于规范 JVM 的一些具体行为。
同样对于 JVM 的内存布局也一样,根据「Java虚拟机规范」的规定,JVM 的内存布局分为以下几个部分:
堆
堆(Java Heap) 也叫 Java 堆或者是 GC 堆,它是一个线程共享的内存区域,也是 JVM 中占用内存最大的一块区域,Java 中所有的对象都存储在这里。
「Java虚拟机规范」对 Java 堆的描述是:
所有的对象实例以及数组都应当在堆上分配。
但这在技术日益发展的今天已经有点不那么准确了,比如 JIT(Just In Time Compilation,即时编译 )优化中的逃逸分析,使得变量可以直接在栈上被分配。
当对象或者是变量在方法中被创建之后,其指针可能被线程所引用,而这个对象就被称作指针逃逸或者是引用逃逸。
比如以下代码中的 sb 对象的逃逸:
public static StringBuffer createString() {
StringBuffer sb = new StringBuffer();
sb.append("Java");
return sb;
}
sb 虽然是一个局部变量,但上述代码可以看出,它被直接 return 出去了,因此可能被赋值给了其他变量,并且被完全修改,于是此 sb 就逃逸到了方法外部。
想要 sb 变量不逃逸也很简单,可以改为如下代码:
public static String createString() {
StringBuffer sb = new StringBuffer();
sb.append("Java");
return sb.toString();
}
小贴士:通过逃逸分析可以让变量或者是对象直接在栈上分配,从而极大地降低垃圾回收的次数以及堆分配对象的压力,进而提高程序的整体运行效率。
回到主题,堆大小的值可通过 -Xms 和 -Xmx 来设置(即设置最小值和最大值),当堆超过最大值时就会抛出 OOM(OutOfMemoryError)异常。
方法区
方法区(Method Area) 也被称为非堆区,来与「Java 堆」的概念进行区分,它也是线程共享的内存区域,用于存储已经被 JVM 加载的类型信息、常量、静态变量、代码缓存等数据。
说到方法区有人可能会联想到“永久代”,但对于「Java虚拟机规范」来说并没有规定这样一个区域,同样它也是 HotSpot 中特有的一个概念。
这是 HotSpot 技术团队把垃圾收集器的分代设计扩展到方法区之后才有的一个概念,可以理解为 HotSpot 技术团队只是用永久代来实现方法区而已,但这会导致一个致命的问题——这样设计更容易造成内存溢出。
因为永久代有 -XX:MaxPermSize(方法区分配的最大内存)的上限,即使不设置也会有默认的大小,例如 32 位操作系统中的 4GB 内存限制等,并且这样设计导致了部分方法在不同类型的 Java 虚拟机下的表现也不同,比如 String::intern() 方法。
所以在 JDK 1.7 时 HotSpot 虚拟机已经把原本放在永久代的字符串常量池和静态变量等移出了方法区,并且在 JDK 1.8 中完全废弃了永久代的概念。
程序计数器
程序计数器(Program Counter Register) 线程独有一块很小的内存区域,保存当前线程所执行字节码的位置,包括正在执行的指令、跳转、分支、循环、异常处理等。
虚拟机栈
虚拟机栈也叫 Java 虚拟机栈(Java Virtual Machine Stack),和程序计数器相同,它也是线程独享的,用来描述 Java 方法的执行。
每个方法被执行时就会同步创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。当调用方法时执行入栈,而方法返回时执行出栈。
本地方法栈
本地方法栈(Native Method Stacks) 与虚拟机栈类似,它是线程独享的,并且作用也和虚拟机栈类似。只不过虚拟机栈是为虚拟机中执行的 Java 方法服务的,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
小贴士:需要注意的是「Java虚拟机规范」只规定了有这么几个区域,但没有规定 JVM 的具体实现细节,因此对于不同的 JVM 来说,实现也是不同的。
例如,“永久代”是 HotSpot 中的一个概念,而对于 JRockit 来说就没有这个概念。所以很多人说的 JDK 1.8 把永久代转移到了元空间,这其实只是 HotSpot 的实现,而非「Java虚拟机规范」的规定。
JVM 的执行流程是,首先把 Java 代码 .java 转化成字节码 .class,然后通过类加载器将字节码加载到内存中。所谓的内存也就是我们上面介绍的运行时数据区,但字节码并不是可以直接交给操作系统执行的机器码,而是一套 JVM 的指令集。
这个时候需要使用特定的命令解析器,也就是我们俗称的执行引擎(Execution Engine),将字节码翻译成可以被底层操作系统执行的指令再去执行,这样就实现了整个 Java 程序的运行,这也是 JVM 的整体执行流程。
欢迎大佬们关注公众号 勾勾的Java宇宙(微信号:Javagogo),拒绝水文,收获干货!