文章为作者原创,转载请注明出处,多谢配合!
本文打算针对jvm执行java代码流程做个简单的梳理。对jvm有个大框架的认识。
首先通过一张整体流程图来宏观了解下jvm执行java代码流程:
下面拆解开来分别进行解读:
一.编译
java编译器:比如javac (sun公司编译器,jdk默认自带的编译器)
java编译器的作用:读入java源代码,进行语法校验,通过后生成中间代码即字节码(.class文件)。字节码文件是一种和任何具体机器环境及操作系统环境无关的中间代码,它是一种二进制文件。编译器编译生成与平台无关的字节码文件后,提供给 JVM (Java虚拟机)执行。
另外需要注意的是:
(1)编译器编译一个java文件,涉及到的对象都会单独生成一一对应的.class文件,有多少对象生成多少个。
(2)字节码 ≠ 机器码 ,字节码是虚拟机认识的码,机器码是操作系统认识的码。
C/C++在编译的时候直接编译成机器码,而java是先编译成字节码,再由虚拟机转换为机器码。
详细了解字节码,可参考:http://www.importnew.com/24088.html
(3)为什么java编译出来的是字节码而不是机器码?
最主要的目的是跨平台,为了实现跨平台,就决定了不能像 c,c++ 那样直接把源代码编译成可执行文件,因为不同cpu,不同操作系 统的指令封装格式是不一样的。java编译成的字节码文件.class,与硬件和操作系统无关,这是跨平台基础,然后具体执行,再用各自平台解释器,解释成本地机器码。java是一种编译+解释的语言。
二.类装载器如何把.class文件装载到内存
在编译期,所有的*.java文件被编译成.class文件。在运行期,class文件只有被加载到jvm内存中才能运行。这个装载工作是由类装载器完成的。实质就是把class文件从硬盘读取到内存中,并对数据进行验证、准备、解析、初始化,最终形成可以被jvm直接使用的java类型。
1.装载方式以及装载器介绍:
类装载方式,有两种
(1)隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
(2)显式装载, 通过class.forname()等方法,显式加载需要的类。
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
Java的类加载器有三个,对应Java的三种类:(java中的类大致分为三种: 1.系统类 2.扩展类 3.由程序员自定义的类 )
Bootstrap Loader // 负责加载系统类 (指的是内置类,像是String,对应于C#中的System类和C/C++标准库中的类)
|
- - ExtClassLoader // 负责加载扩展类(就是继承类和实现类)
|
- - AppClassLoader // 负责加载应用类(程序员自定义的类)
jdk源码角度的加载过程不做过多解释了,有兴趣的可以参看:[https://blog.csdn.net/architect0719/article/details/50411545](https://blog.csdn.net/architect0719/article/details/50411545)
2.JVM类加载机制
•全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
•双亲委派:双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
•缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
- 类加载的过程:
类装载器就是寻找类或接口字节码文件进行解析并构造JVM内部对象表示的组件,在java中类装载器把一个类装入JVM,经过以下步骤:
加载、验证、准备、解析、初始化五个阶段。
加载:
1)通过一个类的全限定名来获取定义此类的的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区运行时的数据结构
3)在内存的堆区生成一个java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证:确保Class文件中的字节流中的包含信息符合jvm的要求,并且不会虚拟机自身的安全。
准备:在方法区给静态的类变量分配内存,并设置初始值。实例变量(未被static修饰的类变量)将会在对象实例化时,随对象一起分配到java堆中。
解析:将符号引用转成直接引用。
初始化:对静态变量和静态代码块执行初始化工作。
三、一个类在jvm内存中数据是如何被管理的
首先我们知道,jvm的一个重要职责就是管理好从计算机内存空间申请来的一亩三分地,运行时数据区常见划分方式为:
1)程序计数器:一个指针,指向执行引擎正在执行的指令的地址;
2)虚拟机栈:局部变量的基本数据类型和引用;
3)堆:引用的对象实体、成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体);
4)方法区:加载类的信息、静态变量;里面包含了常量池,常量池里放常量以及串池;
5)本地方法栈:它的存储跟虚拟机栈类似,只是针对的是native方法。
(其中堆和方法区是线程共享的,其余的则是线程隔离的)
有一个小结论:
局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。
——因为它们属于方法中的变量,生命周期随方法而结束。
成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)
——因为它们属于类,类对象终究是要被new出来使用的。
jvm的gc针对的主要就是堆区:gc的详细分析参考之前的文章:https://www.jianshu.com/p/220d6827be0d
四.执行引擎
1.执行引擎是干嘛的
前面我们了解了,类装载器装载编译后的字节码,并加载到运行时数据区,但是我们知道,Java字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言,所以需要由执行引擎执行这些字节码,转换生成由机器码组成的可被jvm执行的文件。
执行引擎找到入口main方法来执行其中的字节码。
2.转换方式
(1)解释器:一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的。
(2)即时(Just-In-Time)编译器:Oracle Hotspot VM使用一种JIT编译器,即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。不过,用JIT编译器来编译代码所花的时间要比用解释器去一条条解释执行花的时间要多。因此,如果代码只被执行一次的话,那么最好还是解释执行而不是编译后再执行。
因此,内置了JIT编译器的JVM都会检查方法的执行频率,如果一个方法的执行频率超过一个特定的值的话,那么这个方法就会被编译成本地代码。
具体做法:
每个方法被调用一次就给这个方法计数加1。
那些方法的被调用计数越多,JVM就优先翻译成机器码(使用JIT编译器),当然少的可能在执行到的时候再翻译(使用解释器)
(3)AOT(Ahead-Of-Time)编译器 IBM 在IBM JDK 6里不仅引入了JIT编译器,它同时还引入了AOT(Ahead-Of-Time)编译器。它使得多个JVM可以通过共享缓存来共享编译过的本地代码。简而言之,通过AOT编译器编译过的代码可以直接被其他JVM使用。除此之外,IBM JVM通过使用AOT编译器来提前把代码编译器成JXE(Java EXecutable)文件格式来提供一种更加快速的执行方式。