深入理解JVM_java代码的执行机制01
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解JVM_java代码的执行机制01相关的知识,希望对你有一定的参考价值。
本章学习重点:
1、Jvm:
如何将java代码编译为class文件。
如何装载class文件及如何执行class文件。
jvm如何进行内存分配和回收。
jvm多线程:线程资源同步机制和线程之间交互的机制。
3.1 java代码的执行机制
java源码编译机制。
1、三个步骤:
分析和输入到符号表(Parse and Enter)
Parse过程所做的为词法和语法分析。
词法分析:将代码字符串转变为Token序列。
语法分析:根据语法由Token序列生成抽象语法树。
Enter过程为将符合好输入到符号表。
通常包括确定类的超类型和接口,根据需要添加默认构造器、将类中出现的符号输入类自身的符号表中等。
注解处理(Annotation Processing)
主要用于处理用户自定义的Annotation。
语义分析和生成class文件(Analyse and Generate)
Analyse基于抽象语法树进行一系列的语义分析。
包括 将语法树的名字、表达式等元素与变量、方法、类型等联系到一起;
检查变量使用前是否已声明;
推导泛型方法的类型参数;
检查类型匹配性;
检查所有语句都可到达;
检查所有checked exception都被捕获或抛出;
检查变量的确定性赋值(例如有返回值的方法必须确定有返回值);
检查变量的确定性不重复赋值(例如声明为final的变量等);
解除语法糖(消除if(false){...})形式的无用代码;
将泛型java转为普通java;
将含有语法糖的语法树改为含有简单语言结构的语法树,(例如foreach循环、自动装箱/拆箱等);
等。
完成上述步骤,开始生成class文件。步骤为:
(1)将实例成员初始化器收集到构造器中,将静态成员初始化器收集为<clinit>();
(2)将抽象语法树生成字节码,采用的方法为后序遍历语法树,并进行最后的少量代码转换;
(3)从符号表生成class文件。
2、class文件包含了以下信息:
结构信息:
class文件格式版本号及各部分的数量与大小的信息。
元数据:
简单来说,元数据对应的就是java源码中“声明”与“常量”的信息。
主要有:类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池。
方法信息:
简单来说,java源码中“语句”与“表达式”对应的信息。
主要有:字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试用符号信息。
类加载机制。
类加载机制是指class文件加载到JVM,并形成class对象的机制,之后应用就可对class对象进行实例化并调用。
1、分三个步骤:
装载(load):
过程负责找到二进制字节码并加载到JVM中。
链接(Link):
过程负责对二进制字节码的格式进行校验、初始化装载类中的静态变量及解析类中调用的接口、类。
校验如果不符合,则抛出VerifyError;
校验过程中如果碰到要引用到其他的接口和类,也会进行加载,如果失败,则抛出NoClassDefFoundError。
JVM初始化类中的静态变量,并赋默认值,最后对类中的所有属性,方法进行验证,如果该阶段失败,可能会造成NoSuchMethodError、NoSuchFieldError等错误信息。
初始化(init):
过程即执行类中的静态初始化代码,构造器代码及静态属性的初始化,以下4种情况下初始化过程会被触发执行:
调用了new;
反射调用了类中的方法;
子类调用了初始化;
JVM启动过程中指定的初始化类。
JVM的类加载通过ClassLoader及其子类来完成,分为:
BootStrap Class Loader;
采用C++实现,并非 ClassLoader的子类。JDK启动时会初始化此 ClassLoader;
Extension Class Loader;
用来加载扩展功能的一些jar包,例如:JDK目录下有dns工具jar包等;
System Class Loader;
用来加载启动参数中指定的Classpath中的jar包及目录。
User-Defined Class Loader;
开发人员自行实现的ClassLoader。
2、类加载过程中的常见异常:
(1)ClassNotFoundException:
原因为在当前的ClassLoader中加载类时未找到类文件。
(2)NoClassDefFoundError:
原因为加载的类中引用到的另外的类不存在。
(3)LinkageError:
该异常在自定义ClassLoader的情况下更容易出现。原因是此类已经在ClassLoader加载过了,重复地加载会造成该异常。
(4)ClassCastException:
该异常有多种原因,JDK5支持泛型后,合理使用泛型可相对减少此异常的触发。
类执行机制。
2种方式:
1、字节码解释执行方式
在源码编译阶段将源码编译为JVM字节码,是一种中间代码的方式,要由JVM在运行期对其进行解释并执行。
JVM采用四个指令来执行不同的方法调用:
(1)invokestatic:
对应调用static方法。
(2)invokevirtual:
对应调用对象实例的方法。
(3)invokeinterface:
对应调用接口。
(4)invokesprcial:
对应调用private方法和编译源码后生成的<init>方法——此方法为对象实例化时的初始化方法。
JDK基于栈的体系结构来执行字节码:
线程在创建后,会产生程序计数器(PC registers)和栈(Stack)。
作用:
PC registers:存放了下一条要执行的指令在方法内的编译量。
栈:存放了栈帧,每个方法每次调用都会产生栈帧。
栈帧分为:局部变量和操作数栈。
作用:
局部变量:存放方法中的局部变量和参数。
操作数栈:存放方法执行过程中的中间结果。
局部变量区 | 操作数栈 | |
Stack Frame(栈帧) | ||
局部变量区 | 操作数栈 | |
Stack Frame(栈帧) | ||
pc寄存器 | Stack |
三种执行方式:
(1)指令解释执行:
执行方式:获取下一条指令,解码并分派,然后执行。由于很多操作要将值放入到操作数栈中,导致了寄存器和内存要不断地交换数据,效率不高。
SUN JDK进行优化,主要有:栈顶缓存和部分栈帧共享。
(2)栈顶缓存:
将本来位于操作数栈顶的值直接缓存到寄存器上,对于大部分只需要一个值的操作而言,无须将数据放入操作数栈,可直接在寄存器计算,然后放回操作数栈。
(3)部分栈帧共享:
当调用方法时,后一方法可将前一方法的操作数栈作为当前方法的局部变量,从而节省数据copy带来的消耗。
2、编译执行
为了解决解释执行的效率问题,JDK提供将字节码编译为机器码的支持,编译在运行时进行,通常称为JIT编译器。
JDK在执行过程中对执行频率高的代码进行编译,对执行不频繁的代码则继续采用解释的方式,所以JDK又称为HotSpotVM。
在编译上提供2种模式:client compiler和server compiler。
(1)client compiler:
C1 轻量级,只做少量性能开销比高的优化,占用内存少,合适与桌面交互式应用。
在寄存器分配策略上,JDK6以后采用的为线性扫描寄存器分配算法。
其他地方优化:
方法内联:
把调用到的方法的指令直接植入当前方法中。
去虚拟化:
装载class之后,进行类层次分析,如类中的方法只提供一个实现类,那么对于调用了此方法的代码,也可进行方法内联。
冗余消除:
指在编译时,根据运行时状况进行代码的折叠和消除。
等。
(2)server compiler:
C2 重量级,大量的传统编译优化技巧来进行优化,占用内存多,适用于服务器端的应用。
采用的为传统的图着色寄存器分配算法。优化范围更多的在于全局的优化。
收集的信息:分支的跳转/不跳转的频率、某条指令上出现过的类型、是否出现过空值、是否出现过异常。
逃逸分析是很多优化的基础。指的是根据运行状态来判断方法中的变量是否会被外部读取,如不会则认为此变量是逃逸的。基于此在编译时会做:
标量替换:
用标量替换聚合量。
好处:如果创建的对象并未用到其中的全部变量,则可以节省一定的内存。对于代码执行,由于无须去找对象的引用,也会更快。
栈上分配:
如果没有,会在栈上直接创建对象实例,而不是在JVM堆上。
好处:栈上分配更快。回收时随方法的结束,对象也被回收了。
同步消除:
如果发现同步的对象未逃逸,就没有同步的必要,在编译时会直接去掉同步。
等。
运行后C1、C2编译出来的机器码如果不再符合优化条件,则会进行逆优化,也就是回到解释执行的方式。
一种特殊的编译为:OSR(On Stack Replace)。与C1、C2区别:
(1)OSR编译只替换循环代码体的入口;现象:方法的整段代码被编译了,但只有循环代码体才执行编译后的机器码,其他部分仍然是解释执行方式。
(2)C1、C2替换的方法调用的入口。
Sun JDK根据机器来选择C1和C2模式。当CPU超过2核且内存大于2GB时默认为C2模式,但是32位windows机器始终为C1模式。
未选择在启动时即编译成机器码的原因:
(1)静态编译并不能根据程序的运行状况来优化执行的代码。
(2)解释执行比编译执行更节省内存。
(3)启动时解释执行的启动速度比编译再启动更快。
未编译期间解释执行方式会比较慢,JDk主要依据方法上的2个计数器是否超过阀值。
(1)调用计数器,即方法被调用的次数。(CompileThreshold)
该值指当方法被调用多少次后,就编译为机器码。在client模式下默认为1500次,server模式下默认为10000次。
可通过启动时添加-XX:CompileThreshold=10000来设置该值。
(2)回边计数器,即方法中循环执行部分代码的执行次数。(OnStackReplacePercentage)
该值用于计算是否触发OSR编译的阀值。默认情况下client模式为933,server模式为140。
该值通过启动时添加-XX:OnStackReplacePercentage=140来设置。
注意:由于sun JDK这个特性,在对java代码进行性能测试时,要尤其注意是否事先做了足够次数的调用,以保证测试是公平的。
3、反射执行
反射和直接创建对象实例,调用方法的最大不同在于创建、方法调用的过程是动态的。
要实现动态调用,最直接的方法就是动态生成字节码,并加载到JVM中执行。
以上是关于深入理解JVM_java代码的执行机制01的主要内容,如果未能解决你的问题,请参考以下文章