一、Java字节码文件
Java程序经过编译之后会生成class文件,它是供虚拟机解释执行的二进制字节码文件,文件以0xcafebabe(16进制,特殊标志)开头,中间无任何分隔符,如下图(16进制)。
class文件格式复杂,不便于直接阅读,可以使用javap命令来辅助阅读。
javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的指令区、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
二、JVM运行时数据区
JVM是字节码文件的运行环境,它由方法区、堆内存、虚拟机栈、本地方法栈和程序计数器构成。
其中方法区和堆内存由线程共享,虚拟机栈、本地方法栈和程序计数器为线程独占。
线程共享:所有线程均能访问这块内存区域,随虚拟机或者GC创建和销毁。
线程独占:每个线程都有自己独立的空间,随线程生命周期而创建和销毁。
方法区:JVM用来存储加载的类信息、常量、静态变量、编译后的代码等数据。在虚拟机规范中并未定义该区的实现方式,具体实现由各个虚拟机自己决定。如Oracle的Hotspot在Java7中将方法区放在永久代,Java8中将方法区放到了元数据空间,并且通过GC机制对这个区域进行管理。
堆内存:可以细分为老年代(Old)、新生代(Eden),它是JVM启动时创建存放对象实例的地方。垃圾回收器主要就是管理堆内存,如果满了就会出现OutOfMemoryError。
虚拟机栈:每个线程都在虚拟机栈中有一块私有的空间。线程栈由多个栈帧(Stack Frame)组成。一个线程会执行一个或多个方法,一个方法对应一个栈帧。栈帧内容包括:局部变量表、操作数栈、动态链接、方法返回地址、附加信息等。栈内存默认最大是1M,超出则抛StackOverflowError。
本地方法栈:和虚拟机栈功能类似,虚拟机栈是为虚拟机执行Java方法而准备的,本地方法栈是为
虚拟机使用Native本地方法而准备的。虚拟机规范没有规定具体的实现,由不同的虚拟机厂商去实现。HotSpot虚拟机中本地方法栈和虚拟机栈的实现是一样的。超过栈内存大小也会抛出StackOverflowError。
程序计数器(Program Counter Register):记录当前线程执行字节码的位置,存储字节码指令地址,如果执行Native方法,则计数器值为空。每个线程都在该空间中有一个私有的空间,占用极少内存。Java程序运行时,JVM多线程会轮流切换并分配CPU,同一时间CPU只会执行一条线程中的指令。线程切换后,需要通过程序计数器来恢复到该线程的正确执行位置。
三、JVM程序运行演示
1. Demo源码
package test;
public class Demo {
public static void main(String[] args) {
int x = 500;
int y = 100;
int a = x/y;
int b = 50;
System.out.println(a + b);
}
}
2. 编译Demo
javac Demo.java
3. 使用Javap命令查看编译后的字节码文件
javap -v Demo.class > Demo.txt
4. Demo.txt内容解析
版本号/访问控制
public class test.Demo
minor version: 0 //次版本号
major version: 52 //主版本号
flags: ACC_PUBLIC, ACC_SUPER //访问标志
版本号规则:JDK5、6、7、8分别对应49、50、51、52
常量池
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // test/Demo
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 Demo.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 test/Demo
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
类信息包含的静态常量,编译之后就能确认
构造函数
public test.Demo();
descriptor: ()V
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
Demo程序中,并未编写构造函数,此为隐式的无参构造函数。
程序入口main方法
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: sipush 500
3: istore_1
4: bipush 100
6: istore_2
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: bipush 50
13: istore 4
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_3
19: iload 4
21: iadd
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: return
LineNumberTable:
line 6: 0
line 7: 4
line 8: 7
line 9: 11
line 10: 15
line 11: 25
flags描述了该方法的访问控制,stack代表该方法对应栈帧中操作数栈的深度,locals为本地变量数量,包括入参、x、y、a、b,一共5个。args_size为入参的数量。
紧接着的是main方法内容编译后的指令,每条指令前的数字代表偏移量(字节),JVM根据这个偏移量区分不同的指令。需要注意的是,class文件中存储的是指令码,而当前展示的是经javap翻译后的操作符。
5. JVM运行过程
(1)加载信息到方法区
(2)JVM创建线程执行代码
(3)JVM执行main方法指令
执行序号0的指令,将500压入操作数栈
执行序号3的指令,弹出操作数栈栈顶的500,保存到本地变量表
执行序号4的指令,将100压入操作数栈
执行序号6的指令,弹出操作数栈栈顶100,保存到本地变量表
执行序号7的指令
执行序号8的指令
执行序号9的指令,将栈顶两个数据相除,结果入栈
执行序号10的指令
执行序号11的指令
执行序号13的指令
执行序号15的指令
执行序号18的指令
执行序号19的指令
执行序号21的指令
执行序号22的指令
执行序号25的指令
main方法执行结束。