【学习JVM】程序编译与代码优化(语法糖)

程序编译与代码优化(语法糖)

需要参考的准备数据:

《深入理解JAVA虚拟机》

需要参考的知识点:
字节码结构
javac编译器
泛型编程
装箱和拆箱
注解
JIT即时编译器
类加载过程

知识的记录方式:
 - 查看博客,把没有遇见过的或者觉得比较经典的博文段落摘录
 - 自己的理解以条目的形式展示
 - 知识误解标记
 - 知识盲区标记
 - JAVA内存模型这个知识点基本上每本书都会讲解,最好的方式将每本的书的这一章都读一下,然后摘录重要的知识点,通过反复和串联达到效果。

重要笔记:

javac编译器

  • javac并非JVM规范中必要的,而是由虚拟机厂商自己实现的一套工具。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0aGY9qia-1579504969679)(https://i.loli.net/2019/04/26/5cc2c468a9163.png)]

  • javac编译的3大主要过程:
    1.解析语法树和填充符号表过程
    2.注解处理过程(插入式注解处理器的处理过程)
    3.分析和字节码生产过程

  • 将源代码通过词法分解后以语法树的结构存储

  • 将语法树转化成符号表,符号表类似一个信息仓库,存放着接下来编译的各个阶段所需要的信息

  • 注解处理阶段,将注解解析嵌入到编译过程,类似于提供用于介入编译过程的接口。

  • 最后将语法糖解析(这里会发生常量折叠,比如1+2会编译成3,还会将泛型等语法糖解析成普通类型方法)并转化成JVM标准规范的字节码并写入到磁盘。

    • 语法糖是编译器的小把戏,在小把戏下会增加代码的可读性,提高编码的效率。不过我们要去了解语法糖的真实面目,从而更好地去使用它,而不被他迷惑。(比如integer.value(1)和integer.value(2)其实是同一个对象。因为装箱被缓存了)
  • Java语言层面的校验由编译器负责(比如,java层面的方法签名包括:方法名称,参数类型,参数顺序,由编译器保证方法签名不能重复),而字节码层面的校验由JVM的类加载逻辑来负责(比如,字节码层面的方法签名包括:方法名称,参数类型,参数顺序以及返回值、异常列表,由JVM运行时类加载保证方法签名不能重复)

  • 真正的泛型在编译后会产生新的类型,叫做类型膨胀,而java的伪泛型不会,它会在编译后依然是原类型,所有的泛型标记从方法的字节码中擦除,只能通过类字节码的signature中去获取(可以通过反射获取)。

  • java的伪泛型会导致无法支持泛型类型方法重载,因为编译器无法区别方法的签名,如下:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PsqLLJBX-1579504969680)(https://i.loli.net/2019/04/26/5cc2c468ad392.png)]

  • 如果想要重载该方法可以让它的返回值不一样,即让它们字节码层面的签名不一样,这样在Hotspot的javac会破例让他通过编译校验。如下
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zGedTlPe-1579504969680)(https://i.loli.net/2019/04/26/5cc2c467d6f7c.png)]

  • 拆箱和装箱是在javac中使用最多的语法糖。

  • 语法糖不会改变字节码规范,所以了解语法糖背后逻辑的最好方法就是反编译字节码。

  • 注解处理器在JVM编译代码时会触发,用于修改源代码中的语法树的数据(词义分析过后就是语义分析,紧接着生产语法树),修改后的语法树就会被生产字节码了。

  • 注解处理器,是以注解作为过滤条件,然后以visit设计模式访问被注解的类或者方法的语法树。

  • java为什么不需要编译预处理呢?因为java编译的时候会将所有编译单元(语法树)的根节点加入到符号表中,并在并在符号表间建立好了关联关系。

解析器与JIT编译器

  • 当编译器优化效果不好时,会采用逆优化重新让代码回归到解释器去执行(或者是编译程度较弱的C1编译器),此时解释器可以称为是编译器的逃生门。

  • Xint可以强制使用解释器独立工作。

  • Xcomp 可以建议JVM尽可能的使用编译器工作,不得已才用解释器。

  • C1编译器比C2编译器的优化程度较低,如非长时间运行建议采用C1(-client开启),如果长时间运行则采用C2(-server开启),采用C2长时间运行效率较高。

  • XX:+TieredCompilation分层策略会根据编译的规模和耗时将代码分层。不同的层次采用不同的策略,有些代码被分派多个层次的会被编译多次,不同层次采用工具不同(有时只采用解析器,有时候采用C1,有时候采用C2)。在分层策略中C1和C2两种模式混合进行配合。而profiling的性能监控程序会辅助分析来分层。这个功能在1.7版本默认开启。

  • 触发编译的条件有两个:方法栈帧频繁调用,方法内的循环体被多次执行。而编译的对象只有一个:“方法栈帧”。编译过后,栈帧会被替换,这个称为**“OSR编译”**

  • 检测触发条件的热点探测技术有3种主流方式:1.基于内部预设阈值的计数器,2.基于采样统计,3.基于“踪迹Trace”.

  • Hotspot采用基于计数器的热点探测技术。它的方法计数器的阈值通过-XX:compileThreshold设定,默认C1是1500次,C2是1万次。(而回边计数器的阈值可以通过-XX:OnStackReplacePercentage间接设置)

  • 在Hotspot中热点探测的计数器有:方法计数器和回边计数器,这类计数器是某一段时间的统计,过了段时间(半衰期)后,这个统计值会衰变一半。

  • 半衰期可以被启动参数控制(半衰期的时间,以及是否开启半衰期)

下图为方法的内存布局:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vtjkmzdR-1579504969681)(https://i.loli.net/2019/04/26/5cc2c468b5b80.png)]

  • javac去除了-O参数后基本没有多少代码优化技术了,所有的优化技术都用在JIT即时编译器上。也就是说代码由解释器解释后的本地代码远远无法与JIT即时编译器优化后的本地代码相提并论。
  • 空循环会被JIT即时编译器去掉。
  • 函数内联发生在生成高级中间代码的时候发生。inline技术可以减少方法调用成本,还可以为后续的代码优化提供了场景。
  • 被内联的方法如果是动态方法(比如是invokevirtual方法)则需要采用“类继承关系分析技术”去计算出类的上下文,从而实现内联(如果分析出关系不唯一,就使用守护内联,即退化为解析执行。否则就一直使用内联函数直到继承关系不唯一时触发退化)。
  • 方法逃逸(将局部创建的对象传入其他方法的作用域)和线程逃逸(通过全局引用传递局部创建的对象)都是造成堆对象共享的方式。
  • 逃逸分析技术很耗性能,如果逃逸对象少的话,这样的消耗性价比不高。所以Hotspot等虚拟机默认不开启。
  • 对象的普通方法默认就是virtual类型的(通过invokevirtual访问),这个会影响函数内联的覆盖率。
  • java栈上无法分配对象。(最多只能把聚合量替换为标量)

如何与C艹互杠,成为一名合格的JAVA杠精

JAVA相对C++的不足,以及换来的优势:

较C艹不足换取的优势
即时编译器运行占有用户运行时间JAVA可以实现跨平台,并且java代码马上编写就可以马上运行,而且即时编译可以在运行时对运行性能进行实时监控,来做到更加激进的功能优化。(如果过于激进会触发退化,比如守护内联函数)
java是动态的类型安全语言,会占用运行时来进行安全检测(比如数组访问边界检测)使得用户不需要过多担心代码安全问题
java默认方法是虚方法,影响函数内联提高类的动态加载和动态扩展能力
java无法在栈上分配对象内存分配机制简单,垃圾回收也简单高效。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值