Java有三种编译器i,一种是前端编译器,就是将java文件转变为class文件,这是在编译阶段。一种运行期编译器(JIT编译器),将字节码文件转变为机器码,这是在运行阶段。
编译期优化
编译过程主要分为:
- 词法语法分析。
- 填充符号表。
- 注解处理器。
- 语义分析。
- 生成字节码。
泛型
泛型是java语法糖的一种,他的本质是参数化类型。
泛型主要有泛型类,泛型接口,泛型方法。
Java中的泛型只存在于编译阶段,只是用来在编译阶段进行数据校验的作用,在运行时期,泛型就被擦除了,替换为他的原生类型。
1 | List<String> stringArrayList = new ArrayList<String>(); |
在运行阶段,泛型已被擦除,所以,都被替换为ArrayList,是相同的。
个人觉得泛型的作用就是在编译阶段进行语义的审查的作用。
泛型擦除只是从字节码中擦除了,但是元数据中还是保留了泛型信息,所以,我们还是可以通过反射手段取得参数化类型。
- 获取方法返回泛型
1 | Method method = MyClass.class.getMethod("getStringList", null); |
- 获取成员变量泛型参数
1 | Field field = MyClass.class.getField("stringList"); |
运行期优化
javac 将程序源代码编译,转换成 java 字节码,JVM 通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。很显然,经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢很多。为了提高执行速度,引入了 JIT 技术。
当JVM发现某一段代码执行特别频繁的时候,就会认为他是热点代码,为了提高执行效率,虚拟机就会用过JIT编译器将这些代码编译成机器码,缓存下来。

那么,为什么不直接编译呢?
首先,如果这段代码本身在将来只会被执行一次,那么从本质上看,编译就是在浪费精力。因为将代码翻译成 java 字节码相对于编译这段代码并执行代码来说,要快很多。当然,如果一段代码频繁的调用方法,或是一个循环,也就是这段代码被多次执行,那么编译就非常值得了。因此,编译器具有的这种权衡能力会首先执行解释后的代码,然后再去分辨哪些方法会被频繁调用来保证其本身的编译。
JVM是采用解释器与编译器并行的架构。
程序启动时,解释器首先发挥作用,省掉编译的时间,迅速执行。
程序运行后,随着时间的推移,编译器发挥作用,将代码编译成机器码,获取更高执行效率。
HotSpot有两个即时编译器,Client Compiler和Server Compiler,一个注重优化速度,一个注重优化质量。
Client Compiler:编译速度快,优化简单可靠。
Server Compiler:会有一些编译时间比较长的优化。
热点代码
热点代码有两类。
- 被多次调用的方法。
- 被多次执行的循环体。
那么,怎么计数呢?
- 基于采样的热点探测。虚拟机周期性的检查各个线程栈顶,如果某个方法频繁出现,那么这就是一个热点方法。
- 基于计数器的热点探测。为每个方法(代码块)建立计数器,统计执行次数。
在HotSpot采用的是第二种,有方法计数器来统计方法调用次数,回边计数器来统计循环代码调用次数。当计数器超过阈值的时候,就会对其进行编译。
优化技术
公共子表达式消除
如果一个表达式E计算过了,且他的值没有任何变化,那么E再次出现就没必要再次计算,直接用结果。
方法内联
内联举例:
1 | public int add(int a, int b , int c, int d){ |
内联之后:
1 | public int add(int a, int b , int c, int d){ |
调用一个方法需要建立栈帧等,成本比较大,方法内联可以很好的消除方法调用的成本。
内联条件:
- 热点代码。
- 方法体不是太大。
- 如果希望方法被内联,尽量用private、static、final修饰,这样jvm可以直接内联。如果是public、protected修饰方法jvm则需要进行类型判断,因为这些方法可以被子类继承和覆盖,jvm需要判断内联究竟内联是父类还是其中某个子类的方法。
但是,内联并不是这么简单的,我们的程序中大多都是虚方法(不用private,final,static修饰的),那么就会有多态的可能,不知道会不会有子类重写了方法。
JVM团队采用CHA来解决这个问题。
- 方法是非虚方法,直接内联即可。
- 是虚方法,看程序内该方法是否有多个实现,若只有一个,直接内联。
- 若有多个,采取内联缓存。内联缓存中保存的是第一次调用使用的版本,并且以后每次调用都会比较版本信息,一致,继续使用。
- 不一致,取消内联。
逃逸分析
- 方法逃逸:一个对象在方法中被定义后,可能被其他方法使用。
- 线程逃逸:一个对象在方法中定义后,可以被别的线程访问到。
如果一个对象没有逃逸,可以对其做以下优化。
- 栈上分配。如果一个对象没有逃逸出方法之外,那么只会在一个方法内部访问,这样的话,将其分配在栈上,方法结束,对象自动被销毁,GC压力会小很多。
- 同步消除。因为一个对象没有逃逸,所以不会被外部线程访问到,所以同步措施也可以消除。
- 标量替换。标量是指一个数据无法再分解成更小的数据,例如基本数据类型,引用类型等。如果一个对象不会被外部访问,可以选择不创建这个对象,转而直接创建这个对象会被方法调用的成员变量。