大家好,我是非著名程序员羊羽,今天我要和大家聊的是我们编写的Java源代码是怎么执行的,在执行的的过程中经历了什么的话题。如果读完之后你喜欢这篇文章,欢迎关注我并转发文章,今后我将持续输出Java相关的知识。
当你在某个类中点击运行main函数,通常经过一小会之后,你就会得到一个程序执行的结果,在得到运行结果之后,你有没有想过在那一小会的时间里面究竟发生了什么呢?为什么会得出这样的一个结果呢?如果你存在上面的疑惑或者想探究运行的过程,那么本文章将会非常适合你。
从java文件到JVM执行程序输出结果,期间经历了下列步骤
- Java源码到字节码文件
- 字节码文件加载到JVM
- JVM执行程序
1 Java源码到字节码文件
我们如果在IDE工具上编写Java代码,那么它会自动的帮我们进行编译,编译后将会生成class字节码文件。
class文件
.java源文件是如何编译成字节码的呢?在这里我给大家做了一个思维导图:
思维导图
IDE工具实际上自动帮我们执行了javac命令,也就是编译指令,执行了该命令会分为四个步骤,分别是:
- 词法分析,从源码中找出一些规范化的Token流。
- 语法分析,形成一个符合Java语言规范的抽象语法树。
- 语义分析,将复杂的语法转化成最简单的语法。
- 字节码生成器,生成符合JVM规范的字节码。
经过上面步骤之后,一份符合JVM规范的字节码文件就编译完成了,接下来就进入第二个步骤。
2 字节码文件加载到JVM
当你从某个类中main函数启动程序时,JVM就会加载需要的字节码文件到内存中确保程序的正常执行,深入分析之前我们需要看一下JVM的大致结构和作用,如下:
JVM结构
- 方法区中存放类的信息、静态成员/方法、常量(所有线程共享)。
- 堆中保存new出来的对象(所有线程共享)。
- Java虚拟机栈中有方法运行时就会生成一个栈帧。
- 程序计数器用来记录当前线程执行到的方法地址,以免其他线程占用cpu资源释放后当前线程执行发生异常。
- 本地方法栈调用的是本地方法,不同的操作系统有不同的实现。
上面是介绍的是JVM的大致结构和作用,接下来看看字节码怎么样加载到JVM中去,思维导图如下
字节码加载过程
字节码需要通过类加载器才能加载到JVM中去,类加载器的执行过程如上所示,这里需要介绍一下的是双亲委派模式,该模式的设计主要是为了防止自定义的类被当做是系统类加载,具体细节在这里就不展开了。
3 JVM执行程序
在对应的字节码文件加载到JVM内存中后,程序就能执行了,执行过程中每当有方法被调用时就会在栈中创建一个该方法的栈帧
栈帧在执行完成后就会从栈中弹出,程序携带数据返回调用栈帧继续执行,当所有线程的栈帧都从栈中弹出后,JVM的执行结束并退出。
下面以一个例子为例进行说明:
定义了一个GetClass类和Student类,在GetClass里面有一个启动函数main,点击运行后将会经历本文描述的前两步,然后在JVM的栈中将会产生一个main方法的栈帧,如下图:
该栈帧内将会有一个局部变量表和操作栈,局部变量表用来存储栈帧中的局部变量的值,比如说类中的i=0,操作栈将会记录当前栈的操作数。
程序首先执行
int i = 0;
此时,常数0进入操作栈,然后弹出赋值给局部变量i
当程序执行到
Student student = new Student();
此时,JVM会去方法区中找有没有对应的类信息存在,如果没有就会使用类加载器去加载对应的字节码文件,加载到之后就会在堆上创建该对象,对象创建后就会将该对象的地址赋值给引用变量student
当程序执行到
student.setName("zs");
此时,会在栈上创建setName方法的栈帧:
首先将常量zs入栈 ,然后弹出赋值给对象变量name
该栈帧执行完成后将会从栈中弹出,该栈帧弹出之前已经改变堆上Student对象的name值
当程序执行到
System.out.println(student.getName());
首先,会创建一个println方法的栈帧,然后发现调用了getName方法,所以会再创建一个getName方法的栈帧:
getName方法栈帧在执行完成后将会弹出,弹出之前已经将数据传递给println方法栈帧,然后println方法栈帧将操作栈中的数赋值给局部变量,然后将局部变量值打印出来,最后也从栈中弹出,此时栈中还剩下main栈帧
当程序执行到
System.out.println(i);
又会在栈中产生一个println方法栈帧,
main栈帧将局部变量中i的值复制一份给println方法栈帧中局部变量表,然后println方法栈帧执行,打印输出1,接着println方法栈帧弹出,程序返回main栈帧继续执行。
此时main栈帧也执行完毕,main栈帧也从栈中弹出,此时栈中没有任何的栈帧,JVM将退出。
上述例子仅为简化介绍,实际的运行情况会比这个复杂,比如说printIn方法中也会调用很多方法,这些方法都会在栈上创建对应的方法栈帧,调用结束后弹出栈,如果你有兴趣,可以深入研究。
写在最后
到这里,一个程序从源文件到程序运行的过程就说完了,但是我们依然存在一些问题,比如说:
- 如果创建的对象越来越多,堆中空间不够存储该怎么办呢?
- JVM是怎么去判断一个对象是不是垃圾的呢?
- 什么是OOM(内存溢出),JVM是怎么去解决OOM的呢?
- 所有栈帧在执行完毕之后就会弹出,那么栈帧中的数据该要怎么保存呢?
如果你也有以上的问题,欢迎关注我,在接下来的时间里,我将会写一系列的文章来解答上面的问题。
如果你看完之后觉得文章解决了一些你的疑问或者给你带来了一些帮助,欢迎转发或者收藏。