`

JVM学习笔记十一 之 编译期优化和运行期优化

    博客分类:
  • jvm
 
阅读更多

一、概述

语言要在虚拟机上执行,必须先翻译成机器代码,翻译的方式有两种,一种是编译期静态翻译为机器码,一种是编译器翻译为某种表示,运行期在翻译成机器码来执行。

编译器可分为多种类型,1、编译器把java源文件编译成class文件的前端编译器,如javac和eclipse的jdt增量编译器;2、运行期把.class文件翻译成本地机器代码的JIT编译器,如HotSpot VM的C1、C2编译器;3、直接把java源文件编译成本地机器码的提前编译器(Ahead Of Time,AOT),如GNU Compiler for Java。后续我们提到的编译器是指运行期编译器。

java语言是一种解释型语言(语言规范有这定义么?如果用AOT编译呢),编译期把java文件编译成class文件,运行期再把class文件翻译成机器语言。而翻译也有两种方式,一是通过解释器,每执行一次代码就一条条的翻译成本地代码;一种是通过编译器,编译成本地代码后执行。

jvm spec没有规定要用解释器或者编译器来执行字节码。HotSpot VM是编译器+解释器协作完成字节码的运行。通过java -version可用查看当前虚拟机的执行模式,是混合模式;通过-Xint可以让虚拟机以解释模式执行;通过-Xcomp可以让虚拟机以编译模式运行:

D:\>java -version
java version "1.7.0_01"
Java(TM) SE Runtime Environment (build 1.7.0_01-b08)
Java HotSpot(TM) Client VM (build 21.1-b02, mixed mode, sharing)

D:\>java -Xint -version
java version "1.7.0_01"
Java(TM) SE Runtime Environment (build 1.7.0_01-b08)
Java HotSpot(TM) Client VM (build 21.1-b02, interpreted mode, sharing)

tianmai-mac:~ fanhua$ java -Xcomp -version
java version "1.7.0_11"
Java(TM) SE Runtime Environment (build 1.7.0_11-b21)
Java HotSpot(TM) 64-Bit Server VM (build 23.6-b04, compiled mode)

 

虚拟机可以利用解释执行快速启动应用,在运行期根据执行的情况,把热点代码编译成机器代码来执行,编译本身是比较耗时的,但是本地代码执行速度更快。虚拟机可以利用两者协作在启动响应时间和运行期执行效率之间获得折衷。另外解释器可以作为编译器的“逃生门”,运行期编译器可以采取一些比较激进的优化措施,比如说把某个接口的实例的虚方法直接编译为本地代码,而等在后续执行过程中发现接口实例是另外一个类实例的时候,就回退这种优化,叫做逆优化,回退到解释器来执行。(这种描述是否严谨?)

HotSpot VM的编译器分为Client Complier和Server Complier,简称C1和C2编译器。C1编译器做一些快速的优化,C2做一些更耗时的优化但是产生更高效的代码。JDK6加入了多级编译器,解释器可以和C1、C2编译器一起协同运行,JDK7 -server模式下默认启用多级编译器:1、第0级:采用解释器解释执行,不采集性能监控数据,可以升级到第1级;2、第1级,采用C1编译器,会把热点代码快速的编译成本地代码,如果需要可以采集性能数据。3、第2级,采用C2编译器,进行更耗时的优化,甚至可能根据第1级采集的性能数据采取激进的优化措施。

像JRockit虚拟机是没有解释器的,因为它的目标是在服务器上运行,直接采用解释模式,启动过程虽然稍长但是运行期效率更高。

D:\>java -version
java version "1.6.0_22"
Java(TM) SE Runtime Environment (build 1.6.0_22-b04)
Oracle JRockit(R) (build R28.1.1-14-139783-1.6.0_22-20101206-0241-windows-ia32, compiled mode)

此处提到的优化,同时包含了编译期优化和运行期优化。

二、编译期优化

1、javac的编译过程,编译过程不是了解的重点,详细了解需要结合编译原理的整个过程来。此处大概提一下javac的编译过程:解析和填充方法表 -> 注解处理 -> 分析和字节码生成

2、语法糖衣。语法糖衣是指加入到语言中的一些语法特性,为语言使用者带来代码编写上的便利,但是不影响语言本身的功能,甚至都不直接在编译后的代码中体现出来。

java从jdk5后加入了很多语法糖衣,如泛型、自动拆装箱、循环遍历(还有呢?):

public static void main(String[] args) {
		List<String> list = new ArrayList<String>();
		list.add("1");
		list.add("2");
		
		Integer i = 3;
		i += 2;
		
		for(String s : list){
			System.out.println(s);
		}

	}

  编译后的字节码:

  // Method descriptor #15 ([Ljava/lang/String;)V
  // Stack: 2, Locals: 5
  public static void main(java.lang.String[] args);
    new java.util.ArrayList [16]
    dup
    invokespecial java.util.ArrayList() [18]
    astore_1 [list]
    aload_1 [list]
    ldc <String "1"> [19]
    invokeinterface java.util.List.add(java.lang.Object) : boolean [21] [nargs: 2]
    pop
    aload_1 [list]
    ldc <String "2"> [27]
    invokeinterface java.util.List.add(java.lang.Object) : boolean [21] [nargs: 2]
    pop
    iconst_3
    invokestatic java.lang.Integer.valueOf(int) : java.lang.Integer [29]
    astore_2 [i]
    aload_2 [i]
    invokevirtual java.lang.Integer.intValue() : int [35]
    iconst_2
    iadd
    invokestatic java.lang.Integer.valueOf(int) : java.lang.Integer [29]
    astore_2 [i]
    aload_1 [list]
    invokeinterface java.util.List.iterator() : java.util.Iterator [39] [nargs: 1]
    astore 4
    goto 70
    aload 4
    invokeinterface java.util.Iterator.next() : java.lang.Object [43] [nargs: 1]
    checkcast java.lang.String [49]
    astore_3 [s]
    getstatic java.lang.System.out : java.io.PrintStream [51]
    aload_3 [s]
    invokevirtual java.io.PrintStream.println(java.lang.String) : void [57]
    aload 4
    invokeinterface java.util.Iterator.hasNext() : boolean [63] [nargs: 1]
    ifne 52
    return
  Line numbers:
    [pc: 0, line: 13]
    [pc: 8, line: 14]
    [pc: 17, line: 15]
    [pc: 26, line: 17]
    [pc: 31, line: 18]
    [pc: 41, line: 20]
    [pc: 63, line: 21]
    [pc: 70, line: 20]
    [pc: 80, line: 24]
  Local variable table:
    [pc: 0, pc: 81] local: args index: 0 type: java.lang.String[]
    [pc: 8, pc: 81] local: list index: 1 type: java.util.List
    [pc: 31, pc: 81] local: i index: 2 type: java.lang.Integer
    [pc: 63, pc: 70] local: s index: 3 type: java.lang.String
  Local variable type table:
    [pc: 8, pc: 81] local: list index: 1 type: java.util.List<java.lang.String>
  Stack map table: number of frames 2
    [pc: 52, full, stack: {}, locals: {java.lang.String[], java.util.List, java.lang.Integer, _, java.util.Iterator}]
    [pc: 70, same]

  泛型list.add处添加的是Object(line 11);i处自动调用了intValue和valueOf(line 26);for循环处编译为了Iterator进行操作(line 54)

三、运行期优化

1、热点代码及如何确定热点代码

在HotSpot VM mixed mode中,只有热点代码才会被编译成本地代码。什么样的代码才是热点代码呢?执行频繁的代码:1、频繁执行的方法;2、频繁执行的代码块,如循环体。什么样才算执行频繁呢执行次数达到一定上限。上限又是多少?后续会讲到。

另外,怎么确定一个方法或者代码块的执行次数呢通过计数法,方法调用是方法调用计数器,代码块是回边计数器。

方法调用计数器计数具体有两种方式:1、采样计数,运行期定期对栈顶的方法进行采样,采集方法调用次数信息。此方法简便,但是有时候失真,比如方法长时间阻塞时。2、调用计数,为每一个方法维护一个计数器,方法没调用一次就加1,如果方法调用计数器+回边计数器超过了阙值(通过ComplieThreshold设置)就申请编译。编译的时候,方法会继续以解释的方式执行,等编译完成后再次调用方法的时候就执行编译后的代码。编译完后会把方法直接引用指针指向编译后的代码。可以通过-XX:-BackgroundCompilation方式禁止后台编译,这种情况下,会停止以解释方式执行,等待编译完成。(对代码块和方法都有效么?)

回边计数器是在回边指令执行的时候进行判断是否有编译后的代码,如果没有则判断引用计数器(方法调用计数器+回边计数器)是否达到阙值,如果没有达到则加1,然后解释执行,否则提交编译请求,虽然是代码块频繁执行,但是编译的时候确实编译整个方法。由于代码块可能是在代码块在解释执行过程中直接切换到本地代码执行,所以也叫做栈上替换(OSR,OnStackReplacement),代码块的编译请求也叫OSR编译请求。那岂不是要替换整个栈帧??

那多少次的调用次数会启动编译呢?默认的,HotSpot Client VM下执行1500次的方法和Server模式下10000次的代码可以算得上热点代码:

package com.yymt.jvm.syn.runtime.optimize;

public class FrequentCodeTest {

	private static int cnt = 1500;

	public static void main(String[] args) {
		for (int i = 0; i < cnt; i++) {
			looped();
		}
	}

	private static int looped() {
		int i = 0;
		i++;
		return i;
	}
}

 通过参数-XX:+PrintComplication启动vm,看到如下:

  1       java.lang.String::equals (88 bytes)
  2       java.lang.String::hashCode (60 bytes)
  3       java.io.Win32FileSystem::normalize (143 bytes)
  4       java.lang.String::indexOf (151 bytes)
  5       java.lang.String::charAt (33 bytes)
  6       java.lang.String::lastIndexOf (156 bytes)
  7       java.lang.AbstractStringBuilder::append (40 bytes)
  8       java.io.Win32FileSystem::isSlash (18 bytes)
  9       java.lang.Object::<init> (1 bytes)
 10 s     java.lang.StringBuffer::append (8 bytes)
 11       java.io.Win32FileSystem::normalize (231 bytes)
 12       com.yymt.jvm.syn.runtime.optimize.FrequentCodeTest::looped (7 bytes)

 如果把cnt设置为1499则没有第12条的优化。如果再加上-server参数,则只有在cnt>=10000时候才会优化了。

 

而对于频繁执行的代码块,Client模式下回边计数器是通过OSR比率(OnStackReplacePercentage)*CompileThreshold/100 计算得到。默认osr比率是933,client的ComplieThreshold为1500,此处为13995,此处要注意方法调用本身也会计一次方法调用的,所以下边的代码在15899时候会编译loopedMethod,但是在15898时候不会:

package com.yymt.jvm.syn.runtime.optimize;

public class FrequentCodeTest {

	private static int cnt = 15899;

	/**
	 * JVM参数:
	 * 	-XX:+PrintCompilation
		-XX:CompileThreshold=10000
		-XX:OnStackReplacePercentage=159
	 * @param args
	 */
	public static void main(String[] args) {
		loopedMethod();
	}

	private static int loopedMethod() {
		int i = 0;
		i++;
		for (int j = 0; j < cnt; j++) {
			i++;
		}
		return i;
	}

}

 输出:

  1%      com.yymt.jvm.syn.runtime.optimize.FrequentCodeTest::loopedMethod @ 10 (25 bytes)

 Server模式下计算有些不同,是CompileThreshold * (OnStackReplacePercentage - InterpreterProfilePercentage) / 100,InterpreterProfilePercentage是解释器监控比率,实际我在执行的时候达到计算之后并不触发编译。什么原因呢?

package com.yymt.jvm.syn.runtime.optimize;

public class FrequentCodeTest {

	private static int cnt = 12000;

	/**
	 * JVM参数:
	 * 	-server
		-XX:+PrintCompilation
		-XX:CompileThreshold=10000
		-XX:OnStackReplacePercentage=140
		-XX:InterpreterProfilePercentage=40
		-XX:-BackgroundCompilation
	 * @param args
	 */
	public static void main(String[] args) {
		System.out.println(loopedMethod());
	}

	private static int loopedMethod() {
		int i = 0;
		i++;
		for (int j = 0; j < cnt; j++) {
			i++;
			if(i % 1000 == 0){
				System.out.print(i);
			}
		}
		return i;
	}

}
 没有编译代码信息输出。

2、编译过程

字节码->方法内联、常量传播等->HIR(SSA)->空值检查消除、数组边界检查消除等->优化后的HIR->LIR->寄存器分配、窥孔优化->机器码生成->本地代码

3、一些编译优化技术

a、方法内联,方法调用本身是有代价的,要从常量池找到方法地址,然后保存当前栈帧状态,压入新栈帧启动调用过程,调用完弹出,并恢复调用者栈帧。而在运行期,如果方法很频繁的执行,就会运行期把方法内联到调用者方法内部,减少频繁调用的开销:(频率值如何确定?执行过程中内联进去么?弹出当前栈帧,恢复调用者栈帧,栈帧其他数据怎么处理?a方法内联到b方法中后,下次调用到b的时候,会是已经内联过的版本么?)

package com.yymt.jvm.syn.runtime.optimize;

public class InlineTest {

	/**
	 * VM参数:-XX:+PrintInlining
	 * @param args
	 */
	public static void main(String[] args) {
		int r = 0;
		for(int i = 0;i < 100000;i++){
			r += getValue(i);
		}
		System.out.println(r);
	}

	public static int getValue(int i){
		i++;
		return i;
	}
}

 输出:

......
@ 9   com.yymt.jvm.syn.runtime.optimize.InlineTest::getValue (5 bytes)
......

  由于java是动态分派的,所以invokevirtual指令调用的类的实例方法就不能简单的内联,因为运行期可能有多个版本,jvm团队想了

一些办法,比如类型继承关系分析判断继承体系中接口或抽象类方法是否只有一个实现,如果是只有一个,则可以通过激进行为优化,

把方法内联,但是预留逃生门--守护内联,监控类型加载情况,如果加载了导致体系发生变化的类,则需要抛弃已经内联的版本

(就算此处没有用到?为何不是运行过程中监控实际类型?)如果通过继承关系分析发现有方法有多个实现版本,则使用内联缓存,

第一次方法调用时,内联一个版本,后续执行时候先检查方法接受者是否一样,如果一样的使用内敛缓存中内敛过的方法,否则取消内敛,

通过虚方法表分派。

b、数据边界检查,jvm在数组访问过程中会检查访问的下标是否越界,这本来是一个为了安全性提供的功能,但是像下边这段代码,在数组访问过程中,每次都去校验,性能损耗也是很大的。jvm在运行期做了优化,只用校验用来访问数组的起始下标在0到数组最大长度-1之内就行了。通过数据流分析实现。

int [] arrs = new int[10000];
for(int i = 0;i < 10000;i++){
	arrs[i] = i;
}

 c、公共子表达式消除,如果计算a = b * c + c * d + b * c * e,观察返现b*c需要执行两次,jvm在运行期会对这个问题进行优化,设E = b*c ,a = E + c * d + E * e,保存中间结果集,减少计算次数。

d、逃逸分析,如果方法执行过程中,创建了一个对象,并且这个对象没有赋值给静态变量引用,也没有赋给别的对象的字段,即除了当前局部变量表外,没有通过赋值再次添加该对象到gc reference chains中,这个对象就叫做非逃逸对象。对于这种对象,我们就可以做一些优化:

*栈上分配:由于对象是私有的,可以把对象分配在栈上,随方法调用分配内存,结束回收内存,不用再通过堆上gc释放,并且访问效率要高一些。

*标量替换:基本类型如int、long、reference无法进一步分解的数据了,就叫做标量,而像对象是由很多标量组成,叫做聚合量,如果一个对象是非逃逸的,则可以将其用标量分配在栈空间,每次访问都是访问基本类型数据

*同步消除:栈是私有的空间,所以不存在多线程共享栈资源,如果该对象的方法有同步方法,可以消除同步,减少同步资源消耗。

该技术目前尚不成熟,-server模式下可以通过参数-XX:+DoEscapeAnalysis开启逃逸分析,-XX:+PrintEscapeAnalysis

(jdk1.7.0_01-b08 实际使用时候不认识该命令)打印逃逸分析结果。-XX:+EliminateAllocations开启标量替换,

-XX:+PrintEliminateAllocaiton打印标量替换(同样不认识)。-XX:+EliminateLocks开启同步锁消除。

分享到:
评论
1 楼 MultiArrow 2013-01-16  
引用
像JRockit虚拟机是没有解释器的,因为它的目标是在服务器上运行,直接采用解释模式,启动过程虽然稍长但是运行期效率更高。

这一句,直接采用解释模式应该改为编译模式的吧。

相关推荐

    狂神说JVM探究.rar

    - 运行时常量池:方法区的一部分,存储编译期生成的各种字面量和符号引用。 4. **垃圾收集(GC)**: - 垃圾收集的目的是自动回收不再使用的对象所占用的内存。 - 分代收集理论:将堆分为新生代(Eden、Survivor...

    学习笔记.pdf

    Scala 语言的静态类型可以在编译期检查类型错误,避免了运行时的类型错误。Scala 语言的面向对象特性使得它可以与 Java 语言无缝集成。Scala 语言的函数式编程特性使得它可以编写更加简洁、灵活的代码。 3. Scala ...

    Java学习笔记精彩版.doc

    2. 运行期:JVM 将字节码文献解释执行,并与操作系统和硬件交互。 五、Java 语言基础 Java 语言基础包括: 1. 修饰符:public、private、protected 等。 2. 类和对象:类是对象的模板,对象是类的实例化。 3. ...

    1 JAVA学习笔记.zip

    - **注解**: 提供元数据,帮助编译器和JVM在编译期或运行期进行验证和处理。 9. **泛型** - **泛型**: 提高类型安全,减少强制类型转换,增强代码可读性和重用性。 10. **JDBC** - **Java数据库连接**: 提供与...

    java学习笔记

    - **运行期**:通过JVM解释执行字节码文件。 #### 八、Java程序的编写示例 以下是一个简单的Java程序示例,用于输出"Hello!!"。 ```java public class HelloWorld { public static void main(String[] args) { ...

    Java学习笔记

    ### Java学习笔记 #### Java的优势 1. **跨平台(平台=OS)可移植性** - **字节码文件**:Java程序被编译成字节码文件(`.class`),这些文件不包含任何特定于操作系统的内存布局信息。这意味着它们与操作系统和...

    _JavaSE内部学习笔记

    ### JavaSE内部学习笔记知识点概览 #### 一、Java语言概述 - **1.1 基础常识** - **软件分类**:软件分为系统软件和应用软件两大类。系统软件支持计算机硬件正常工作,包括操作系统、设备驱动程序等;应用软件则...

    1 第一天 魔乐java基础视频学习笔记.docx

    自1995年发布JDK1.0以来,Java经历了三个发展阶段,包括完善期、平稳期和发展期。Oracle在2010年收购SUN公司,使得Java成为Oracle的重要组成部分,进一步巩固了其在编程语言市场的地位。 【Java的主要特点】 1. ...

    SUN SCJP 认证笔记

    了解注解的使用及其在编译期和运行期的作用。 8. **异常处理**:熟悉Java的异常体系,如何抛出和捕获异常,以及如何使用try-catch-finally语句块进行异常处理。 9. **JVM原理**:虽然不是考试的直接内容,但理解...

    魔乐JAVA培训课堂笔记

    - **平稳期**(JDK 1.3至JDK 1.4):在此期间,Java语言的核心特性相对稳定,主要致力于性能优化和错误修复。 - **发展期**(JDK 1.5至JDK 1.7):此阶段引入了大量的新特性,包括泛型、注解、枚举等,极大地提高...

    java_note笔记

    - **处理器**:`@Processor`注解用于自定义编译期注解处理器,执行自定义的编译逻辑。 这些笔记内容全面地覆盖了Java开发的核心概念和技术,对于学习和提升Java技能非常有帮助。通过深入理解和实践,开发者可以更...

    JSD1906达内Java .rar

    【JSD1906达内Java .rar】是一个压缩包文件,主要包含了"all代码和笔记",意味着它提供了全面的学习资源,很可能是达内教育机构针对JSD1906期Java课程的学生所准备的资料。达内是知名的IT培训机构,其课程通常涵盖了...

    Java笔记---李兴华

    这一时期主要对已有的特性和API进行了优化和改进。 - **发展期**:从JDK1.5至JDK1.7版本。这一阶段引入了许多新特性,如泛型、注解等,大大增强了Java的功能性和灵活性。 - **更名与升级**:1998年,Java2发布,...

    黑马程序员_Java基础辅导班教程课件[第01期]第4天

    Java是一种广泛使用的面向对象的编程语言,以其跨平台性、高效性和丰富的类库而闻名。在"黑马程序员_Java基础辅导班教程课件[第01期]第4...此外,理解JVM(Java虚拟机)的工作原理和内存管理也对优化代码性能至关重要。

    CTS-seminar-1081-Vlaicu-Elena

    6. **JVM优化**:了解JVM的工作原理,如类加载机制、内存模型、垃圾回收算法,有助于进行性能调优。 7. **Java测试**:JUnit、TestNG等单元测试框架,以及Mockito等模拟工具,对于保证代码质量至关重要。 由于没有...

Global site tag (gtag.js) - Google Analytics