学习java使用java很久了,都知道java可以“一处编写,到处运行”。那么一个普通的java类到底是如何被java虚拟机执行的呢,我们可以一起来探究一下。
在探究java运行原理之前,我们需要首先对一些概念有一定的了解。
一、class文件
java代码被执行的第一步就是首先由.java文件编译为.class文件,然后再由java虚拟机来根据class文件中的信息来进行执行代码,输出结果。那么首先我们来了解class文件的一些基本信息。
class文件包含java程序执行的字节码;数据严格按照格式紧凑排列在class文件中的二进制流,中间无任何分隔符;文件开头有一个0xcafebabe(16进制)特殊的一个标志。
我们可以通过notepad++的HEX-Editor来打开一个class文件大致的看一下这个结构,如下图所示
上面的结构是16进制表示的结果,我们可以看到开头的0xcafebabe这个标志,jvm虚拟机在加载class文件的时候,就会通过这个标记来做校验,如果没有这个标记,校验就不会通过。肉眼我们看不出来里面都包含哪些信息,实际上里面的内容主要有以下几部分:
版本:指的是jdk的版本
访问标志:指这个类的访问属性如public
常量池:...
当前类:...
超级类:...
接口:...
字段:...
方法:...
属性:...
大家看上面的这些信息可能没有什么头绪,一会我们将会通过javap的命令来详细的看一看class文件中的内容到底是什么。接下来我们来了解另一个部分
二、jvm运行时数据区
jvm运行时数据区都包含什么呢?画个草图
1、线程共享部分
所有线程都能访问这块内存区域,随着虚拟机或者GC而创建和销毁
方法区
方法区是jvm用来存储加载的类信息、常量、静态变量、编译后的代码等数据的。
虚拟机规范中这是一个逻辑区域,并没有规定具体的实现方式,具体的实现根据不同的虚拟机决定。例如oracle的HotSpot在java7中,将方法区放在永久代,java8之后永久代被消除,方法区就被放在了元数据空间。元数据空间也是通过GC机制对这个区域进行管理。
堆内存:
堆内存是存放对象的区域。可以细分为两个部分:老年代、新生代。
新生代又可以分为三个部分:Eden、From Survivor、To Survivor。
垃圾回收机制主要管理的就是这个区域。具体的一些垃圾回收算法在后续章节我们会有深入的了解
2、线程独占部分
每个线程都会有它独立的空间,随着线程生命周期而创建和销毁
虚拟机栈
每个线程都在这个空间有一个私有的空间。
线程由多个栈帧(Stack Feame)组成。
一个线程会执行一个或者多个方法,一个方法对应一个栈帧。
栈帧内容包括:局部变量表,操作数栈,动态链接、方法返回地址、附加信息等
栈内存默认最大是1M。超出则会抛出StackOverFlowError
本地方法栈
和虚拟机栈功能类似,虚拟机栈是为虚拟机执行java方法而准备的,本地方法栈是为虚拟机使用Native本地方法而准备的。
虚拟机规范中也没有规定本地方法栈的具体实现。由不同的虚拟机厂商去实现。
HotSpot虚拟机中虚拟机栈和本地方法栈实现方式一样,同样超出大小会抛出StackOverFlowError.
程序计数器(Program Counter Register)
程序计数器记录的是当前线程执行字节码的位置,存储的是字节码的指令地址。就是说当前线程的下一秒或者下一步要执行的是哪一条命令,是由程序计数器来控制的。
每一个线程都会在这一个空间有一个私有的空间,占用的内存很小。
CPU同一时间内,只会执行一条线程中的指令,JVM多线程的实现就是通过分配不同的线程的执行时间来实现的,所以当线程切换之后,我们需要通过程序计数器去获取到上一次执行这个线程到了什么位置,进而将这个线程回复到正确的执行位置。
上面的辅助知识我们已经了解的差不多了,接下来我们通过一个简单的java代码来探究一下java程序的运行流程。
三、实际探究
1、编写Demo.java程序
public class Demo{
public static void main(String[] args){
int x = 200;
int y = 100;
int a = x/y;
int b = 20;
System.out.println(a+b);
}
}
2、通过javac命令编译该文件
javac Demo.java
3、通过javap命令获取class文件详细解析内容
javap -v Demo.class > Demo.txt
4、分析class文件信息
注意:作者使用的环境为:
win7旗舰版
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
上Demo.txt文件
Classfile /E:/code/Demo.class
Last modified 2020-4-13; size 412 bytes
MD5 checksum 3d8dade4567c7be9622397f9f58bcfdc
Compiled from "Demo.java"
public class Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // 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 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 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 1: 0
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 200
3: istore_1
4: bipush 100
6: istore_2
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: bipush 20
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 3: 0
line 4: 4
line 5: 7
line 6: 11
line 7: 15
line 8: 25
}
SourceFile: "Demo.java"
接下来我们分析一下这个文件的内容。前面五行是一些基本信息不用看,直接从第六行开始
minor version: 0 --java副版本号
major version: 52 --java主版本号 java的版本号规则:JDK5,6,7,8分别对应49,50,51,52
flags: ACC_PUBLIC, ACC_SUPER 表示这个java文件的访问标志
关于访问标志对应的信息如下图表格所示:
接下来就到了Constant pool 常量池的信息了。这里面的主要内容其实就是类相关的一些基本信息,比如类用到的一些常量,方法等这里都有相关的,后面的main方法中还会引用到这里面的东西。
关于常量池中的一些字段的解释
接下来我们主要看一下我们的main方法中的内容。
stack=3, locals=5, args_size=1
表示:
方法对应栈帧中操作数栈的深度为3
本地变量有5个,注意此处是我们自己定义的四个变量加上一个main方法的args入参
参数数量有一个为args
再往下的部分我们看到的英文单词交操作符,实际代表的是class文件中的呗jvm虚拟机执行的指令码。因为实际class中存储的并不是咱们在这个TXT文件中看到的这个英文单词,而是一些十六进制的指令。关于操作符和指令吗对应关系请移步下面的链接:
接下来我们将重点分析main方法执行的过程,首选在大家脑子里构思出来这样一个结构。下面的截图就是程序初始化还没有开始执行的时候,虚拟机栈和程序计数器的大致情况。
需要提醒大家一点的是左侧的这一列数字,其实就是程序计数器中存放的值,他就是么一条命令的索引值,可以通过这个值获取到我们要执行的命令是什么。
接下来开始一点一步一步分析上面的16个指令码的含义
0: sipush 200
含义:将int值200入栈(压栈)
3: istore_1
含义:将栈顶int类型值保存到局部变量1中(弹栈,此时操作数中为空)
4: bipush 100
含义:将100扩展成int值入栈。我们发现这个命令和200的入栈命令不一样,其实这是因为不同的大小的数值的入栈指令不同而已。
6: istore_2
含义:将栈顶int类型值保存到局部变量2中(弹栈,此时操作数栈为空)
7: iload_1
含义:从局部变量1中装载int类型值入栈
8: iload_2
含义:从局部变量2中装载int类型值入栈
上面两步属于相同操作,执行结束之后会栈中情况如下图所示。注意压栈原理
9: idiv
含义: 将栈顶两个int类型值相除,并且结果入栈
10: istore_3
含义:将栈顶int类型值保存到局部变量3中(弹栈,此时操作数栈为空)
11: bipush 20
含义:将20扩展成int值入栈
13: istore 4
含义:将栈顶int类型值保存到局部变量indexbyte中,此处的indexByte就是4,也就是说保存到局变量4中
此处省略了变量20的入栈过程,直接将上面两步的执行结果给大家展示出来了。
大家在看这个分析过程的时候,如有不懂的命令请查阅上面的jvm指令码表,全部都有。
到这一步位置,我们可以看到局部变量表中已经保存满了、大家可能会好奇,为什么会正好装满呢?因为jvm虚拟机在分配内存的时候就是根据你实际情况来做的。
15: getstatic #2
含义:获取静态字段#2的值,放入栈顶
#2就是常量池中的属性引用。我们也可以结合着常量池中的内容来看这个命令,如下图,我们发现其实代码这个时候已经执行到了System.out.println(a+b);这一句了,那么这一句是怎么执行的呢,我们接着往下面看
18: iload_3
含义:将变量3的int值入栈
19: iload 4
含义:将变量4的int值入栈
21: iadd
含义:将栈顶两个int类型值相加,结果入栈
22: invokevirtual #3
含义:运行时方法调用绑定方法#3
此时jvm会根据#3这个方法的描述,重新开启一个线程执行这个方法,那么我们知道新的方法就要开启一个新的线程,对应的会有一个新的方法栈帧,这个时候main方法停止执行,方法中的参数从操作数栈中弹栈出来,押入虚拟机栈中新的这个方法栈帧中,继续执行。
至于sysout具体是怎么执行的我们这里不做深入的了解,最后这句代码之后之后,会在控制条打印一条细信息。
25: return
含义:void函数返回。
返回之后线程被销毁,上面的虚拟机栈这些信息都会被收回。java程序的执行就到此结束了。这就是java程序的运作过程。
分析完这个过程,其实我们可以总结一下:java程序运行的过程,其实就是jvm虚拟机对本地变量表、操作数栈等等这些线程里面的信息在做操作,最终实现程序运行出来的效果的一个过程。
这只是一个简单的例子,复杂的例子其实也是由这些简单的例子一点一点完成之后呈现出来给我们的效果。
通过这样的一个过程,我们大家可以整体上对于java虚拟机内部运行代码的过程有一个大致的感知,从此再也不是一头雾水。可以更深刻的去理解内部的本质,同时对于我们后期的调优过程也是一个铺垫。
千里之行始于足下,大家一起学习吧。
原创不易,转载请注明出处!
有任何问题欢迎留言,一起交流进步~