性能微优化-编译Java代码
第5章的性能优化策略适合各种开发语言,应用这些策略有较大的系统性能提升。有些场景下,核心系统或者工具类性能有极致的要求,这时候可以应用Java 自身的性能最佳实践,这些实践需要以Java编译和运行作为基础知识,掌握字节码和JIT等知识。
学完本章,将掌握如下知识
- Java编译,把Java源码编译成字节码,并在编译过程中做的性能优化。
- 字节码的基本知识,包括如何查看字节码,以及字节码的常用指令。本章的部分性能优化策略需要结合字节码解释。同时Java系統很多高级功能均是通过动态字节码生成实现
- JIT基本知识,包括了解JIT的运行过程,以及观察JIT。通过对核心系统或者工具的运行的JIT日志进行分析,确保实现预期的Java优化。
- Java语言中的一些高性能实践,可以应用核心系统或者工具库提高性能,比如String的最佳使用办法,位运算等
前三节涉及到JVM的知识,读者可以先阅读6.4节以后的内容,如果有不理解,在查看前3节的相应内容
编译Java代码
Java的编译通常有
- 前端编译Javac,把Java源码编译成字节码,字节码可以被JVM解释执行。
- 即时编译JIT(Just-In-Time),字节码通过JVM执行过程中,动态编译成机器码的过程,JIT通常会分析系统的热点,对热点代码会再次尝试更加激进的优化措施从而提高Java系统性能。
- 提前编译AOT(Ahead-Of-Time),将Java源码编译成机器码后通过操作系统执行,优点是启动速度快,执行速度快,缺点是牺牲了平台无关性,另外,有些优化需要在运行过程中分析确认,这时候只有JVM才能进步一优化,而AOT做不到。
这里的前端编译是指将Java源码编译成Java字节码的过程,JDK提供javac命令将Java源程序编译成class
${java_home}/bin/javac Hello.java前端编译Java源程序不一定是来自文件,比如,可以使用javax.tools.JavaCompiler(JDK6开始支持)类,将字符串源码编译成字节码存到Test.class里
public class CompileString {
public static void main(String[] args) throws IOException {
//获取一个JavaCompiler
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//编译的目标Java源码
JavaStringObject stringObject =
new JavaStringObject("Test.java", getSource());
//编译输出目录
DiagnosticCollector<JavaFileObject> diagnostics =
new DiagnosticCollector<>();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
String classes = System.getProperty("user.dir")+"/jmh-java/target/classes";
File classesFile = new File(classes);
fileManager.setLocation(CLASS_OUTPUT,Arrays.asList(classesFile));
//配置编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null,
fileManager, diagnostics, null, null, Arrays.asList(stringObject));
//开始编译
boolean success = task.call();
System.out.println(success?"编译成功":"编译失败");
diagnostics.getDiagnostics().forEach(System.out::println);
}
public static String getSource() {
return "public class Test {"
+ " }";
}
}这里有几个类需要重点说明
| 类 | 说明 |
|---|---|
| JavaCompiler | 用于编译java代码,javac命令也会调用JavaCompiler。diagnostics用于在编译过程中保留调试,警告,或者错误信息。 |
| StandardJavaFileManager | 对象用于管理源码和编译后输出文件。本例子设定了CLASS_OUTPUT 目录 |
| JavaStringObject | 是自定义的一个对象继承了SimpleJavaFileObject,用于代表Java源代码 |
JavaStringObject定义如下,name表示Java名,source表示其源码:
public class JavaStringObject extends SimpleJavaFileObject {
private final String source;
protected JavaStringObject(String name, String source) {
super(URI.create(name), Kind.SOURCE);
this.source = source;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors)
throws IOException {
return source;
}
}JavaStringObject最重要的方法是实现了getCharContent,提供Java代码。这个例子里,Java代码以字符的形式提供,而不是文件。
为了编译JavaStringObject,需要创建 CompilationTask,并执行call方法 ,如下代码
JavaCompiler.CompilationTask task = compiler.getTask(null,
fileManager, diagnostics, options, null,
Arrays.asList(stringObject));
boolean success = task.call();
System.out.println(success?"编译成功":"编译失败");
diagnostics.getDiagnostics().forEach(System.out::println);task.call 如果返回true,表示编译成功,可以在/jmh-java/target/classes 下找到编译好的Test.class.
代码最后,打印出编译过程中的调试,告警,或者错误信息
在Java源码编译成字节码过程中,Javac会处理Java的语法糖,这是通过com.sun.tools.javac.comp.Lower类来处理,比如
1) 条件编译。如下代码FLAG是final类型,因此编译时候可以省去这个代码
static final boolean FLAG = false;
public void run(){
if(FLAG){
System.out.println("hello");
}
}通过com.sun.tools.javac.comp.Lower.visitIf 来处理
编译结果,run方法是个空方法。
public void run() {
}如果你想查看com.sun.tools.javac.comp.Lower.visitIf源码,你可在visitIf代码打上断点,并修改CompileString的getSouce方法,如下
public static String getSource() {
return " public class Test {"
+"static final boolean FLAG = false;"
+ " public void run(){"
+ " if(FLAG){"
+ " System.out.println(\"hello\");"
+ " }"
+ "}"
+ "}";
}以Debug方式运行CompileString,可以看到JavaCompile进入visitIf代码的cond.type.isFalse() 分支
2) 最常见的语法糖之一是foreach,如下代码
List<Integer> list = Arrays.asList(1,2);
for(Integer key:list){
System.out.println(key);
}编译成class后,用eclipise或者idea等IDE直接打开class,,可以看到反编译后的代码
List<Integer> list = Arrays.asList(1, 2);
Iterator var2 = list.iterator();
while(var2.hasNext()) {
Integer key = (Integer)var2.next();
System.out.println(key);
}这段通过com.sun.tools.javac.comp.Lower.visitIterableForeachLoop方法处理。
3)常量折叠
static final int b = 3;
public void run(){
int a = 1+b;
System.out.println(a);
}编译代码后是, ConstFold.fold
public void run() {
int a = 4;
System.out.println(a);
}4) 装箱和拆箱,这个功能是在代码在Lower.unBox处理
Integer i = 1;编译代码
Integer i = Integer.valueOf(1);注意,当使用IDE反编译class的时候,仍然看到的是Integer i=1, 这是IDE反编译工具对反编译结果的显示做了还原处理
其他编译时期生成的还有默认构造函数,语法糖还包括字符串+ 改成使用StringBuilder::append方法(注意,JDK9后将调整,会在6.4里描述此处改动),Lambda语法,断言Assert语法糖,枚举处理,以及switch支持String等语法糖.
