美文网首页
从jvm代码优化角度谈撸代码

从jvm代码优化角度谈撸代码

作者: VIPSHOP_FCS | 来源:发表于2017-10-27 23:26 被阅读0次

1.前言

以编译程序执行本地代码,比解释执行更快,除虚拟机解释执行字节码额外消耗时间的原因之外,另一个很重要原因就是虚拟机设计团队几乎把对代码的所有优化措施都集中在即时编译器中。一般即时编译器生成的本地代码比javac产生的字节码更优秀。

HotSpot虚拟机在即时编译器中采用的优化技术:https://wiki.openjdk.java.net/display/HotSpot/PerformanceTacticIndex

下面谈一下jit编译优化的常见手段。

2 方法内联

编译器最总要的优化是方法内联。遵循面向对象方式编写的代码,通常会有很多get,set方法。

优化前代码:
static class B{
int value;
final int get(){
return value;
}
}

public void foo(){
y = b.get();
z = b.get();
sum = y + z;
}

内联是最重要的优化措施,原因有二。一,降低调用的开销(如建立栈帧等);二,方法内联为其他优化措施建立良好基础,方法内联膨胀后,可在更大范围采用后续优化手段。

内联后代码:
public void foo(){
y = b.value;
z = b.value;
sum = y + z;
}

第二步优化:冗余访问消除
y和z的表达式一样,所以可以不必去访问对象b,替换为z=y。如果把b.value视为表达式,则该优化也可以理解为公共表达式消除。

冗余存储消除后代码:
public void foo(){
y = b.value;
z = y;
sum = y + z;
}

第三步优化:复写传播
z与y的值一样,不必使用额外的变量,用y代替z

复写传播优化后:
public void foo(){
y = b.value;
y = y;
sum = y + y;
}

第四步优化:无用代码消除

public void foo(){
y = b.value;
sum = y + y;
}

经过优化以后,作用一样,但是省略了很多代码(从字节码,机器码指令上的差距更大),执行效率更高。

由上面的例子,我们可以看到,方法内联是很多优化手段的基础。方法是否内联,取决于方法是否够热(jvm根据内部方法判断,如是否调用频繁),以及大小。只有方法够热,并且字节码小于325字节,或者方法小于35字节时才能内联。

由此我们可以和平时撸代码的一个小经验印证起来,多写小方法,把职责完整的逻辑段抽取出来,成为独立的小方法。小方法好处有几个:
一 逻辑简单,职责单一,与设计原则的单一性对应;
二 抽出去小方法,类似的,职责上接近的,可以当成下一步抽象优化的基础;
三 小方法更容易满足内联条件。

3 逃逸分析

逃逸分析与方法内联相似,也是其他优化手段的基础。
逃逸分析的行为就是分心对象动态作用域,当一个对象不会逃逸到方法或者线程之外,也就是其他方法或者线程无法通过任何路径访问到该对象时,则可进行一些高效优化。

栈上分配:堆中分配的对象,无论是验证,整理或者清理都需耗费时间,如果确定对象不会逃逸,则可在栈上分配内存,对象所在的内存就可以随栈帧出栈而销毁。在一般应用中,不会逃逸的局部对象比例很大,栈上分配可减少垃圾回收压力。

同步消除:线程同步耗时很大,不会逃逸的对象,读写不会有竞争,相应的同步措施可以消除。

标量替换:标量是指无法再分解的数据。java虚拟机中的原始数据类型都可以称为标量。如果可以继续分解,则称为聚合量,如对象。如果把一个java对象拆散,根据程序访问的情况,将其实用到的成员变量恢复原始类型,来访问,叫做标量替换。如果逃逸分析证明一个对象不会被外部访问,并且可以拆解,那么程序真正执行的时候将很可能不创建这个对象,改为直接创建该对象的成员变量来代替。进而直接在栈上分配和读写,为后续优化打基础。

栈上存储的数据,很大概率会被虚拟机分配到物理机的高速寄存器中。

可以看到,逃逸分析也是jit优化代码的重要措施,那撸代码时有啥常识可以印证?
答案:广义的迪米特法则。控制对象之间的信息流量,流向,影响。内部实现和外部接口隔离。严格控制对象的作用域,访问权限等,使对象,方法的影响最小,不但优雅,而且更合编译器口味。(全局变量自然不满足逃逸分析)

4 再谈对象作用域

前面都在聊jit,我们回到编译方式执行程序的场景(代码不够热,解释器执行)。看看下面这段代码。

public static void main (String[] args)(){
{
byte[] a = new byte[64 * 1024 * 1024];
}
System.gc();
}

jvm 加上运行参数"-verbose:gc",查看运行日志

[GC 66846K->65888K(125632K), 0.0009397 secs]
[Full GC 65888K->65746K(125632K), 0.0051574 secs]

内存居然没被回收。改一下代码
public static void main (String[] args)(){
{
byte[] a = new byte[64 * 1024 * 1024];
}
int b = 0;
System.gc();
}

再执行一次,回收了。。。
[GC 66401K->65778K(125632K), 0.0035471 secs]
[Full GC 65778K->218K(125632K), 0.0140596 secs]

原因在于局部变量表。局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在第一段代码中,虽然变量离开了作用域,但是在此之后,没有其他局部变量表的读写,GC Roots一部分的局部变量表仍然保存着对它的关联。正常情况下没影响,但如果遇到一些特殊情况(对象占用内存大,此方法的栈帧长时间不能被回收,方法调用次数达不到jit编译条件),则会采用手动set为null的方式(用来代替那句b = 0,把局部变量表清空)。

然而有一个问题,解释执行和jit编译后执行是完全不同的代码,这里用来救命的 set null,在jit编译后,很大可能会当成无用代码消除,jit编译后,第一段代码可以正常释放内存。更优雅的方式应当还是严格控制对象作用域,减少对set null的依赖。

5 总结
写小方法
遵循广义迪米特法则
严格控制对象作用域

6 参考
深入理解java虚拟机
java性能权威指南

相关文章

  • 从jvm代码优化角度谈撸代码

    1.前言 以编译程序执行本地代码,比解释执行更快,除虚拟机解释执行字节码额外消耗时间的原因之外,另一个很重要原因就...

  • jvm原理

    35 个 Java 代码性能优化总结 优化代码可以减小代码的体积,提高代码运行的效率。从 JVM 内存模型谈线程安...

  • 移动端SDK的优化之路2

    极限优化 所谓极限优化,是指从多个角度、维度对sdk进行优化,重点是考虑网络优化以及电量消耗优化。能够做到代码精简...

  • 什么是可维护性的代码

    什么是可维护性的代码 今天我们不聊性能优化,只是从后期维护代码的角度谈谈如何优雅的书写代码 为什么需要些可维护性高...

  • 问题解决者

    做了leader之后,撸的代码少了,心中的焦虑感却上升了。本想等业务稳定了,就可以沉下心来写点代码,优化下代码结构...

  • 怎么提升(需整合)

    代码质量(各种方式优化,扩展性,封装,复用性,低耦合) 拿到需求,不要直接撸代码;(编程思维--架构--底层支持库...

  • 撸代码

    撸代码的时候,会不由自主的减少交流,不要被人打扰,各种通讯中断....要知道,安安静静的写代码其实是件奢侈的事。

  • 有关JIT你需要知道的

    JVM对代码执行的优化可分为运行时(runtime)优化和即时编译器(JIT)优化。运行时优化主要是解释执行和动态...

  • 3,优化程序的性能

    前言 优化你的代码, 有时要在深度思考问题, 编译器的角度看待你的程序的效率 正文 一, 优化你的代码 jle 汇...

  • web前端开发编码规范及性能优化

    代码优化 这个部分仅仅将代码优化本身,不考虑性能,关于代码部分的性能优化在 页面渲染 部分 代码优化 中 HTML...

网友评论

      本文标题:从jvm代码优化角度谈撸代码

      本文链接:https://www.haomeiwen.com/subject/okfdpxtx.html