从Java虚拟机角度审视针对移动设备的代码优化

这篇文章是很早以前针对J2ME代码的优化而撰写的,那个时候很多设备的虚拟机性能烂,内存少,对JAR包的体积还有要求(比如老S40),所以极限化优化是一种需求。

当下的智能手机甚至功能手机都和几年前已不可同日而语,下面的很多技巧仅供参考吧,至少能让大家对java编译器和虚拟器的一些机制有些了解。

以下的技巧根据不同需求可归纳如下:

1. 通过改变编码习惯减少生成的字节码指令数量,这样既能节省内存也能提高运行期性能
2. 使用局部变量代替静态和成员变量,提高性能
3. 改变数组初始化方式,减小编译生成的class文件体积


正文开始:

==============================================
最近因为工作需要,对java虚拟机进行了一些研究,在阅读过了不少编译器产生的字节码之后,对java的细节优化有了一些进一步的认识,现在把这些心得写出来和大家分享。

当然,相对于对程序整个结构或算法上的优化,下面的这些细节的优化方法起到的作用可能是非常小的。不过相对的,在编程中应用它们的成本也是非常低廉的,可能只是编程习惯的些许改变。对于java的桌面开发或者服务器端开发,我想没有必要对它们过于关注。但是对于J2ME开发,很多设备的应用程序存储空间非常紧张,比如nokia旧40系列手机要求jar的大小必须在64K以内,如果想成功登陆百宝箱,那么你还得进一步把jar的大小控制在59K以内。在这种情况下,如果只要稍稍在编程之中注意一下,就能不经意间节省几百甚至几千字节,同时让你的代码比原来快1%,何乐而不为?

虽然现在大家的PC配置已经很高了,不在乎几条指令的,但不可否认,在某些领域,如手机软件,或智能卡软件,对空间耗用或执行性能的要求依然是很高的,这种情况下,我们还是应该秉承能省则省的编码原则,下面一些风格,就是基于这样的原则。有些人认为,空间占用(jar包大小或内存占用)和执行性能本身就存在矛盾,是此消彼长的关系,我认为这是从宏观的角度,而从微观的角度来说,更多的时候时间和空间是一致的,较少的字节码就意味这较小的静态类文件尺寸,也就是较少的类装入时间,而在运行时也就意味着执行较少的虚拟机指令,和较小的内存占用,这所有方面整合起来也就意味着较高的整体性能。

* for循环的优化
未优化代码:
for (int i = 0; i < aCollection.size(); i++) ...
如果aCollection的长度在循环体内不会发生变化,那么显然应该如下优化:
for (int i = 0, n = aCollection.size(); i < n; i++) ...
如果是从0开始循环且对执行的顺序无所谓,那么倒序循环将更高效:
for (int i = n; --i >= 0;) ...
这是因为和常量n比较则需要先把n入栈再比较,而和0比较则有专门的指令


* 局部变量 vs 成员域 (field)
如果在函数体中要反复读取一个成员域,应把此域赋值给一个局部变量,然后以该局部变量代之。
因为每个成员域的取值实际上都是两个字节码,即要先将this入栈,再使用getfield指令获取域的值,而局部变量则仅需要一个指令


* 连续赋值
未优化代码:
int a = 0, b = 0, c = 0;
可以用连续赋值来节省几个常量入栈指令:
int a, b, c;
a = b = c = 0;


* 延迟初始化(lazy initializing)
int[] a = getA();
if (a != null && a.length > 0) {
    return a[0];
}
可以写成:
int[] a; //局部变量的声明是不会产生字节码的
if ((a = getA()) != null && a.length > 0) { // 这里减少了一个局部变量重新入栈的指令
    return a[0];
}


* 对复杂成员变量或静态变量的赋值或操作
未优化代码,成员变量,虽然this可以在编码时省略不写,但在生成的字节码中,this引用依然是要入栈的:
    void Foo() {
        this.list = new ArrayList();
        this.list.add("1");
        this.list.add("2");
    }
或静态变量:
    static {
        ThisClass.list = new ArrayList();
        ThisClass.list.add("1");
        ThisClass.list.add("2");
    }
使用一个临时局部变量,因为无论是字节码的数量还是执行速度,访问局部变量都比成员变量或静态变量更优化:
    void Foo() {
        List tmp;
        this.list = tmp = new ArrayList();
        tmp.add("1");
        tmp.add("2");
    }


* 数组初始化
java的编译器对数组初始化的实现是比较笨拙的,比如下面的代码:
int[] ARRAY = { 1, 2, 3, 4, 5 };
实际上等价于:
int[] ARRAY = new int[5];
ARRAY[0] = 1;
ARRAY[1] = 2;
ARRAY[2] = 3;
ARRAY[3] = 4;
ARRAY[4] = 5;
如果要初始化的数组较小,如几个元素,那么没有必要进行优化,但如果是初始化一个很大的常量数组,那么编译生成的字节码将包含大量重复的数组赋值指令,这种情况下就有必要对其进行优化。
对数组赋值的优化可根据实际情况处理,比如下面提供了一种较为通用的方式,即解析字符串,这个例子是典型的时间换空间,但因为数组初始化仅发生在类载入时且仅运行一次,因此大部分情况下是划算的。
其中split()方法可以重复使用于所有需要切分字符串数组的地方。
    public static final List split(String src, char delim) {
        List ret = new ArrayList();
        int i1, i2;
        i1 = i2 = 0;
        for (;;) {
            i2 = src.indexOf(delim, i1);
            if (i2 < 0) break; // reaches end
            ret.add(src.substring(i1, i2));
            i1 = i2 + 1;
        }
        if (i1 < src.length()) {
            ret.add(src.substring(i1));
        }
        return ret;
    }


    final static String ARRAY_STR = "1,2,3,4,5,";
    final static int[] ARRAY;
    static {
        List l = split(ARRAY_STR, ',');
        int[] tmp;
        ARRAY = tmp = new int[l.size()];
        for (int i = tmp.length; --i >= 0; tmp[i] = Integer.parseInt(((String) l.get(i)).trim()));
    }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值