点击上方“Java技术驿站”,选择“置顶公众号”。
有内涵、有价值的文章第一时间送达!
精品专栏
作者:某人的喵星人 原文:https://www.cnblogs.com/dqrcsc/p/4671879.html
简单说来,一个java程序的运行需要编辑源码、编译生成class文件、加载class文件、解释或编译运行class中的字节码指令。
下面有一段简单的java源码,通过它来看一下java程序的运行流程:
class Person{ private String name; private int age; public Person(int age, String name){ this.age = age; this.name = name; } public void run(){ }}interface IStudyable{ public int study(int a, int b);}public class Student extends Person implements IStudyable{ private static int cnt=5; static{ cnt++; } private String sid; public Student(int age, String name, String sid){ super(age,name); this.sid = sid; } public void run(){ System.out.println("run()..."); } public int study(int a, int b){ int c = 10; int d = 20; return a+b*c-d; } public static int getCnt(){ return cnt; } public static void main(String[] args){ Student s = new Student(23,"dqrcsc","20150723"); s.study(5,6); Student.getCnt(); s.run(); }}
1、编辑源码
无论是使用记事本还是别的什么,编写上面的代码,然后保存到Student.java,我直接就放到桌面了
2.编译生成class字节码文件
打开命令窗口,输入命令javac Student.java将该源码文件编译生成.class字节码文件。
由于在源码文件中定义了两个类,一个接口,所以生成了3个.clsss文件:
这样能在java虚拟机上运行的字节码文件就生成了
启动java虚拟机运行字节码文件
在命令行中输入 javaStudent
这个命令,就启动了一个 java 虚拟机,然后加载 Student.class 字节码文件到内存,然后运行内存中的字节码指令了。
我们从编译到运行 java 程序,只输入了两个命令,甚至,如果使用集成开发环境,如 eclipse,只要 ctrl+s 保存就完成了增量编译,只需要按下一个按钮就运行了 java 程序。但是,在这些简单操作的背后还有一些操作……
字节码文件,看似很微不足道的东西,却真正实现了 java 语言的跨平台。各种不同平台的虚拟机都统一使用这种相同的程序存储格式。更进一步说,jvm 运行的是 class 字节码文件,只要是这种格式的文件就行,所以,实际上 jvm 并不像我之前想象地那样与 java 语言紧紧地捆绑在一起。如果非常熟悉字节码的格式要求,可以使用二进制编辑器自己写一个符合要求的字节码文件,然后交给 jvm 去运行;或者把其他语言编写的源码编译成字节码文件,交给 jvm 去运行,只要是合法的字节码文件, jvm 都会正确地跑起来。所以,它还实现了跨语言……
通过 jClassLib 可以直接查看一个 .class 文件中的内容,也可以给 JDK 中的 javap 命令指定参数,来查看 .class 文件的相关信息:
javap–vStudent
好多输出,在命令行窗口查看不是太方便,可以输出重定向下:
javap–vStudent>Student.class.txt
桌面上多出了一个 Student.class.txt
文件,里面存放着便于阅读的Student.class文件中相关的信息
里面的内容如下(部分):
部分 class 文件内容,从上面图中,可以看到这些信息来自于 Student.class ,编译自 Student.java ,编译器的主版本号是 52,也就是 jdk1.8,这个类是 public ,然后是存放类中常量的常量池,各个方法的字节码等,这里就不一一记录了。
总之,我想说的就是字节码文件很简单很强大,它存放了这个类的各种信息:字段、方法、父类、实现的接口等各种信息。
Java 虚拟机要运行字节码指令,就要先加载字节码文件,谁来加载,怎么加载,加载到哪里……谁来运行,怎么运行,同样也要考虑……
上面是一个JVM的基本结构及内存分区的图,有点抽象,简单说明下:
JVM中把内存分为直接内存、方法区、Java栈、Java堆、本地方法栈、PC寄存器等。
JVM的功能模块主要包括类加载器、执行引擎和垃圾回收系统。
privatestaticintcnt=5;
此时,并不会执行赋值为5的操作,而是将其初始化为0。<clinit>()
。此时,执行引擎会调用这个方法对静态字段进行代码中编写的初始化操作。在 Student.java 中关于静态字段的赋值及静态代码块有两处:
private static int cnt=5;static{ cnt++;}
将按出现顺序拼接,形式如下:
void <clinit>(){ cnt = 5; cnt++;}
可以通过 jClassLib 这个工具看到生成的 <clinit>()
方法的字节码指令:
从字节码来看,确实先后执行了 cnt=5
及 cnt++
这两行代码。
在这里有一点要注意的是,这里笼统的描述了下类的加载及初始化过程,但是,实际中,有可能只进行了类加载,而没有进行初始化工作,原因就是在程序中并没有访问到该类的字段及方法等。
此外,实际加载过程也会相对来说比较复杂,一个类加载之前要加载它的父类及其实现的接口:加载的过程可以通过java –XX:+TraceClassLoading参数查看:
如: java-XX:+TraceClassLoadingStudent
,信息太多,可以重定向下:
查看输出的 loadClass.txt 文件:
可以看到最先加载的是 Object.class 这个类,当然了,所有类的父类。
直到第 390 行才看到自己定义的部分被加载,先是 Studen t实现的接口 IStudyable ,然后是其父类 Person ,然后才是 Student 自身,然后是一个启动类的加载,然后就是找到 main() 方法,执行了。
要了解方法的运行,需要先稍微了解下 java 栈:
JVM 中通过 java 栈,保存方法调用运行的相关信息,每当调用一个方法,会根据该方法的在字节码中的信息为该方法创建栈帧,不同的方法,其栈帧的大小有所不同。栈帧中的内存空间还可以分为3块,分别存放不同的数据:
只有当前正在运行的方法的栈帧位于栈顶,当前方法返回,则当前方法对应的栈帧出栈,当前方法的调用者的栈帧变为栈顶;当前方法的方法体中若是调用了其他方法,则为被调用的方法创建栈帧,并将其压入栈顶。
注意:局部变量表及操作数栈的最大深度在编译期间就已经确定了,存储在该方法字节码的Code属性中。
简单看下main()方法:
public static void main(String[] args){ Student s = new Student(23,"dqrcsc","20150723"); s.study(5,6); Student.getCnt(); s.run();}
对应的字节码,两者对照着看起来更易于理解些:
注意main()方法的这几个信息:
开始模拟main()中一条条字节码指令的运行:
创建栈帧:
局部变量表长度为 2,slot0 存放参数 args ,slot1 存放局部变量 Student s,操作数栈最大深度为 5。
new #7 指令:在 java 堆中创建一个 Student 对象,并将其引用值放入栈顶。
invokespecial #10:调用#10这个常量所代表的方法,即Student.()这个方法
<init>()
方法,是编译器将调用父类的 <init>()
的语句、构造代码块、实例字段赋值语句,以及自己编写的构造方法中的语句整合在一起生成的一个方法。保证调用父类的 <init>()
方法在最开头,自己编写的构造方法语句在最后,而构造代码块及实例字段赋值语句按出现的顺序按序整合到 <init>()
方法中。
注意到 Student.<init>()
方法的最大操作数栈深度为 3,局部变量表大小为 4。
此时需注意:从 dup 到 ldc #9 这四条指令向栈中添加了4个数据,而Student.()方法刚好也需要4个参数:
public Student(int age, String name, String sid){ super(age,name); this.sid = sid;}
虽然定义中只显式地定义了传入3个参数,而实际上会隐含传入一个当前对象的引用作为第一个参数,所以四个参数依次为this,age,name,sid。
上面的4条指令刚好把这四个参数的值依次入栈,进行参数传递,然后调用了Student.()方法,会创建该方法的栈帧,并入栈。栈帧中的局部变量表的第0到4个slot分别保存着入栈的那四个参数值。
创建 Studet.<init>()
方法的栈帧:
Student.()方法中的字节码指令:
从Person.()返回之后,用于传参的栈顶的3个值被回收了。
重新回到main()方法中,继续执行下面的字节码指令:
astore_1:将当前栈顶引用类型的值赋值给slot1处的局部变量,然后出栈。
创建study()方法的栈帧:
最大栈深度3,局部变量表5
方法的java源码:
public int study(int a, int b){ int c = 10; int d = 20; return a+b*c-d;}
对应的字节码:
注意到这里,通过 jClassLib 工具查看的字节码指令有点问题,与源码有偏差……
改用通过命令 javap –v Student 查看 study() 的字节码指令:
上面4条指令,完成对c和d的赋值工作。
iload1、iload2、iload_3这三条指令将slot1、slot2、slot3这三个局部变量入栈:
重新回到main()方法中:
以上,就是一个简单程序运行的大致过程