开发过java的人员都知道,java是一次编译,多次运行。可以在不同的操作系统上运行代码,用的就是虚拟机jvm,这个东西在jar中,也就是java 的运行环境,所以当你下载jdk的时候,会在官网上找自己需要的版本。
基本原理
java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然后在不同的操作系统上依靠不同的java虚拟机进行解释,最后再转换为不同平台的机器码,最终得到执行。那么了解了这个基本原理后,我们尝试去做更深的研究,一个普通的java程序它的执行流程到底是怎样的呢?例如我们有一个HelloWorld.java,运行流程如下。
首先java文件被编译成class文件,然后找到\jdk1.8.0_101\jre\lib\amd64\jvm.cfg文件,拿到具体的配置信息。接着jdk1.8.0_101\jre\bin\server\jvm.dll就会对jvm进行初始化,所以说jvm.dll是jvm的具体实现。接下来,jvm会获取JNI接口,JNI就是java的本地接口,跟你的硬件、操作系统交互用的,比如说,把文件从硬盘上扔到内存中就是JNI干的。最后,找到main方法,你的程序就可以跑起来了。
好了,现在知道了jvm的运行原理,下面看一下jvm的内部结构,如下图
JVM内存空间包含:方法区、java堆、java栈、本地方法栈。
方法区是各个线程共享的区域,存放类信息、常量、静态变量。
java堆也是线程共享的区域,我们的类的实例(对象)就放在这个区域,可以想象你的一个系统会产生很多实例,因此java堆的空间也是最大的。如果java堆空间不足了,程序会抛出OutOfMemoryError异常。
java栈是每个线程私有的区域,它的生命周期与线程相同,一个线程对应一个java栈,每执行一个方法就会往栈中压入一个元素,这个元素叫“栈帧”,而栈帧中包括了方法中的局部变量、用于存放中间状态值的操作栈,这里面有很多细节,我们以后再讲。如果java栈空间不足了,程序会抛出StackOverflowError异常,想一想什么情况下会容易产生这个错误,对,递归,递归如果深度很深,就会执行大量的方法,方法越多java栈的占用空间越大。
本地方法栈角色和java栈类似,只不过它是用来表示执行本地方法的,本地方法栈存放的方法调用本地方法接口,最终调用本地方法库,实现与操作系统、硬件交互的目的。
PC寄存器,说到这里我们的类已经加载了,实例对象、方法、静态变量都去了自己改去的地方,那么问题来了,程序该怎么执行,哪个方法先执行,哪个方法后执行,这些指令执行的顺序就是PC寄存器在管,它的作用就是控制程序指令的执行顺序。
执行引擎当然就是根据PC寄存器调配的指令顺序,依次执行程序指令。
线程通讯
上节我们知道了jvm运行原理和内部结构,现在看下内部运行模型,线程通信。
线程1(在栈中)将数据写给主内存(堆),然后线程2再从主内存中读到信息。主内存与工作内存的交互,则需要Java内存模型(JMM)来管理器。当然为了信息的实时更新,java中提供了volatile关键词。
指令重排
public void writer() {
a = 1;
flag = true;
}
有个这样的问题,以上这段代码执行顺序是 a=1 先还是 flag=true呢。你可能说我是傻子,但是运行可能是flag = true;先执行哦,这就涉及指令重排的问题。两个赋值语句尽管他们的代码顺序是一前一后,但真正执行时却不一定按照代码顺序执行。你可能会说,有这个指令重排序那不是乱套了吗?我写的程序都不按我的代码流程走,这怎么玩?这个你可以放心,你的程序不会乱套,因为java和CPU、内存之间都有一套严格的指令重排序规则,哪些可以重排,哪些不能重排都有规矩的。这里就不展开说了。
类加载器原理
还有最后一个问题,字节码文件是怎样装载到JVM中的呢?
加载是类装载的第一步,首先通过class文件的路径读取到二进制流,并解析二进制流将里面的元数据(类型、常量等)载入到方法区,在java堆中生成对应的java.lang.Class对象。
验证的主要目的就是判断class文件的合法性,比如class文件一定是以0xCAFEBABE开头的,另外对版本号也会做验证,例如如果使用java1.8编译后的class文件要再java1.6虚拟机上运行,因为版本问题就会验证不通过。
准备过程就是分配内存,给类的一些字段设置初始值,例如:public static int v=1;这段代码在准备阶段v的值就会被初始化为0,只有到后面类初始化阶段时才会被设置为1。但是对于static final(常量),在准备阶段就会被设置成指定的值,例如:public static final int v=1;这段代码在准备阶段v的值就是1。
解析过程就是将符号引用替换为直接引用,例如某个类继承java.lang.object,原来的符号引用记录的是“java.lang.object”这个符号,凭借这个符号并不能找到java.lang.object这个对象在哪里?而直接引用就是要找到java.lang.object所在的内存地址,建立直接引用关系,这样就方便查询到具体对象。
初始化过程,主要包括执行类构造方法、static变量赋值语句,staic{}语句块,需要注意的是如果一个子类进行初始化,那么它会事先初始化其父类,保证父类在子类之前被初始化。所以其实在java中初始化一个类,那么必然是先初始化java.lang.Object,因为所有的java类都继承自java.lang.Object。
类加载器ClassLoader,它是一个抽象类,ClassLoader的具体实例负责把java字节码读取到JVM当中,ClassLoader还可以定制以满足不同字节码流的加载方式,比如从网络加载、从文件加载。ClassLoader的负责整个类装载流程中的“加载”阶段。