三种编译器

通常java代码需要进行编译成java字节码文件后才能被jvm运行,但是在运行过程中,jvm也需要把java字节码编译成二进制汇编指令集,有三种编译器指定

  1. -client: 可以叫C1,初始化编译java字节码之后缓存汇编指令较少,用于快速启动,的场景下,比如打开一个app,可以快速打开,给用户一种加载速度很快的感觉,但是它不适合那种长时间运行的程序
  2. -server: 可以叫C2,初始化编译java字节码之后缓存汇编指令较多,java8默认是这个编译器,可以使用java -version命令查看到使用的默认编译器,用于长时间启动状态的程序
  3. -XX:+TieredCompilation: 分层编译,默认是false,如果关闭分层编译,会直接使用-server,可以把它看做自动调整的一种方案,在长时间运行的程序中,使用这个编译器是最佳

    增加缓存指令内存

  4. -XX:ReservedCodeCacheSize=128m
  5. 这些编译器缓存的汇编指令,有一个存储默认值瓶颈,可以使用工具jconsole中的内存查看,图标选择,Compressed Class Space 来观察目前程序的最大值,已使用值是多少,如果使用的够多了,可以适当的增加这个存储最大值,有利于使用-server的时候能多存储一些指令集

    编译阈值

  6. -XX:CompileThreshold=10000
  7. 上面所提到编译,缓存指令集,并不是编译所有的类,只是相对编译的较多,或较少,Java程序运行的速度是越来越快,有一个预热的过程,当一段方法,或者循环,在短时间执行次数较多,超过了CompileThreshold设置的阈值就会进入编译队列,在经过编译缓存指令后,程序才开始变快,设置这个参数的目的就是减少预热的时间,和让一些永远不会被编译缓存的方法能够被缓存指令集,因为这个调用次数的计数器,在一段时间后会减少,如果每次都没有到这个阈值,之后又减少调用,这样就永远不会预热这个方法

    开启编译日志

    -XX:+PrintCompilation=true

    Jstat 了解编译器内部工作

  8. 在没有开启编译日志的情况下,可以使用这个方法来处理,有两个参数可以获取信息
  9. jstat -compiler 端口号 可以获取到有多少方法被编译的概要信息,这里也列出了编译失败,和最近编译失败的方法名
  10. jstat -printcompilation 端口号 1000(多少毫秒执行一次) 获取最近被编译的方法,可以看到编译出错,出错的原因一般两个,编译缓存满了,加大编译指令集的缓存,编译类的时候发生了修改,之后会再次编译

    编译队列

  11. 编译阈值达到限制后,进入编译队列,这是个异步的,编译队列遵守的是谁越重要就优先编译谁,取决于调用计数器,次数越多越重要,并不是前进先出的规则,这也是编译日志输出时id乱序的一个原因
  12. 处理编译队列的线程个数取决于各方面因素,比如在一个cpu的情况下,C1=1,C2=1,四个cpu则C1=1,C2=2
  13. 如果是分层编译器则是开启多个C1和C2线程
  14. -XX:CICompilerCount=3 设置编译队列线程个数,对于分层编译,其中三分之一的个数(至少一个)会被分给C1,其余的线程(至少一个)被分给C2,所以可以得出分层编译器最少需要2个线程
  15. -XX:+BackgroundCompilation=true 开启异步执行队列,默认值是true

内联

  1. 编译最重要的是优化方法的内联,就是优化代码
  2. 内联主要是修改你方法的引用调用方式,比如一般调用get,set方法来对属性赋值,取指,优化后的代码是直接访问属性进行操作,不通过方法了,
  3. -XX:-Inline可以关闭,事实上永远不要关闭,会非常影响执行效率
  4. 方法是否内联,取决于它的大小,和调用频繁度
  5. 如果方法是因为调用频繁而可以内联,只有在它小于325字节才会内联,可以 -XX:MaxFreqInlineSize 修改大小
  6. 如果方法占用字节小于35的时候也会内联,可以 -XX:MaxInlineSize 修改大小
  7. 从这里看得出,保持方法职责单一是很有必要的,最好是方法尽量少代码,内联优化大部分用在了属性的封装方法上,例如get,set

    逃逸分析

    是编译器能做的最复杂的优化,默认是开启的,-XX:+DoEscapeAnalysis=false 可以关闭

    逆优化

    讲的是在编译缓存指令后,不得不撤销某些编译,两种情况是会发生逆优化

  8. 代码被丢弃:在分层编译中发生,C2编译结果覆盖C1,比如接口的实现,在不同条件下需要被赋予不同的实现类的实体
  9. 生产僵尸代码:被GC回收了,编译器就会将它从缓存中移除

    分层编译级别

  10. 因为C1编译级别有3中,分层编译总共有5中级别
  11. 0:解释代码,1:简单C1编译,2:受限C1编译,3:完全C1编译,4:C2完全编译
  12. 多数方法默认第一次编译级别是3,如果运行的做够频繁,它就会变成编译级别4,结果覆盖掉3级别
  13. 如果C2编译队列满了,会取出以级别2的方式进行编译,这个编译因为内容少,所以速度更快,同样的也会升级编译级别
  14. 如果C1编译队列忙,可以安排原来3级别的编译,直接进行4级别编译,同样它也能转到2级别编译,然后升级到4级别
  15. 不太重要的方法可以从2级别或3级别开始编译,随后会因为他们的重要性低,降级到1级别
  16. 代码在逆编译的时候级别会转为0
  17. 当方法按期望的顺序,即级别 0 → 级别 3 → 级别 4 编译时,性能可以达到最优。如果方法经常被编译为级别 2,并且还额外有可用的 CPU 周期,那就可以考虑增加编译器的线程数,从而减少 server 编译器队列的长度。如果没有额外可用的 CPU 周期,那你唯一能做的就是尽力减小应用的大小。

    final 关键字

    在以前这个关键字是影响性能的因素,但是现在无论有没有final关键字都不会影响性能,可以只在确实必要的时候添加这个关键字

    结尾

    代码越简单,优化越多,分析反馈和逃逸分析可以使代码更快,但是复杂的循环结构和大方法限制了它的有效性