JVM类加载与运行时优化

来源:互联网 发布:258优化网站 编辑:程序博客网 时间:2024/04/30 16:42

1. 类加载生命周期

这里写图片描述

    a. 装载(load)        i. 开始时机:            1) new实例化对象时,若类没有加载            2) 读取或设置一个类static字段,若类没有被加载。final除外,因为final字段的值已经在编译期放到了常量池中            3) 调用类的static方法            4) 反射调用类            5) 初始化一个类时,若父类没有被加载,会先加载父类        ii. 不会加载类的情况            1) 通过子类去引用父类的static字段,不会导致子类加载            2) 数组定义引用类,不会导致类加载。 如 Student[] stu = new Student[10], student类不会被加载            3) 读取类的final字段,不会导致类加载。        iii. 流程            1) 通过类的全限定名获取定义此类的二进制流。可以从class文件,网络,或者运行时计算(如动态代理)出这个二进制流            2) 将字节流代表的static存储结构转化为方法区的运行时数据结构,也就是存储到方法区中            3) 内存中生成一个代表此类的class对象    b. 链接        i. 验证            1) 目的:防止加载的class文件危害虚拟机本身安全            2) 流程:                a) 文件格式验证,如magic是否为0xCAFEBABE,主次版本号是否在当前VM能处理范围内                b) 元数据验证,主要验证描述信息是否符合Java语言规范                c) 字节码验证,最复杂,通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的                d) 符号引用验证,如通过全限定名能否找到类,字段方法的可访问性等。        ii. 准备            1) 目的:为static变量分配内存,并将它们统一初始化为0. static final除外        iii. 解析            1) 目的:将常量池中的符号引用替换为直接引用                a) 符号引用:字面量,如类名,方法名等                b) 直接引用:类或方法存放在内存中的地址    c. 初始化        i. 初始化static变量为实际的值。通过执行类构造器<clint>方法来完成。这个方法是编译器自动生成在字节码中的        ii. clinit:            1) static变量的赋值 + static{}语句块。 static{}语句块只能访问到定义在它之前的变量。            2) clinit和实例构造器init不同,不需要显式调用父类构造器。JVM保证子类的clinit执行前,父类的clinit肯定被执行过            3) 父类的clinit先执行,故父类中的static{}语句块在子类的static变量赋值前被调用            4) 接口中不能使用static{}语句块,但仍然可以为变量赋值            5) JVM可以保证clinit方法是线程安全的。多个线程同时去初始化一个类,只有一个线程会执行clinit方法,其他都会阻塞等待。

2. 编译期优化(早期优化)

  1. 为了保证JRuby,Groovy等语言编译的字节码也能得到性能优化,JVM将性能优化放在了后期的运行时优化,即JIT运行时编译优化中

  2. 编译期优化主要为语法糖,用来实现Java的各种新的语法特性,比如泛型,变长参数,自动装箱/拆箱

  3. Java语法糖:与字节码无关,编译后会去掉它们。作用仅仅为方便码农写代码,以及将运行时异常在编译期及早发现(如泛型的使用)

    1) 泛型与类型擦除

    Java泛型只在编译期存在,编译完成后的字节码中会替换为原生类型。故称Java泛型为伪泛型。C#的泛型在运行期仍然存在。

    2) 条件编译

    if语句中使用常量。比如if(false) {}, 这个语句块不会被编译到字节码中.这个过程在编译时的控制流分析中完成。

3. 运行时优化(晚期优化)

1). 解释执行和编译执行的对比

    a. 解释执行需要JVM解释器,速度慢。编译执行生成了本地机器语言,速度快。    b. 解释执行利用字节码,比机器码紧凑,故需要内存比编译执行小。    c. 解释执行可以直接在字节码上执行,而编译执行需要运行时将字节码先转化为机器码。故编译执行启动时间较长。    d. 编译器可以将一些运行频繁的热点字节码,优化为机器码,从而加快运行速度。解释器也可以将一些优化得比较激进的机器代码,逆优化为字节码,进行解释执行。 

2)不同JVM的运行时优化策略

    a. Hotspot采用解释器与编译器并存的构架。        i. 第0层,解释执行,不开启性能监控器,可触发第一层编译        ii. 第1层,将字节码编译为机器码,进行简单可靠的优化,可以开启性能监控        iii. 第2层,将字节码编译为机器码,会开启一些编译耗时的优化和一些不可靠的激进优化    b. 早期JVM很多只有解释器    c. JRockit虚拟机只有编译器,没有解释器。因为它是面向服务器应用的。

3)编译对象和触发条件

    a. 热点代码        i. 被多次调用的方法        ii. 被多次执行的循环体    b. 热点探测方式        i. 基于采样的热点探测:            1) 周期性检查各个线程的栈顶,发现某个方法经常位于栈顶,则被认为是热点方法            2) 简单,高效,并且可以得到方法调用链。但很难精确确认方法热度,因为线程可能阻塞等        ii. 基于计数器            1) 为每个方法建立计数器,统计它的执行次数            2) 麻烦,但很精确。HotSpot中采用的就是这种方法    c. 流程:        i. 检查是否存在被JIT编译过的方法版本        ii. 不存在,则计数器加1        iii. 判断计数是否超过阈值。超过,则提交即时编译的申请        iv. 执行引擎不会同步等待编译完成,而是继续使用解释器执行。        v. JIT编译完成后,方法的调用入口会被自动更新。这样,下一次调用的时候,就是新的入口,也就是JIT编译之后的了。

4)编译过程

    a. 字节码 -> HIR,基础化的优化,如方法内联    b. HIR -> LIR, 从HIR中产生低级中间代码表示    c. LIR -> 机器码,使用线性扫描算法,在LIR上分配寄存器,产生机器代码

5)Java即时编译与C/C++编译对比

    a. 劣势        i. Java即时编译是在运行时,故会占用用户程序的运行时间。而C/C++是静态编译为机器码的,完全不占用运行时间。        ii. Java运行时不能进行一些比较耗时的优化,故能做的优化也没有C那么多        iii. 多态选择频率远高于C,需要建立虚方法表。也正是多态的存在,使得编译优化难度远高于C。因为多态较难预测代码跳转分支。        iv. Java运行时可以加载新的类,如网络中的二进制流。这使得编译器无法看清程序全貌,全局优化很难进行。        v. Java对象都是在堆上分配(除了class对象在方法区),而C/C++既可以在堆上,又可以在栈上。栈上分配可以减轻垃圾回收压力,且速度远快于堆。    b. 优势        i. 可以进行性能监控,热点探测,分支频率预测,调用频率预测,从而有选择性的优化代码一句话,Java性能上的劣势,是为了换回码农开发效率上的优势。Java用心良苦,码农们不要再吐槽它的性能了!
0 0
原创粉丝点击