[深入理解JVM 一]Java程序执行流程

2018-09-27  本文已影响0人  boyiis

本篇是《深入理解JVM》系列博客的第一篇,旨在全局把控,先对整体流程有个认识,然后再分阶段详解.本篇博客大部分内容来自http://www.cnblogs.com/dqrcsc/p/4671879.htmljava一些地方重新进行了整理,根据自己的理解重新规划了内容—TML

概述

程序执行流程我把它划分为以下几个步骤:编辑源码、编译生成class文件、(加载class文件、运行class字节码文件),其中后两个步骤都是在jvm虚拟机上执行的。

执行流程

编辑

过程描述

编辑源代码,就是我们在任何一个工具上编写源代码,可以是记事本,最后命名为Student.java。

这部分相当于我们在myeclipse这样的ide上新建一个.java的Class然后写内容。

源码文件

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类,与java文件同名
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();
    }

}

编译

过程描述

生成.class字节码文件,输入命令javac Student.java将该源码文件编译生成.class字节码文件。由于在源码文件中定义了两个类,一个接口,所以生成了3个.clsss文件。

这部分的操作就相当于我们在myeclipse这样的ide上写完代码ctrl+s保存

字节码文件

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

20170809114544464.png

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

它存放了这个类的各种信息:字段、方法、父类、实现的接口等各种信息。

运行

过程描述

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

这部分的操作就相当于我们在myeclipse这样的ide上点击运行按钮

JVM基本结构介绍

Jvm的运行时内存分区和溢出处理见我的本系列第二篇博文(java底层分析—jvm内存分析)http://blog.csdn.net/sinat_33087001/article/details/76976027
,有具体描述,这里简单说下。

20170809115320310.png

JVM中把内存分为方法区、Java栈、Java堆、本地方法栈、PC寄存器5部分数据区域。
方法区:用于存放类、接口的元数据信息,加载进来的字节码数据都存储在方法区
Java栈(虚拟机栈):执行引擎运行字节码时的运行时内存区,采用栈帧的形式保存每个方法的调用运行数据
本地方法栈:执行引擎调用本地方法时的运行时内存区
Java堆():运行时数据区,各种对象一般都存储在堆上
PC寄存器(程序计数器):功能如同CPU中的PC寄存器,指示要执行的字节码指令。

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

类加载

加载阶段
1)类加载器会在指定的classpath中找到Student.class(通过类的全限定名)这个文件,然后读取字节流中的数据,将其存储在方法区中。
2)会根据Student.class的信息建立一个Class对象,这个对象比较特殊,一般也存放在方法区中,用于作为运行时访问Student类的各种数据的接口。
验证阶段
3)必要的验证工作,格式、语义等
准备阶段
4)为Student中的静态字段分配内存空间,也是在方法区中,并进行零初始化,即数字类型初始化为0,boolean初始化为false,引用类型初始化为null等。

 private static int cnt=5; 

此时,并不会执行赋值为5的操作,而是将其初始化为0。
解析阶段
5)由于已经加载到内存了,所以原来字节码文件中存放的部分方法、字段等的符号引用可以解析为其在内存中的直接引用了,而不一定非要等到真正运行时才进行解析。
初始化阶段
6)由于已经加载到内存了,所以原来字节码文件中存放的部分方法、字段等的符号引用可以解析为其在内存中的直接引用了,而不一定非要等到真正运行时才进行解析。
在Student.java中只有一个静态字段:

一个类加载之前要加载它的父类及其实现的接口


20170816143551888.png

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

运行字节码指令

执行引擎找到main()这个入口方法,执行其中的字节码指令:
只有当前正在运行的方法的栈帧位于栈顶,当前方法返回,则当前方法对应的栈帧出栈,当前方法的调用者的栈帧变为栈顶;当前方法的方法体中若是调用了其他方法,则为被调用的方法创建栈帧,并将其压入栈顶。

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

public static void main(String[] args){
    Student s = new Student(23,"dqrcsc","20150723");
    s.study(5,6);
    Student.getCnt();
    s.run();
}
20170816143933834.png

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

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

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

20170816144555899.png

执行过程如下:

1.为main方法创建栈帧:

20170816144303175.png

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

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

20170816144615371.png

3.初始化一个对象(通过实例构造的方式)

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

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

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

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

20170816144830251.png

4. invokespecial #10:调用#10这个常量所代表的方法,即Student.()这个方法,这步是为了初始化对象s的各项值

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

20170816144946109.png

注意到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.<init>()方法,会创建该方法的栈帧,并入栈。栈帧中的局部变量表的第0到4个slot分别保存着入栈的那四个参数值。

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

20170816145524342.png

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

20170816145627113.png

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

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

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

20170816145755714.png

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

return:返回调用方即main()方法,当前方法栈帧出栈。
重新回到main()方法中,继续执行下面的字节码指令:

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

20170816150925158.png

5. 到这儿为止,第一行代码执行完毕,将s返回给局部变量表,执行下边的

public static void main(String[] args){
    Student s = new Student(23,"dqrcsc","20150723");//执行完毕
    s.study(5,6);
    Student.getCnt();
    s.run();
}

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

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

bipush 6:将常数6入栈

20170816151022684.png

6. 开始执行第二行代码,也就是strudy方法

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

创建study()方法的栈帧:

20170816151434396.png

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

20170816151526635.png

方法的java源码:

public int study(int a, int b){
    int c = 10;
    int d = 20;
    return a+b*c-d;
}

20170816151708837.png

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这三个局部变量入栈:

20170816151839398.png

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

20170816151924992.png

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

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

20170816152018858.png

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

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

20170816152046502.png

7. 到这儿为止,第二行代码执行完毕,返回值返回给s,执行下边的

public static void main(String[] args){
    Student s = new Student(23,"dqrcsc","20150723");//执行完毕
    s.study(5,6);
    Student.getCnt();
    s.run();
}

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

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

aload_1:将slot1处的引用值入栈

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

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

总结

总结起来,一个类文件首先加载到方法区,一些符号引用被解析(静态解析)为直接引用或者等到运行时分派(动态绑定),经过一系列的加载过程(class文件的常量池被加载到方法区的运行时常量池,各种其它的静态存储结构被加载为方法区运行时数据解构等等)

然后程序通过Class对象来访问方法区里的各种类型数据,当加载完之后,程序发现了main方法,也就是程序入口,那么程序就在栈里创建了一个栈帧,逐行读取方法里的代码所转换为的指令,而这些指令大多已经被解析为直接引用了,那么程序通过持有这些直接引用使用指令去方法区中寻找变量对应的字面量来进行方法操作。

操作完成后方法返回给调用方,该栈帧出栈。内存空间被GC回收,堆里被new的那些也就被来及回收机制GC了。

全流程包括以下几步:源码编写–编译(javac编译和jit编译,java语法糖)—类文件被加载到虚拟机(类Class文件结构,虚拟机运行时内存分析,类加载机制)—-虚拟机执行二进制字节码(虚拟机字节码执行系统)—垃圾回收(JVM垃圾回收机制)

分别对应我的其它7篇博客,这是该系列博客的第一篇
这就是一个java程序从编写,编译,到运行的全流程。各部分可参考的我同系列的博文链接,这里再次感谢这篇博文所讲,让我茅塞顿开http://www.cnblogs.com/dqrcsc/p/4671879.html

上一篇下一篇

猜你喜欢

热点阅读