Java源代码是怎么被机器识别并执行的呢?答案是Java虚拟机。
一、字节码
0和1是计算机仅能识别的信号,经过0和1的不同组合产生了数字之上的操作。另外通过不同的组合亦产生了各种字符。同样可以通过不同的组合产生不同的机器指令。
机器码是离CPU指令集最近的编码,是CPU可以直接解读的指令,因此机器码肯定是与底层硬件系统耦合的。
在代码执行的过程中,JVM将字节码解释执行,屏蔽对底层操作系统的依赖;JVM也可以将字节码编译执行,如果是热点代码,会通过JIT动态地编译为机器码,提高执行效率。
字节码主要指令如下:
(1)加载和存储指令
在某个栈帧中,通过指令操作数据在虚拟机栈的局部变量表与操作栈之间来回传输,常见指令如下:
- 将局部变量加载到操作栈中。如ILOAD(将int类型的局部变量压入栈和ALOAD(将对象引用的局部变量压入栈)等。
- 从操作栈顶存储到局部变量表。如ISTORE、ASTORE等。
- 将常量加载到操作栈顶,这是极为高频使用的指令。如 ICONST、BIPUSH、SIPUSH、LDC等。
(2)运算指令
对两个操作栈帧上的值进行运算,并把结果写入操作栈顶,如IADD、IMUL等。
(3) 类型转换指令
显示转换两种不同的数值类型。如I2L、D2F等。
(4)对象创建与访问指令
根据类进行对象的创建、初始化、方法调用相关指令,常见指令如下:
- 创建对象指令
- 访问属性指令
- 检查实例类型指令
(5)操作栈管理指令
JVM提供了直接控制操作栈的指令,常见指令如下:
- 出栈操作。如POP即一个元素,POP2即两个元素。
- 复制栈顶元素并压入栈。如DUP。
(6)方法调用与返回指令
- INVOKEVIRTUAL 指令: 调用对象的实例方法
- INVOKESPECIAL 指令: 调用实例初始化方法、私有方法、父类方法等
- INVOKESTATIC 指令: 调用类静态方法
- RETURN 指令:返回VOID类型
(7)同步指令
我们编写好的.java文件是源代码文件,并不能交给机器直接执行,需要将其编译称为字节码甚至是机器码文件。静态编译器将源码转字节码流程:
词法解析是通过空格分隔出单词,操作符,控制符等信息,将其形成token信息流,传递给语法解析器;在语法解析时,把词法解析得到的token信息流按照JAVA语法规则组成以可语法树,在语义分析阶段,需要检查关键字的使用是否合理、类型是否匹配、作用域是否正确等;当语义分析完成之后,即可生成字节码。
字节码必须通过类加载过程加载到JVM环境后,才可以运行。字节码必须通过类加载过程加载到JVM 环境后,才可以执行。执行有三种模式第一,解释执行,第二, JIT 编译执行第三, JIT 编译与解释混合执行(主流 JVM默认执行模式)。混合执行模式的优势在于解释器在启动时先解释执行,省去编译时间。随着时间推进 JVM通过热点代码统计分析 识另 高频的方法调用、循环体、公共模块等,基于强大的JlT 动态编译技术,将热点代码转换成机器码,直接交给 PU执行。 JIT 作用是将Java 字节码动态地编译成可以直接发送给处理器指令执行的机器码。简要流程如图 所示。
二、类加载过程
任何程序都需要加载到内存才能与CPU进行交流。字节码.class文件同样需要加载到内存中,才可以实例化类。ClassLoader的使命就是提前加载.classl类文件到内存中。在加载类时,使用的是“Parent Delegation Model” 译为双亲委派模型。
Java的类加载器是一个运行时核心基础设施模块,主要是在启动之初进行类的Load、Link、Init,即加载、链接、初始化。
- 第一步,Load 阶段读取类文件产生二进制流,并转化为特定的数据结构,初步校验cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.Class实例。
- 第二步,Link 阶段包括验证、准备、解析三个步骤。验证是更详细的校验,比如final是否合规、类型是否正确、静态变量是否合理等;准备阶段是为静态变量分配内存,并设定默认值,解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局。
- 第三步,Init 阶段执行类构造器<clinit>方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。
类加载过程图
类加载是一个将.class字节码文件实例化成Class对象并进行相关初始化的过程。在这个过程中,JVM会初始化继承树上还没有被初始化的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。某些类在使用时,也可以按需由类加载器进行加载。
类加载器类似于原始部落结构,存在权力等级制度。
(1)最高的一层是家族中威望最高的Bootstrap,它是在JVM启动时创建的,通常由与操作系统相关的本地代码实现,是最根本的类加载器,负责装载最核心的Java类,比如Object、System、String等。
(2)第二层是在JDK9版本中,称为Platform ClassLoader,即平台类加载器,用以加载一些扩展的系统类,比如XML、加密、压缩相关的功能类等,JDK9之前的加载器是Extension ClassLoader。
(3)第三层是Application ClassLoader的应用类加载器,主要是加载用户定义的CLASSPATH路径下的类。
第二、第三类加载器为Java语言实现,用户也可以自定义类加载器。
什么情况下需要自定义类加载器?
(1)隔离加载类。在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如,***某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。
(2)修改类加载方式。类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载。
(3)扩展加载源。比如从数据库、网络,甚至是电视机机顶盒进行加载。
(4)防止源码泄露。Java代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,还原加密的字节码。
三、内存布局
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。
-------------------------------------------------------------------------------------------------------------------------------------------
JVM内存布局图
Heap(堆区)
Heap是OOM故障最主要的发源地,它存储着几乎所有的实例对象,堆由垃圾收集器自动回收,堆区由各子线程共享使用。通常情况下,它占用的空间是所有内存区域中最大的,但如果无节制地创建大量对象,也容易消耗所有的空间。堆的内存空间既可以固定大小,也可以在运行时动态地调整,可以通过相应的参数进行设置。通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,势必形成不必要的系统压力,所以在线上生产环境中,JVM的Xms和Xmx设置成一样大小,避免在GC后调整堆大小时带来的额外压力。
JVM Stack(虚拟机栈)
栈(Stack)是一个先进后出的数据结构,就像子弹的弹夹,最后压入的子弹先发射,压在底部的子弹最后发射,撞针只能访问位于顶部的那一颗子弹。
相对于基于寄存器的运行环境来说,JVM是基于栈结构的运行环境。栈结构移植性更好,可控性更强。JVM中的虚拟机栈是描述Java方法执行的内存区域,它是线程私有的。栈中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程。在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。而StackOverflowError表示请求的栈溢出,导致内存耗尽,通常出现在递归方法中。JVM能横扫千军,虚拟机栈就是它的心腹大将,当前方法的栈帧,都是正在战斗的战场,其中的操作栈是参与战斗的士兵。
虚拟机栈通过压栈与出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上。在执行的过程中,如果出现异常,会进行异常回溯,返回地址通过异常处理表确定。栈帧在整个JVM体系中的地位颇高,包括局部变量表、操作栈、动态连接、方法返回地址等。
Native Method Stack(本地方法栈)
本地方法栈(Native Method Stack)在JVM内存布局中,也是线程对象私有的,但是虚拟机栈“主内”,而本地方法栈“主外”。
Program Counter Register (程序计数寄存器)
在程序计数寄存器()中,Register的命名源于CPU的寄存器,CPU只有把数据装载到寄存器才能够运行。寄存器存储指令相关的现场信息,由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等。线程执行或恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域也不会发生内存溢出异常。
最后,从线程共享的角度来看,堆和元空间是所有线程共享的,而虚拟机栈、本地方法栈、程序计数器是线程内部私有的,从这个角度看一下Java内存结构。
四、垃圾回收 (Garbage Collection, GC)
Java会对内存进行自动分配与回收管理,使上层业务更加安全,方便地使用内存实现程序逻辑。在不同的JVM实现及不同的回收机制中,堆内存的划分方式是不一样的。
垃圾回收的主要目的是清楚不再使用的对象,自动释放内存。
。。。。。。。。