> 文章列表 > 能够提高Java应用性能的编码建议

能够提高Java应用性能的编码建议

能够提高Java应用性能的编码建议

前言

如果对GC(垃圾收集)有一定了解的读者可跳过该部分直接阅读正文。
现代垃圾收集器都有STW(stop the world:进行GC时整个应用程序都会暂停,就像是整个世界都暂停了一样)这个困扰,因为程序在运行时对象的引用关系是在不断的发生变换的,所以需要暂停用户线程才能更安全的进行GC操作。也就是说GC操作会降低应用程序的性能
既然GC会降低性能,那么减少GC操作就能提升性能(这个前提是同一款垃圾收集器,STW相同的情况下。CMS触发GC的次数可能会比G1的少,但是可能CMS性能不比G1的好),最直观的办法就是减少应用程序中的垃圾,垃圾减少了GC(垃圾回收)自然也会减少。

正文

1、资源共享的情况下,尽可能的使用单例模式

最有力的论证就是Spring的Bean默认情况下是单例,因为大部分的Bean都是无状态(只做处理,不存储信息)的,因此这些Bean基本都能够共享。Spring也确实是这么做的,他把作用域为单例的Bean都做了一个缓存,避免创建大量的垃圾。
Q:那么有状态的Bean能否使用单例模式?
A:这就要根据具体的使用场景自行衡量利弊。比如在Service中需要有那么几个属性存储一些信息,这种情况下仍然建议使用单例模式,对于存储信息的属性可以使用ThreadLocal或者synchronized保证其线程安全。对于类似于实体类这样的Java Bean,他们存在的目的可能就是为了存储信息,这种情况下就没必要使用单例,一般也不会有开发者把Java Bean注册为Spring Bean。(这里补充一个知识点,Spring Bean是被Spring管理的对象,而Java Bean是一种规范,他们两个没有什么关联关系,别搞混了)

2、存在多个相同对象时,考虑使用享元模式

最有力的论证就是大家常用的装箱基本类型Integer,Integer缓存了-128到127的Integer实例,如果通过Integer.valueOf()获取一个在此范围内的Integer实例,那么你会发现无论获取多少次他们都是一样的,甚至可以用==来判断他们是否相等。当然new Integer()和Integer.valueOf()获取的实例还是只能用equals()判断是否相等,因为new代表的是创建一个新对象,与Integer的缓存无关。所以在获取对象实例时能用静态方法就不要用new,这一点在《Effective Java》中也提到了第1条:用静态工厂方法代替构造器,静态方法的好处并不只是为了使用缓存,具体内容可自行阅读原书籍。

3、对于全是静态方法的工具类,把构造器进行私有化

关于这一点我没有去找一些工具类举例,但是不难论证,如果一个类全是静态方法,那么这个类也没有实例化的必要,因为将这个类实例化将毫无用处。所以建议把构造器私有化,以防止客户端对这个类进行不必要的实例化操作。

4、基本类型优先于装箱基本类型

虽然说Java的自动拆装箱开发者提供了便利,但是这容易让开发者在编写代码时没有意识到程序会进行自动拆装箱。无意识拆装箱浪费一些内存倒还算是小事,还很容易发生逻辑错误(比如说自动装箱后用==判断是否相等),更要命的是这种错误无法被编译器发现。本篇文章内容只讨论内存方面的问题,只是顺带提一嘴,就不展开讲了。

	Runtime runtime = Runtime.getRuntime();System.out.println("第一段代码执行前的空余内存:"+runtime.freeMemory());//第一段代码int sum = 1000;for (int i = 0; i < 100000; i++) {sum += i;}Integer sumInteger = Integer.valueOf(sum);System.out.println("第一段代码执行后的空余内存:"+runtime.freeMemory());//第二段代码Integer sum2 = 1000;for (int i = 0; i < 100000; i++) {sum2 += i;}System.out.println("第二段代码执行后的空余内存:"+runtime.freeMemory());运行结果:
第一段代码执行前的空余内存:249999128
第一段代码执行后的空余内存:249999128
第二段代码执行后的空余内存:248667424

可以看到两段功能一样的代码,使用的内存大小却是天壤之别。原因就是第一段代码是用基本类型进行计算,计算完成后再转成Integer,也就是这一段代码只创建了一个Integer对象。而第二段代码因为sum2是包装类型,并且直接在循环中参与了运算,每次运算都会进行自动拆装箱,产生新的Integer,10万次循环也就是产生了10万个Integer实例。所以能使用基本类型的时候就不要用装箱基本类型。

5、在使用容器类时,指定初始化大小以减少容器扩容操作

如果读者了解容器自动扩容的原理,那就应该知道,容器在扩容时不断的复制原容器的内容后丢弃原容器的内容,也就是说容器在不断的扩容就会不断的产生垃圾。

	Runtime runtime = Runtime.getRuntime();System.out.println("第一段代码执行前的空余内存:"+runtime.freeMemory());//第一段代码int sum = 1000;for (int i = 0; i < 100000; i++) {sum += i;}Integer sumInteger = Integer.valueOf(sum);System.out.println("第一段代码执行后的空余内存:"+runtime.freeMemory());//第二段代码Integer sum2 = 1000;for (int i = 0; i < 100000; i++) {sum2 += i;}System.out.println("第二段代码执行后的空余内存:"+runtime.freeMemory());运行结果:
第一段代码执行前的空余内存:244139
第一段代码执行后的空余内存:243259
第二段代码执行后的空余内存:242869

从运行结果可以发现第一段代码正是因为在不断的扩容,占用的内存比第二段代码多了约500k,所以如果能提前知道需要多大的容器,在创建容器时就指定容器初始化大小,哪怕是估算的也比容器自动大量扩容要好。

总结

核心思想就是减少在堆中创建垃圾(应用程序在运行过程中创建垃圾是不可避免的事)。但是我为什么不说减少类的实例化操作呢?因为现在的虚拟机不一定只能够在堆中分配内存,有可能类的实例化分配的内存是在栈上。具体可自行学习栈上分配、逃逸分析等相关内容,在我的上篇博客《Java开发的一些编码建议》结语中也有些许介绍。