JVM Basic¶
Java 代码是如何运行起来的¶
字节码文件¶
Java 源代码经过编译后得到 .class 文件,又称为字节码文件
JDK, JRE, JVM¶
- JDK : Java Development Kit
- JRE : Java Runtime Environment
各个操作系统有自己对应的 JDK,JDK 包含了 JRE
JVM 内存结构¶
JVM 内部的 java 内存模型在 逻辑上 划分为
线程栈
(又称为方法栈 / 调用栈)堆内存
线程栈
中保存了调用链上正在执行的所有方法中的局部变量,每个线程只能访问自己的线程栈。堆内存
中保存了代码中创建的所有对象(不论是哪个线程创建的,且涵盖了 Integer 等包装类型)。
总结:
- 原始数据类型和对象引用地址在栈上
- 对象、对象成员与类定义、静态变量在堆上
-
当不同线程(通过本地的栈上的引用)访问同一个对象实例中的成员变量时,每个线程在本地会有一个变量的副本
关于 volatile :
- volatile 修饰的是共享内存中的数据,比如修饰 static 数据,其修饰的数据储存在方法区中
- 对于方法区的变量,线程直接读取/改写,而非将其挪到线程的栈上
- 但是注意到 CPU 和内存间的高速缓存,volatile 对于变量的修改不一定会直接反映在主存中
- 这里可以结合 cache 的 write back 策略理解
JVM 运行时数据区¶
- JVM 内存:受虚拟机内存大小的参数控制,当大小超过设置的大小时会报 OOM
- 本地内存:被利用但是不虚拟机直接管理,其大小不受参数控制,但是当超过物理内存的容量限制时会报 OOM
1. 程序计数器¶
任何时间一个线程都只有一个方法在执行,称为当前方法。
- 当前线程正在执行 Java 方法,程序计数器记录 JVM
字节码
指令的行号 - 正在执行 native 方法,则是 undefined
程序计数器是线程私有的,生命周期与线程的生命周期一致,由字节码解释器
控制
此数据区是唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 的区域
2. 虚拟机栈(Java栈)¶
注意,这里的“基本数据类型”指的是类型是基本数据类型的方法本地变量
Java 方法有两种返回函数的方式:
-
正常返回 return
-
抛出异常
虚拟机栈有两种异常状况:
- 请求的栈深度大于虚拟机允许的深度:
StackOverFlowError
- 如果虚拟机栈容量可以动态扩展,当无法在扩展时申请到足够的内存:
OutOfMemoryError
- 请求的栈深度大于虚拟机允许的深度:
3. 本地方法栈¶
- 虚拟机栈为虚拟机执行 Java 方法(字节码)服务
- 本地方法栈为虚拟机使用到的 native method 服务
二者都是线程私有的。
Hotspot 虚拟机直接把本地方法栈和虚拟机栈合二为一了
4. 堆¶
在 Java8 中主要存放:
对象实例
字符串常量池
静态变量
:又称为类变量
,static 修饰线程分配缓冲区
:用于提升对象分配时的效率
Java 虚拟机规范规定,Java 堆可以处在物理不连续,但逻辑需要连续的区域中,堆的大小是可以拓展的(通过 -Xmx
和 -Xms
控制)。
如果堆中的内存不够完成实例的内存分配,且堆无法再拓展,则会抛出
OutOfMemoryException
异常
堆内存的进一步划分¶
- Eden 区:新对象最初会被分到 Eden 区,且 Eden 区较大,频繁进行垃圾回收
- Survivor 区:两个 Suervivor 区 S0 和 S1 交替使用,新对象在 Eden 区经过一次垃圾回收后存放到其中的一个 Survivor 区,进一步存活的对象会移到另一个 Survivor 区,最终变为老年代
- 老年代:长生命周期对象经过多次垃圾回收后会被移到老年代,Major GC 在老年代进行,频率低、耗时长
5. 方法区¶
方法区是所有线程共享的内存,
- 在java8以前是放在JVM内存中的,由永久代实现,受JVM内存大小参数的限制
-
在java8中移除了永久代的内容,方法区由元空间(Meta Space)实现,并直接放到了本地内存中,不受JVM参数的限制(当然,如果物理内存被占满了,方法区也会报OOM),并且将原来放在方法区的字符串常量池和静态变量都转移到了Java堆中,方法区与其他区域不同的地方在于,方法区在编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:
-
类元信息(Klass)
- 类元信息在类编译期间放入方法区,里面放置了类的基本信息,包括类的版本、字段、方法、接口以及常量池表(Constant Pool Table)
- 常量池表(Constant Pool Table)存储了类在编译期间生成的字面量、符号引用(什么是字面量?什么是符号引用?),这些信息在类加载完后会被解析到运行时常量池中
-
运行时常量池(Runtime Constant Pool)
- 运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些
- 运行时常量池具备动态性,可以添加数据,比较多的使用就是String类的intern()方法
-
- JDK8 之前,HotSpot 使用永久代来实现方法区
- JDK8 之后使用元空间 metaspace ,元空间使用的是本地内存
运行时常量池¶
当类加载到内存中后,JVM就会将class常量池中的内容存放到运行时常量池中;运行时常量池里面存储的主要是编译期间生成的字面量、符号引用等等。
- 类加载在链接环节的解析过程,会符号引用转换成直接引用(静态链接)。此处得到的直接引用也是放到运行时常量池中的。
- 运行期间可以动态放入新的常量。
- 类加载 的“解析”阶段,使用
静态链接
来将符号引用转换为直接引用
- 适用于 静态方法、私有方法、构造方法、final 方法等
- 运行时 通过
动态链接
将符号引用替换为直接引用
- 适用于实例方法(无论是否被重写)
// 动态链接的例子 public class Dog extends Animal { @Override public void sound() { System.out.println("Dog barks"); } } public class Test { public static void main(String[] args) { Animal animal = new Dog(); // 向上转型 // 静态类型为 Animal // 实际类型为 Dog // 编译器并不知道其实际类型,方法调用依赖于实际类型 animal.sound(); } }
这段代码中的 animal.sound(); 就是符号引用,编译器会将 sound 方法的符号引用存在常量池中
这部分内容将在类加载后进入方法区的运行时常量池
中存放。JVM 为每个已加载的类型(类 / 接口)都维护一个运行时常量池,在加载类和接口到虚拟机后创建。所以运行时常量池相对于 Class 文件常量池的一个重要特性是动态性。
不要求常量只有在编译期才能产生,运行时也可以把新的常量放入池中,例如: