目录
一、字节码文件的秘密
二、JVM运行时数据区
三、JVM工作原理
四、小结
说在后面
java入门的第一课,都会讲用java语言开发软件的基本流程,分三步:1、编写源代码,即.java文件;2、将.java文件编译为字节码文件,即.class文件;3、在装有JRE(Java Runtime Environment,java运行时环境)的电脑上运行你编写的软件。那么.class文件里是什么内容呢?JVM虚拟机是怎么工作的呢?让我们一起探究。
一、字节码文件的秘密
.class文件由java源代码文件经过编译器编译得到(编译命令:javac Xxx.java),称为字节码文件,其数据是严格按照格式紧凑排列在.class文件中的二进制流,中间无任何分隔符。用编辑工具打开该文件,我们可以看到,文件开头有一个cafe babe的特殊标志。
图1-1 java
源代码
图1-2 字节码文件(16 进制)
肉眼直接看字节码文件肯定看不懂,我们需要一双火眼金睛。我们可以通过javap命令我们可以将字节码文件转换为指令码文件。如,javap –v Demo1.class>Demo1.txt,这个命令可以将Demo1.class字节码文件转换为Demo1.txt指令码文件,如下:
Classfile /F:/Demo1.class
Last modified 2019-3-5; size 414 bytes
MD5 checksum ae6fa820973681b35609c75631cb255b
Compiled from "Demo1.java"
public class Demo1
minor version: 0//次版本号
major version: 52//主版本号(JDK 5/6/7/8分别对应49/50/51/52)
flags: ACC_PUBLIC, ACC_SUPER//访问标志
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 // Demo1
#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 Demo1.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 Demo1
#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 Demo1();//构造函数start
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 1: 0//构造函数end
public static void main(java.lang.String[]);//main方法start
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC//访问控制
Code:
//方法对应栈帧中操作数栈的深度stack=3;本地变量数量locals=5;参数数量args_size=1
stack=3, locals=5, args_size=1
0: sipush 500//指令码start//详细请查阅《JVM指令码表》
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//指令码end//main方法end
LineNumberTable:
line 3: 0
line 4: 4
line 5: 7
line 6: 11
line 7: 15
line 8: 25
}
SourceFile: "Demo1.java"
(以上“//注释部分”为作者添加的注释内容)。
通过指令码文件,我们可以发现,字节码文件的内容主要包括:版本、访问标志、常量池、当前类、超级类、接口、字段、方法、属性等几部分内容。
将大象放进冰箱分:1、打开冰箱门;2、将大象放进去;3、关上冰箱门。类似的,字节码文件就像是冰箱的操作说明书,JVM加载完字节码文件之后就可以按部就班的运行了。具体某个指令对应的操作内容可以查阅《JVM指令码表》,下图是部分截图。
图1-3 指令码表
二、JVM运行时数据区
在讲解JVM具体是如何运行之前,让我们先了解一下JVM运行时数据区有哪些?
图1-4 JVM 运行时数据区
其中,方法区、堆内存是线程共享区域,即所有线程都能够访问到这两个区域,随JVM虚拟机或者GC而创建和销毁。虚拟机栈、本地方法栈、程序计数器,属于线程独占区域,即JVM会为每个线程划分一个独立的空间,随着线程生命周期而创建和销毁。
方法区:主要存储加载的类信息、常量、静态变量、编译后的代码等数据;
堆内存:JVM启动时创建,用于存放对象的实例,如果堆内存不够了会报OutOfMemroyError。堆内存还可以细分为老年代、新生代,新生代又分为Eden、From Survivor或S1、To Survivor或S2,三个区域。
虚拟机栈:由多个栈帧(Stack Frame)组成,一个方法对应一个栈帧,是每个线程私有的空间。栈帧包括局部变量表、操作数栈、动态链接、方法返回地址、附加信息等内容。虚拟机栈默认最大是1M,超出后报StackOverflowError。
本地方法栈:为运行Native本地方法准备的内存区域。
程序计数器(Program Counter Register):记录当前线程执行字节码的位置(字节码指令地址),如果执行Native方法,则其值为空。
三、JVM工作原理
知道了JVM运行时数据区的划分,接下来我们看看JVM是怎么工作的。
首先,JVM会加载指定的字节码文件到方法区中;然后,JVM会创建线程来执行字节码文件中的指令;执行完或者有异常情况,退出程序。
我们以Demo1.txt指令码文件为例:
JVM将字节码文件加载到方法区,然后从main方法开始执行。
public static void main(java.lang.String[]);//main方法start
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC//访问控制
Code:
//方法对应栈帧中操作数栈的深度stack=3;本地变量数量locals=5;参数数量args_size=1
stack=3, locals=5, args_size=1
0: sipush 500//指令码start
3: istore_1
4: bipush 100
以上是main方法的部分指令码,JVM对应状态如下图:
图1-5 程序入口main 方法
执行的第一个指令是,0: sipush 500。查《JVM指令码表》,得:
图 1-6 查《 JVM 指令码表》
即,将500压入到线程的操作数栈中。
图1-7 执行sipush 指令
同理,3:istore_1,的含义是:将操作数栈顶int类型值存储到本地变量表1的位置。
图1-8 查istore 指令含义
图1-9 执行istore_1 指令
下一行,bipush:valuebyte值带符号扩展成int值入栈。
图1-10 执行bipush 100 指令
图1-11 执行istore_2 指令
图1-12 执行iload_1 指令
图1-13 执行iload_2 指令
图1-14 执行idiv 指令
图1-15 执行istore_3 指令
图1-16 执行bipush 50 指令
图1-17 执行istore 4 指令
图1-18 执行getstatic #2 指令
图1-19 执行iload_3 指令
图1-20 执行iload 4 指令
图1-21 执行iadd 指令
图1-22 执行invokevirtual #3 指令(调用System.out.println() 方法)
图1-23 程序执行结束
四、小结
这一节我们学习了字节码文件、JVM运行时数据区、JVM运行原理及示例,这些内容偏底层,如果有兴趣可以进一步研究。