JAVA 的宗旨是其驰名的 WOTA:“一次编写,随处运行”。为了利用它,Sun Microsystems 创立了 Java 虚拟机,这是对编译后的 Java 代码进行解释的底层操作系统的形象。JVM 是 JRE(Java 运行时环境)的外围组件,它是为运行 Java 代码而创立的,但当初被其余语言(Scala、Groovy、JRuby、Closure ……)应用。
在本文中,我将重点关注 JVM 标准中形容的 运行时数据区。这些区域旨在存储程序或 JVM 自身应用的数据。我将首先介绍 JVM 的概述,而后介绍字节码是什么,最初介绍不同的数据区域。
内容[显示]
寰球概览
JVM 是底层操作系统的形象。它确保无论 JVM 在什么硬件或操作系统上运行,雷同的代码都将以雷同的行为运行。例如:
- 无论 JVM 是在 16 位 /32 位 /64 位操作系统上运行,原始类型 int 的大小始终是从 -2^31 到 2^31-1 的 32 位有符号整数。
- 无论底层操作系统 / 硬件是大端还是小端,每个 JVM 都以大端程序(高字节在前)存储和应用内存中的数据。
留神:有时,JVM 实现的行为与另一个不同,但通常是雷同的。
此图给出了 JVM 的概述:
- JVM解释 通过编译类的源代码 产生 的字节码。只管 JVM 一词代表“Java 虚拟机”,但它能够运行其余语言,如 scala 或 groovy,只有它们能够编译成 java 字节码。
- 为了防止磁盘 I/O,字节码由运行时数据区域之一中的 类加载器加载到 JVM。这段代码始终保留在内存中,直到 JVM 进行或类加载器(加载它的)被销毁。
- 加载的代码而后由 执行引擎 解释 和执行。
- 执行引擎须要存储数据,例如指向正在执行的代码行的指针。它还须要存储在开发人员代码中解决的数据。
- 执行引擎还负责解决底层操作系统。
留神:如果常常应用,许多 JVM 实现的执行引擎会将字节码编译为本机代码,而不是总是解释字节码。它被称为即时 (JIT ) 编译,大大放慢了 JVM。编译后的代码长期保留在通常称为 代码缓存 的区域中。因为该区域不在 JVM 标准中,因而我不会在本文的其余部分探讨它。
基于堆栈的架构
JVM 应用基于堆栈的体系结构。尽管它对开发人员来说是不可见的,但它对生成的字节码和 JVM 架构有微小的影响,这就是为什么我将简要解释这个概念的起因。
JVM 通过执行 Java 字节码中形容的基本操作来执行开发人员的代码(咱们将在下一章中看到)。操作数是指令对其进行操作的值。依据 JVM 标准,这些操作要求参数通过称为 操作数堆栈 的堆栈传递。
例如,让咱们以 2 个整数的根本加法为例。此操作称为 iadd ( 用于 整数 加法)。如果想在字节码中增加 3 和 4:
- 他首先将 3 和 4 压入操作数堆栈。
- 而后调用 iadd 指令。
- iadd 将从操作数堆栈中弹出最初两个值。
- int 后果 (3 + 4) 被压入操作数堆栈以供其余操作应用。
这种运行形式称为基于堆栈的架构。还有其余解决基本操作的办法,例如基于寄存器的体系结构将操作数存储在小寄存器中而不是堆栈中。桌面 / 服务器 (x86) 处理器和以前的 android 虚拟机 Dalvik 应用这种基于寄存器的架构。
字节码
因为 JVM 解释字节码,因而在深刻之前理解它是有用的。
java 字节码是将 java 源代码转化为一组基本操作。每个操作由一个字节组成,示意要执行的指令(称为 操作码 或操作码),以及用于传递参数的零个或多个字节(但大多数操作应用操作数堆栈来传递参数)。在 256 个可能的 1 字节长的 操作码中,有 204 个以后在 java8 标准中应用。
这是不同类别的字节码操作的列表。对于每个类别,我增加了一个小形容和操作码的十六进制范畴:
- 常量:用于将值从常量池(咱们稍后会看到)或从已知值推送到操作数堆栈中。从值 0x00 到 0x14
- Loads:用于将局部变量中的值加载到操作数堆栈中。从值 0x15 到 0x35
- Stores:用于从操作数堆栈存储到局部变量中。从值 0x36 到 0x56
- Stack:用于解决操作数堆栈。从值 0x57 到 0x5f
- Math:用于对操作数堆栈中的值进行根本数学运算。从值 0x60 到 0x84
- 转换:用于从一种类型转换为另一种类型。从值 0x85 到 0x93
- 比拟:用于两个值之间的根本比拟。从值 0x94 到 0xa6
- 管制:基本操作,如 goto、return ……容许更高级的操作,如循环或返回值的函数。从值 0xa7 到 0xb1
- 援用:用于调配对象或数组,获取或查看对象、办法或静态方法的援用。也用于调用(动态)办法。从值 0xb2 到 0xc3
- Extended:之后增加的其余类别的操作。从值 0xc4 到 0xc9
- 保留:供每个 Java 虚拟机实现外部应用。3 个值:0xca、0xfe 和 0xff。
这 204 个操作很简略,例如:
- 操作数ifeq (0x99) 查看 2 个值是否相等
- 操作数iadd (0x60) 增加 2 个值
- 操作数i2l (0x85) 将整数转换为长整数
- 操作数arraylength (0xbe) 给出了数组的大小
- 操作数pop (0x57) 从操作数堆栈中弹出第一个值
要创立字节码须要一个编译器,JDK 中蕴含的规范 java 编译器是javac。
咱们来看一个简略的加法:
public class Test {public static void main(String[] args) {
int a =1;
int b = 15;
int result = add(a,b);
}
public static int add(int a, int b){
int result = a + b;
return result;
}
}
“javac Test.java”命令在 Test.class 中生成一个字节码。因为 java 字节码是二进制代码,因而人类不可读。Oracle 在其 JDK 中提供了一个工具 javap,该工具将二进制字节码转换为来自 JVM 标准的人类可读的标记操作代码集。
命令“javap -verbose Test.class”给出以下后果:
Classfile /C:/TMP/Test.class
Last modified 1 avr. 2015; size 367 bytes
MD5 checksum adb9ff75f12fc6ce1cdde22a9c4c7426
Compiled from "Test.java"
public class com.codinggeek.jvm.Test
SourceFile: "Test.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Methodref #3.#16 // com/codinggeek/jvm/Test.add:(II)I
#3 = Class #17 // com/codinggeek/jvm/Test
#4 = Class #18 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 add
#12 = Utf8 (II)I
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
#15 = NameAndType #5:#6 // "<init>":()V
#16 = NameAndType #11:#12 // add:(II)I
#17 = Utf8 com/codinggeek/jvm/Test
#18 = Utf8 java/lang/Object
{public com.codinggeek.jvm.Test();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: bipush 15
4: istore_2
5: iload_1
6: iload_2
7: invokestatic #2 // Method add:(II)I
10: istore_3
11: return
LineNumberTable:
line 6: 0
line 7: 2
line 8: 5
line 9: 11
public static int add(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=2
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
LineNumberTable:
line 12: 0
line 13: 4
}
可读的 .class 表明字节码蕴含的不仅仅是 java 源代码的简略转录。它蕴含:
- 类的常量池的形容。常量池是 JVM 的数据区域之一,它存储无关类的元数据,例如办法的名称、它们的参数……当类在 JVM 中加载时,这部分会进入常量池。
- LineNumberTable 或 LocalVariableTable 等信息,用于指定函数的地位(以字节为单位)及其在字节码中的变量。
- 开发人员 java 代码的字节码转录(加上暗藏的构造函数)。
- 解决操作数堆栈的特定操作以及更宽泛的传递和获取参数的形式。
仅供参考,这是对存储在 .class 文件中的信息的简要形容:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
运行时数据区
运行时数据区域是设计用于存储数据的内存区域。这些数据由开发人员的程序或 JVM 用于其外部工作。
此图显示了 JVM 中不同运行时数据区域的概览。有些区域是举世无双的,其余区域是每个线程的。
堆
堆是所有 Java 虚拟机线程之间共享的内存区域。它是在虚拟机启动时创立的。所有类 实例 和数组 都在堆中 调配(应用 new 运算符)。
MyClass myVariable = new MyClass();
MyClass[] myArrayClass = new MyClass[1024];
该区域必须由 垃圾收集器 治理,以在不再应用时删除开发人员调配的实例。清理内存的策略取决于 JVM 的实现(例如,Oracle Hotspot 提供了多种算法)。
堆能够动静扩大或膨胀,并且能够具备固定的最小和最大大小。例如,在 Oracle Hotspot 中,用户能够通过以下形式应用 Xms 和 Xmx 参数指定堆的最小大小“java -Xms=512m -Xmx=1024m …”
留神:堆不能超过最大大小。如果超过此限度,JVM 将抛出OutOfMemoryError。
办法区
办法区是所有 Java 虚拟机线程之间共享的内存。它是在虚拟机启动时创立的,并由 类加载器 从字节码加载。只有加载它们的类加载器还活着,办法区中的数据就会保留在内存中。
办法区存储:
- 类信息(字段 / 办法的数量、超类名称、接口名称、版本……)
- 办法和构造函数的字节码。
- 每个加载的类都有一个运行时常量池。
标准不强制在堆中实现办法区。例如,在 JAVA7 之前,Oracle HotSpot应用名为 PermGen 的区域来存储办法区域。这个 PermGen 与 Java 堆(以及像堆一样由 JVM 治理的内存)是间断的,并且被限度为 64Mo 的默认空间(由参数 -XX:MaxPermSize 批改)。从 Java 8 开始,HotSpot 当初将办法区存储在称为 Metaspace 的独立本机内存空间中,最大可用空间是可用的零碎总内存。
留神:办法区域不能超过最大大小。如果超过此限度,JVM 将抛出OutOfMemoryError。
运行时常量池
该池是办法区的子局部。因为它是元数据的重要组成部分,Oracle 标准将运行时常量池与办法辨别开形容。每个加载的类 / 接口都会减少这个常量池。这个池就像传统编程语言的符号表。换句话说,当一个类、办法或字段被援用时,JVM 通过运行时常量池在内存中搜寻理论地址。它还蕴含常量值,如字符串文字或常量原语。
String myString1 =“This is a string litteral”;
static final int MY_CONSTANT=2;
pc 寄存器(每个线程)
每个线程都有本人的 pc(程序计数器)寄存器,与线程同时创立。在任何时候,每个 Java 虚拟机线程都在执行单个办法的代码,即该线程的 以后办法。pc 寄存器蕴含以后正在执行的 Java 虚拟机指令(在办法区域中)的地址。
留神:如果线程以后正在执行的办法是 native,则 Java 虚拟机的 pc 寄存器的值是未定义的。Java 虚拟机的 pc 寄存器足够宽,能够保留特定平台上的 returnAddress 或 native 指针。
Java 虚拟机堆栈(每线程)
堆栈区域存储多个帧,因而在探讨堆栈之前,我将介绍这些帧。
框架
帧是一种数据结构,其中蕴含示意 以后办法(被调用的办法)中线程状态的多个数据:
- 操作数堆栈:我曾经在对于基于堆栈的体系结构的章节中介绍了操作数堆栈。字节码指令应用此堆栈来解决参数。该堆栈还用于在(java)办法调用中传递参数,并在调用办法的堆栈顶部获取被调用办法的后果。
- 局部变量数组:该数组蕴含以后办法范畴内的所有局部变量。该数组能够保留原始类型、援用或 returnAddress 的值。这个数组的大小是在编译时计算的。Java 虚拟机在办法调用时应用局部变量来传递参数,被调用办法的数组是从调用办法的操作数栈中创立的。
- 运行时常量池援用 :援用以后正在执行的 办法的 以后类 的常量池。JVM 应用它来将符号办法 / 变量援用(例如:myInstance.method())转换为理论内存援用。
堆
每个 Java 虚拟机线程都有一个公有的Java 虚拟机堆栈,与线程同时创立。Java 虚拟机堆栈存储帧。每次调用办法时都会创立一个新框架并将其放入堆栈中。框架在其办法调用实现时被销毁,无论该实现是失常的还是忽然的(它会引发未捕捉的异样)。
只有一帧,即执行办法的帧,在给定线程的任何点都处于活动状态。该帧称为 以后帧 ,其办法称为 以后办法 。定义以后办法的类是 以后类。对局部变量和操作数堆栈的操作通常参考以后帧。
让咱们看上面的例子,它是一个简略的加法
public int add(int a, int b){return a + b;}
public void functionA(){
// some code without function call
int result = add(2,3); //call to function B
// some code without function call
}
以下是运行 functionA() 时它在 JVM 中的工作形式:
在 functionA() 中,Frame A 是堆栈帧的顶部,并且是以后帧。在对 add() 的外部调用开始时,将一个新帧(Frame B)放入堆栈中。帧 B 成为以后帧。帧 B 的局部变量数组通过弹出帧 A 的操作数堆栈来填充。当 add() 实现时,帧 B 被销毁并且帧 A 再次成为以后帧。add() 的后果被放入 Frame A 的操作数堆栈,以便 functionA() 能够通过弹出其操作数堆栈来应用它。
留神:此堆栈的性能使其可动静扩大和膨胀。堆栈不能超过最大大小,这限度了递归调用的数量。如果超过此限度,JVM 会抛出 StackOverflowError。
对于 Oracle HotSpot,您能够应用参数 -Xss 指定此限度。
本机办法堆栈(每线程)
这是用 Java 以外的语言编写并通过 JNI(Java 本地接口)调用的本地代码的堆栈。因为它是“本机”堆栈,因而该堆栈的行为齐全取决于底层操作系统。
来填充。当 add() 实现时,帧 B 被销毁并且帧 A 再次成为以后帧。add() 的后果被放入 Frame A 的操作数堆栈,以便 functionA() 能够通过弹出其操作数堆栈来应用它。
留神:此堆栈的性能使其可动静扩大和膨胀。堆栈不能超过最大大小,这限度了递归调用的数量。如果超过此限度,JVM 会抛出 StackOverflowError。
对于 Oracle HotSpot,您能够应用参数 -Xss 指定此限度。
本机办法堆栈(每线程)
这是用 Java 以外的语言编写并通过 JNI(Java 本地接口)调用的本地代码的堆栈。因为它是“本机”堆栈,因而该堆栈的行为齐全取决于底层操作系统。
对于 JVM 内存模型,你学废了么?