为了实现Java最重要的思想:一次编写,到处运行
。Sun
公司创建了Java虚拟机。虚拟机抽象了底层的操作系统,解析编译后的Java代码。JVM(Java Virtual Machine)是JRE(Java Runtime Environment)的核心,用于运行Java代码,现在也被其他语言使用(Scala,Groovy,JRuby,Closure.)。
这篇文章的重点在于JVM规范中描述的运行时数据区(Runtime Data Areas)。这些区域被设计为存储程序或由JVM本身使用的数据。首先我会概述,然后什么是字节码,最后解释不同的数据区域。
概述
JVM是对底层操作系统的抽象,不管底层的硬件和系统是否有差异,相同的代码在JVM上运行的结果保证一致。
example:
- 整型的大小永远都是
32
位,范围-2^31~2^31-1
。 - 存储数据使用大端模式(高地址存放低位)
- Java代码先编译成字节码文件,JVM解析字节码文件。
- 为了避免磁盘I/O,JVM通过类加载器,报字节码文件加载到运行时数据区(RDA)。这些字节码会一直保存,直到JVM停止或类加载器销毁。
- 加载的字节码被执行引擎解析并执行。
- 执行引擎会保存正在执行的状态,以及与底层操作系统的协作。
注意:经常使用的代码会被编译成本地代码。这叫做即时编译(JIT),加快运行速度。
基于堆栈的架构
JVM使用基于堆栈的架构。虽然,对于开发者它是不可见的,但是它对生成的字节码和JVM架构有巨大的影响。
JVM通过执行的Java字节码中描述的基本操作来执行开发人员的代码。操作数是指令操作的值。根据JVM规范,这些操作需要在操作栈中进行。
- 将操作数3,4入栈
- 调用加法指令
- 3,4出栈
- 计算3+4,将结果7入栈
注意: 其它的架构,如基于寄存器的架构,X86架构
的处理器和Android虚拟机Dalvik
使用。
字节码
Java一个字节组成的操作码,有256中,其中,204
个是Java8使用规范的。
比如:
ifeq(0x99)
表示equalsiadd(0x66)
表示addi2l(0x85)
integer 转 longarraylength(0xbe)
给出数组长度pop(0x57)
堆栈第一个元素出栈
简答的加法
JDK提供javap,可以将字节码转成可以理解的语句。javap -verbose Test.class
从上可知,不只是简单的转录Java类
- 包含了
常量池
的描述。常量池JVM数据区的一部分,用于存储类的元数据,如方法名称、参数
…。当一个类被加载到JVM中,这些部分就会放在常量池中。 LineNumberTable
和LocalVariableTable
指定了方法的位置和方法的变量。- 加上了
默认构造函数
- 指定了堆栈的操作信息。
运行时数据区
运行时数据区是设计用来存储数据的内存区域。这些数据是由开发者的程序或JVM其内部工作使用。
堆
堆是所有JVM线程之间共享的内存区域。它是在虚拟机启动时创建的。所有的实例对象和数组被分配到堆中。
如:
堆必须要由垃圾回收器(GC
)管理,当对象不使用时,由GC回收被分配的内存。GC的回收策略由取决于具体的JVM实现(Oracle Hotspot 就实现了很多算法)
堆可以动态扩大或收缩,也可以有固定的最大值和最小值。比如,在Oracle Hotspot里,用户可以指定参数:java -Xms=512m -Xmx=1024m
注意:堆不能超过最大值,否则会抛出OutOfMemoryError
方法区
方法区也是所有JVM线程共享的内存区域。也是在虚拟机启动时创建,并通过类加载器加载。方法区会一直保持,知道类加载器关闭。
方法区保存:
- 类信息(字段、方法、父类名称、接口名称、版本…)
- 方法和构造函数的字节码表示
- 每个类都要加载的运行时常量池
JVM规范并没有强制要求方法区要在堆中实现。例如,Oracle HotSpot使用一种叫PermGen
的区域来存储方法区。这个PermGen区邻居Java的堆,并限制了默认空间大小为64M(XX:MaxPermSize
可以修改)。从java 8开始,HotSpot把方法区存放在叫做Metaspace
的独立的本地内存中。最大的可用空间等于系统可用的内存空间。
注意:如果方法区超过最大的范围,就会抛出OutOfMemoryError
运行时常量池
运行时常量池是方法区的子集。因为它是元数据的重要组成部分,Oracle规定将运行时常量池从方法区中分离出来。这个常量池会随着加载的类和接口而增加。这个常量池就像传统编程语言中的符号表。换句话说,当一个类、方法或字段被引用时,JVM会使用运行时常量池查找真实的内存地址。同时,常量池也保存字符串和基本类型的常量值。
PC寄存器(每线程)
每个线程都有自己的PC(程序计数器)寄存器
,与线程同时创建。任何时候,每一个线程都在执行当前的方法。寄存器指向当前正在执行的命令(在方法区)。
注意:如果方法被本地线程执行,那么寄存器的值是未知的。JVM的寄存器足够保存返回地址或指针。
Java虚拟机栈(每个线程)
Java栈中保存了很多帧。
帧
帧是一种数据结构,它包含了很多数据,这些数据表示正在执行当前方法的线程状态。
- 操作数栈
- 本地变量数组
- 运行时常量池引用
栈
每个Java虚拟机线程有一个专用的Java虚拟机栈,与线程同时创建。栈保存了帧。每调一次方法,新的帧就会被放到栈中。当方法调用结束,帧也销毁了。不管是否正常结束。
只有执行方法的帧,总是保持活动的。这个就是当前帧,它涉及当前方法和当前类。
example
注意:栈是动态的,但是有一个最大尺寸限制,如果递归调用过多,或引发StackOverflowError
Oracle HotSpot 可以修改-Xss
本地方法栈(每个线程)
只是为非Java语言写的本地方法(JNI)使用的栈,所以有操作系统控制。
总结
希望这篇文章可以帮你更好的理解JVM。我觉得栈是重点,因为涉及到JVM的内部功能。
如果想要深入理解:
- 你可以看看JVM规范
- 推荐Understanding JVM Internals