java拾遗4----一个简单java程序的运行全过程

时间:2021-10-09 08:14:29

简单说来,一个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,我直接就放到桌面了

java拾遗4----一个简单java程序的运行全过程

2.编译生成class字节码文件:

在桌面上,按住shift,然后按下鼠标右键:

java拾遗4----一个简单java程序的运行全过程

点击“在此处打开命令窗口”

java拾遗4----一个简单java程序的运行全过程

输入命令javac Student.java将该源码文件编译生成.class字节码文件。

由于在源码文件中定义了两个类,一个接口,所以生成了3个.clsss文件:

java拾遗4----一个简单java程序的运行全过程

这样能在java虚拟机上运行的字节码文件就生成了

3.启动java虚拟机运行字节码文件:

java拾遗4----一个简单java程序的运行全过程

在命令行中输入java Student这个命令,就启动了一个java虚拟机,然后加载Student.class字节码文件到内存,然后运行内存中的字节码指令了。

我们从编译到运行java程序,只输入了两个命令,甚至,如果使用集成开发环境,如eclipse,只要ctrl+s保存就完成了增量编译,只需要按下一个按钮就运行了java程序。但是,在这些简单操作的背后还有一些操作……

java拾遗4----一个简单java程序的运行全过程

1.从源码到字节码:

字节码文件,看似很微不足道的东西,却真正实现了java语言的跨平台。各种不同平台的虚拟机都统一使用这种相同的程序存储格式。更进一步说,jvm运行的是class字节码文件,只要是这种格式的文件就行,所以,实际上jvm并不像我之前想象地那样与java语言紧紧地捆绑在一起。如果非常熟悉字节码的格式要求,可以使用二进制编辑器自己写一个符合要求的字节码文件,然后交给jvm去运行;或者把其他语言编写的源码编译成字节码文件,交给jvm去运行,只要是合法的字节码文件,jvm都会正确地跑起来。所以,它还实现了跨语言……

通过jClassLib可以直接查看一个.class文件中的内容,也可以给JDK中的javap命令指定参数,来查看.class文件的相关信息:

javap –v Student

java拾遗4----一个简单java程序的运行全过程

好多输出,在命令行窗口查看不是太方便,可以输出重定向下:

javap –v Student > Student.class.txt

java拾遗4----一个简单java程序的运行全过程

桌面上多出了一个Student.class.txt文件,里面存放着便于阅读的Student.class文件中相关的信息

java拾遗4----一个简单java程序的运行全过程

java拾遗4----一个简单java程序的运行全过程

部分class文件内容,从上面图中,可以看到这些信息来自于Student.class,编译自Student.java,编译器的主版本号是52,也就是jdk1.8,这个类是public,然后是存放类中常量的常量池,各个方法的字节码等,这里就不一一记录了。

总之,我想说的就是字节码文件很简单很强大,它存放了这个类的各种信息:字段、方法、父类、实现的接口等各种信息。

2.Java虚拟机的基本结构及其内存分区

Java虚拟机要运行字节码指令,就要先加载字节码文件,谁来加载,怎么加载,加载到哪里……谁来运行,怎么运行,同样也要考虑……

java拾遗4----一个简单java程序的运行全过程

上面是一个JVM的基本结构及内存分区的图,有点抽象,有点丑……简单说明下:

JVM中把内存分为直接内存、方法区、Java栈、Java堆、本地方法栈、PC寄存器等。

直接内存:就是原始的内存区

方法区:用于存放类、接口的元数据信息,加载进来的字节码数据都存储在方法区

Java栈:执行引擎运行字节码时的运行时内存区,采用栈帧的形式保存每个方法的调用运行数据

本地方法栈:执行引擎调用本地方法时的运行时内存区

Java堆:运行时数据区,各种对象一般都存储在堆上

PC寄存器:功能如同CPU中的PC寄存器,指示要执行的字节码指令。

JVM的功能模块主要包括类加载器、执行引擎和垃圾回收系统

3.类加载器加载Student.class到内存

1)类加载器会在指定的classpath中找到Student.class这个文件,然后读取字节流中的数据,将其存储在方法区中。

2)会根据Student.class的信息建立一个Class对象,这个对象比较特殊,一般也存放在方法区中,用于作为运行时访问Student类的各种数据的接口。

3)必要的验证工作,格式、语义等

4)为Student中的静态字段分配内存空间,也是在方法区中,并进行零初始化,即数字类型初始化为0,boolean初始化为false,引用类型初始化为null等。

在Student.java中只有一个静态字段:

private static int cnt=5;

此时,并不会执行赋值为5的操作,而是将其初始化为0。

5)由于已经加载到内存了,所以原来字节码文件中存放的部分方法、字段等的符号引用可以解析为其在内存中的直接引用了,而不一定非要等到真正运行时才进行解析。

6)在编译阶段,编译器收集所有的静态字段的赋值语句及静态代码块,并按语句出现的顺序拼接出一个类初始化方法<clinit>()。此时,执行引擎会调用这个方法对静态字段进行代码中编写的初始化操作。

在Student.java中关于静态字段的赋值及静态代码块有两处:

 private static int cnt=5;

 static{

           cnt++;

 }

将按出现顺序拼接,形式如下:

 void <clinit>(){

               cnt = 5;

               cnt++;

 }

可以通过jClassLib这个工具看到生成的<clinit>()方法的字节码指令:

java拾遗4----一个简单java程序的运行全过程

iconst_5指令把常数5入栈

putstatic #6将栈顶的5赋值给Student.cnt这个静态字段

getstatic #6 获取Student.cnt这个静态字段的值,并将其放入栈顶

iconst_1 把常数1入栈

iadd 取出栈顶的两个整数,相加,结果入栈

putstatic #6 取出栈顶的整数,赋值给Student.cnt

return 从当前方法中返回,没有任何返回值。

从字节码来看,确实先后执行了cnt =5 及 cnt++这两行代码。

在这里有一点要注意的是,这里笼统的描述了下类的加载及初始化过程,但是,实际中,有可能只进行了类加载,而没有进行初始化工作,原因就是在程序中并没有访问到该类的字段及方法等。

此外,实际加载过程也会相对来说比较复杂,一个类加载之前要加载它的父类及其实现的接口:加载的过程可以通过java –XX:+TraceClassLoading参数查看:

如:java -XX:+TraceClassLoading Student,信息太多,可以重定向下:

java拾遗4----一个简单java程序的运行全过程

查看输出的loadClass.txt文件:

java拾遗4----一个简单java程序的运行全过程

可以看到最先加载的是Object.class这个类,当然了,所有类的父类。

java拾遗4----一个简单java程序的运行全过程

直到第390行才看到自己定义的部分被加载,先是Student实现的接口IStudyable,然后是其父类Person,然后才是Student自身,然后是一个启动类的加载,然后就是找到main()方法,执行了。

4.执行引擎找到main()这个入口方法,执行其中的字节码指令:

要了解方法的运行,需要先稍微了解下java栈:

JVM中通过java栈,保存方法调用运行的相关信息,每当调用一个方法,会根据该方法的在字节码中的信息为该方法创建栈帧,不同的方法,其栈帧的大小有所不同。栈帧中的内存空间还可以分为3块,分别存放不同的数据:

局部变量表:存放该方法调用者所传入的参数,及在该方法的方法体中创建的局部变量。

操作数栈:用于存放操作数及计算的中间结果等。

其他栈帧信息:如返回地址、当前方法的引用等。

只有当前正在运行的方法的栈帧位于栈顶,当前方法返回,则当前方法对应的栈帧出栈,当前方法的调用者的栈帧变为栈顶;当前方法的方法体中若是调用了其他方法,则为被调用的方法创建栈帧,并将其压入栈顶。

java拾遗4----一个简单java程序的运行全过程

注意:局部变量表及操作数栈的最大深度在编译期间就已经确定了,存储在该方法字节码的Code属性中。

简单查看Student.main()的运行过程:

简单看下main()方法:

 public static void main(String[] args){

               Student s = new Student(23,"dqrcsc","20150723");

               s.study(5,6);

               Student.getCnt();

               s.run();

 }

对应的字节码,两者对照着看起来更易于理解些:

java拾遗4----一个简单java程序的运行全过程

注意main()方法的这几个信息:

java拾遗4----一个简单java程序的运行全过程

Mximum stack depth指定当前方法即main()方法对应栈帧中的操作数栈的最大深度,当前值为5

Maximum local variables指定main()方法中局部变量表的大小,当前为2,及有两个slot用于存放方法的参数及局部变量。

Code length指定main()方法中代码的长度。

开始模拟main()中一条条字节码指令的运行:

创建栈帧:

java拾遗4----一个简单java程序的运行全过程

局部变量表长度为2,slot0存放参数args,slot1存放局部变量Student s,操作数栈最大深度为5。

new #7指令:在java堆中创建一个Student对象,并将其引用值放入栈顶。

java拾遗4----一个简单java程序的运行全过程

dup指令:复制栈顶的值,然后将复制的结果入栈。

bipush 23:将单字节常量值23入栈。

ldc #8:将#8这个常量池中的常量即”dqrcsc”取出,并入栈。

ldc #9:将#9这个常量池中的常量即”20150723”取出,并入栈。

java拾遗4----一个简单java程序的运行全过程

invokespecial #10:调用#10这个常量所代表的方法,即Student.<init>()这个方法

<init>()方法,是编译器将调用父类的<init>()的语句、构造代码块、实例字段赋值语句,以及自己编写的构造方法中的语句整合在一起生成的一个方法。保证调用父类的<init>()方法在最开头,自己编写的构造方法语句在最后,而构造代码块及实例字段赋值语句按出现的顺序按序整合到<init>()方法中。

java拾遗4----一个简单java程序的运行全过程

注意到Student.<init>()方法的最大操作数栈深度为3,局部变量表大小为4。

此时需注意:从dup到ldc #9这四条指令向栈中添加了4个数据,而Student.<init>()方法刚好也需要4个参数:

 public Student(int age, String name, String sid){

               super(age,name);

               this.sid = sid;

 }

虽然定义中只显式地定义了传入3个参数,而实际上会隐含传入一个当前对象的引用作为第一个参数,所以四个参数依次为this,age,name,sid。

上面的4条指令刚好把这四个参数的值依次入栈,进行参数传递,然后调用了Student.<init>()方法,会创建该方法的栈帧,并入栈。栈帧中的局部变量表的第0到4个slot分别保存着入栈的那四个参数值。

创建Studet.<init>()方法的栈帧:

java拾遗4----一个简单java程序的运行全过程

Student.<init>()方法中的字节码指令:

java拾遗4----一个简单java程序的运行全过程

aload_0:将局部变量表slot0处的引用值入栈

aload_1:将局部变量表slot1处的int值入栈

aload_2:将局部变量表slot2处的引用值入栈

java拾遗4----一个简单java程序的运行全过程

invokespecial #1:调用Person.<init>()方法,同调用Student.<init>过程类似,创建栈帧,将三个参数的值存放到局部变量表等,这里就不画图了……

从Person.<init>()返回之后,用于传参的栈顶的3个值被回收了。

aload_0:将slot0处的引用值入栈。

aload_3:将slot3处的引用值入栈。

java拾遗4----一个简单java程序的运行全过程

putfield #2:将当前栈顶的值”20150723”赋值给0x2222所引用对象的sid字段,然后栈中的两个值出栈。

return:返回调用方,即main()方法,当前方法栈帧出栈。

重新回到main()方法中,继续执行下面的字节码指令:

astore_1:将当前栈顶引用类型的值赋值给slot1处的局部变量,然后出栈。

java拾遗4----一个简单java程序的运行全过程

aload_1:slot1处的引用类型的值入栈

iconst_5:将常数5入栈,int型常数只有0-5有对应的iconst_x指令

bipush 6:将常数6入栈

java拾遗4----一个简单java程序的运行全过程

invokevirtual #11:调用虚方法study(),这个方法是重写的接口中的方法,需要动态分派,所以使用了invokevirtual指令。

创建study()方法的栈帧:

java拾遗4----一个简单java程序的运行全过程

最大栈深度3,局部变量表5

java拾遗4----一个简单java程序的运行全过程

方法的java源码:

public int study(int a, int b){

int c = 10;

int d = 20;

return a+b*c-d;

}

对应的字节码:

java拾遗4----一个简单java程序的运行全过程

注意到这里,通过jClassLib工具查看的字节码指令有点问题,与源码有偏差……

改用通过命令javap –v Student查看study()的字节码指令:

java拾遗4----一个简单java程序的运行全过程

bipush 10:将10入栈

istore_3:将栈顶的10赋值给slot3处的int局部变量,即c,出栈。

bipush 20:将20入栈

istore 4:将栈顶的20付给slot4处的int局部变量,即d,出栈。

上面4条指令,完成对c和d的赋值工作。

iload_1、iload_2、iload_3这三条指令将slot1、slot2、slot3这三个局部变量入栈:

java拾遗4----一个简单java程序的运行全过程

imul:将栈顶的两个值出栈,相乘的结果入栈:

java拾遗4----一个简单java程序的运行全过程

iadd:将当前栈顶的两个值出栈,相加的结果入栈

iload 4:将slot4处的int型的局部变量入

java拾遗4----一个简单java程序的运行全过程

isub:将栈顶两个值出栈,相减结果入栈:

ireturn:将当前栈顶的值返回到调用方。

java拾遗4----一个简单java程序的运行全过程

重新回到main()方法中:

pop指令,将study()方法的返回值出栈

invokestatic #12 调用静态方法getCnt()不需要传任何参数

pop:getCnt()方法有返回值,将其出栈

aload_1:将slot1处的引用值入栈

invokevirtual #13:调用0x2222对象的run()方法,重写自父类的方法,需要动态分派,所以使用invokevirtual指令

return:main()返回,程序运行结束。

以上,就是一个简单程序运行的大致过程,只是今天看书的一些理解,也许有错误的地方,希望不会贻笑大方……

java拾遗4----一个简单java程序的运行全过程的更多相关文章

  1. 图解简单C程序的运行时结构

    程序在内存中的存储分为三个区域,分别是动态数据区.静态数据区和代码区.函数存储在代码区,全局变量以及静态变量存储在静态数据区,而在程序执行的时候才会在动态数据区产生数据.程序执行的本质就是代码区的指令 ...

  2. 输出多行字符的一个简单JAVA小程序

    public class JAVA { public static void main(String[] args) { System.out.println("-------------- ...

  3. JAVA课程设计——一个简单的教务人事管理系统

    大三上学期期末总结,没错,上学期,写在下学期新学期开始,哈哈哈. 上学期学习了面向对象程序设计,课程设计的题目使用JAVA语言完成一个简单的教务人事管理系统,能够实现访问数据库的登录验证,分别按部门和 ...

  4. 使用JAVA实现的一个简单IOC注入实例

    https://blog.csdn.net/echoshinian100/article/details/77977823 欲登高而望远,勿筑台于流沙 RSS订阅 原 使用JAVA实现的一个简单IOC ...

  5. 使用 jquery 的 上传文件插件 uploadify 3&period;1 配合 java 来做一个简单的文件上次功能。并且在界面上有radio 的选择内容也要上传

    使用 jquery 的 上传文件插件 uploadify 3.1 配合 java 来做一个简单的文件上次功能.并且在界面上有radio 的选择内容也要上传 uploadify 插件的 下载和文档地址  ...

  6. Java RMI 实现一个简单的GFS(谷歌文件系统)——介绍篇

    本系列主要是使用Java RMI实现一个简单的GFS(谷歌文件系统,google file system),首先整体简单介绍下该项目. [为了更好的阅读以及查看其他篇章,请查看原文:https://w ...

  7. Java RMI 实现一个简单的GFS(谷歌文件系统)——背景与设计篇

    目录 背景 系统设计 1. 系统功能 2. Master组件 2.1 命名空间 2.2 心跳机制 2.3 故障恢复和容错机制 3. ChunkServer组件 3.1 本地存储 3.2 内存命中机制 ...

  8. 关于SIGSLOT的一个简单的程序

    废话少说直接看代码即可,这只是一个简单的程序,可以帮我们简单地明白SIGSLOT是怎么回事.至于深入研究自己去百度吧. #include "sigslot.h" using nam ...

  9. 用java代码写一个简单的网上购物车程序

    需求:1.写一个商品类,有商品编号.商品名称.商品分类.商品单价属性.2.写一个商品条目信息类,有商品和数量两个属性,有商品总价格方法. 3.写一个购物车类,有添加商品方法.查看订单信息,删除商品,修 ...

随机推荐

  1. Spring-Context之六&colon;基于Setter方法进行依赖注入

    上文讲了基于构造器进行依赖注入,这里讲解基于Setter方法进行注入.在Java世界中有个约定(Convention),那就是属性的设置和获取的方法名一般是:set+属性名(参数)及get+属性名() ...

  2. thymeleaf 内联语法

    十二. thymeleaf内联语法 内联:th:inline,值有三种:text,javascript,none 12.1 th:inline="text"文本内联 <p t ...

  3. fineui框架

    http://fineui.com/demo/#/demo/layout/fit.aspx 虽然比较丑陋,但功能实用 此框架比较简单, 框架的作用你懂的,重点是要有帮助文档, 进阶型的容易上手的帮助文 ...

  4. Qt使用MinGW编译,如何忽略警告

    Qt编译时经常出现以下警告: warning: unused parameter 'arg1' [-Wunused-parameter] warning: unused variable 'i' [- ...

  5. C语言--第0周作业

    1.翻阅邹欣老师博客关于师生关系博客,并回答下列问题: 1)最理想的师生关系是健身教练和学员的关系,在这种师生关系中你期望获得来自老师的哪些帮助? 答: 若教练和学员的关系是最理想的师生关系,那就意味 ...

  6. 比较ArrayList、LinkedList、Vector

    翻译人员: 铁锚 翻译时间: 2013年12月2日 原文链接: ArrayList vs. LinkedList vs. Vector 1. List概述 List,就如图名字所示一样,是元素的有序列 ...

  7. Flex很难?一文就足够了

    Flexible Box 是什么   布局的传统解决方案,基于盒状模型,依赖 display 属性 + position属性 + float属性.它对于那些特殊布局非常不方便,比如,垂直居中就不容易实 ...

  8. Aooms&lowbar;微服务基础开发平台实战&lowbar;002&lowbar;工程构建

    一.关于框架更名的一点说明 最近在做年终总结.明年规划.还有几个项目需要了结.出解决方案,事情还比较多,死了不少脑细胞,距离上一篇文章发出已经过了3天,是不是有些人会认为我放弃了又不搞了,NONO,一 ...

  9. Java进程线程笔记

    什么是并行和并发? 并发和并行是即相似又有区别:(微观) 并行:指两个或多个事件在同一时刻发生: 强调的是时间点. 并发:指两个或多个事件在同一时间段内发生: 强调的是时间段. 进程和线程的区别? 进 ...

  10. Vue(SPA) WebPack模块化打包、SEO优化(Vue SSR服务端同构直出)、全浏览器兼容完整解决方案

    白驹过隙,时光荏苒 大概去年这个时候写了angular 结合webpack的一套前端方案,今年此时祭出vue2结合webpack的一套前端方案. 明年的这个时候我又是在做什么... 读在最前面: 1. ...