深入理解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的主要内容,如果未能解决你的问题,请参考以下文章

深入理解Java异常处理机制

深入理解java异常处理机制

深入理解java异常处理机制

深入理解Dalvik虚拟机- 解释器的运行机制

深入理解java异常处理机制

深入理解JVM方法调用的内部机制