package com.cheng.improve151suggest;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;
import java.util.Arrays;
import com.cheng.highqualitycodestudy.R;
import com.cheng.improve151suggest.adapter.I151SuggestListAdapter;
/**
第10章 性能和效率
建议132: 提升java性能的基本方法
建议133: 若非必要,不要克隆对象
建议134: 推荐使用“望闻问切”的方式诊断性能
建议135: 必须定义性能衡量标准
建议136: 枪打出头鸟—解决首要系统性能问题
建议137: 调整jvm参数以提升性能
建议138: 性能是个大“咕咚”
*/
public class I151SChapter10Activity extends AppCompatActivity {
private static final String TAG = "I151SChapter10Activity";
private ListView mChapterLV;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_i151suggestchapter);
initView();
initData();
}
private void initView() {
this.mChapterLV = (ListView) findViewById(R.id.isi_chapter_lv);
}
private void initData() {
String[] suggests = getResources().getStringArray(R.array.i151schapter10);
I151SuggestListAdapter adapter = new I151SuggestListAdapter(this, Arrays.asList(suggests));
mChapterLV.setAdapter(adapter);
}
/**
* 建议132: 提升java性能的基本方法
*/
private void suggest132() {
/*
1)不要在循环条件中计算
如果在循环(如for循环、while循环)条件中计算,则每循环一遍就要计算一次,这会降低系统效率
2)尽可能把变量、方法声明为final static类型
假设要将阿拉伯数字转换为中文数字,其定义如下:
public String toChineseNum(int num) {
// 中文数字
String[] cns = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"};
return cns[num];
}
每次调用该方法时都会重新生成一个cns数组,注意该数组不会改变,属于不变数组,在这种情况下,把它声
明为类变量,并且加上final static修饰会更合适,在类加载后就生成了该数组,每次方法调用则不再重新
生成数组对象了,这有助于提高系统性能,代码如下:
// 声明为类变量
final static String[] cns = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"};
public String toChineseNum(int num) {
return cns[num];
}
3)缩小变量的作用范围
关于变量,能定义在方法内的就定义在方法内,能定义在一个循环体内的就定义在循环体内,能放置在一个
try...catch块内的就放置在该块内,其目的是加快GC的回收
4)频繁字符串操作使用StringBuilder或StringBuffer
虽然String的联接操作(“+”号)已经做了很多优化,但在大量的追加操作上StringBuilder或StringBuffer
还是比“+”号的性能好很多
5)使用非线性检索
如果在ArrayList中存储了大量的数据,使用indexOf查找元素会比java.utils.Collections.binarySearch
的效率低很多,原因是binarySearch是二分搜索法,而indexOf使用的是逐个元素对比的方法。这里要注意:
使用binarySearch搜索时,元素必须进行排序,否则准确性就不可靠了
6)覆写Exception的fillInStackTrace方法
fillInStackTrace方法是用来记录异常时的栈信息的,这是非常耗时的动作,如果在开发时不需要关注栈信
息,则可以覆盖之,如下覆盖fillInStackTrace的自定义异常会使性能提升10倍以上:
class MyException extends Exception {
public Throwable fillInStackTrace() {
return this;
}
}
7)不要建立冗余对象
不需要建立的对象就不能建立,说起来很容易,要完全遵循此规则难度就很大了,我们经常就会无意地创建冗
余对象,例如这样一段代码:
public void doSomething() {
// 异常信息
String exceptionMsg = "我出现异常了,快来救救我";
try {
Thread.sleep(10);
} catch (Exception e) {
// 转换为自定义运行期异常
throw new MyException(e, exceptionMsg);
}
}
注意看变量exceptionMsg,这个字符串变量在什么时候会被用到?只有在抛出异常时它才有用武之地,那它
是什么时候创建的呢?只要该方法被调用就创建,不管会不会抛出异常。我们知道异常不是我们的主逻辑,不
是我们代码必须或经常要到达的区域,那位了这个不经常出现的场景就每次都多定义一个字符串变量,合适吗?
而且还要占用更多的内存!所以,在catch块中定义exceptionMsg才是正道:需要的时候才创建对象
我们知道运行一段程序需要三种资源:CPU、内存、I/O,提升CPU的处理速度可以加快代码的执行速度,直接
变现就是返回时间缩短了,效率提交了;内存是Java程序必须考虑的问题,在32位的机器上,一个JVM最多只
能使用2GB的内存,而且程序占用的内存越大,寻址效率也就越低,这也是影响效率的一个因素。I/O是程序展
示和存储数据的主要通道,如果它很缓慢就会影响正常的显式效果。所以我们在编程时需要从这三个方面入手
*/
/**
* 注意
* Java的基本优化方法非常多,但是随着Java的不断升级,很多看似很正确的优化策略就逐渐过时了(或者
* 说已经失效了),这一点还需要注意。最基本的优化方法就是自我验证,找出最佳的优化途径,提高系统性
* 能,不可盲目信任
*/
}
/**
* 建议133: 若非必要,不要克隆对象
*/
private void suggest133() {
/*
通过clone方法生成一个对象时,就会不再执行构造函数了,只是在内存中进行数据块的拷贝,此方法看上去
似乎比new方法的性能好很多,但是Java的缔造者们也认识到“二八原则”,80%(甚至更多)的对象是通过new
关键字创建出来的,所以对new在生成对象(分配内存、初始化)时做了充分的性能优化,事实上,一般情况
下new生成的对象比clone生成的性能方面要好很多,如下示例代码:
private static class Apple implements Cloneable {
public Object clone() { // 注:这个实验去掉clone方法的try...catch应该要公平点
try {
return super.clone();
} catch (CloneNotSupportedException e) {
throw new Error();
}
}
}
public static void main(String[] args) {
// 循环10万次
final int maxLoops = 10 * 10000;
int loops = 0;
// 开始时间
long start = System.nanoTime();
// “母”对象
Apple apple = new Apple();
while (++loops < maxLoops) {
apple.clone;
}
long mid = System.nanoTime();
System.out.println("clone 方法生成对象耗时:" + (mid-start) + " ns");
// new生成对象
while (--loops > 0) {
new Apple();
}
long end = System.nanoTime();
System.out.println("new 生成对象耗时:" + (end-mid) + " ns");
}
运行查看输出结果,发现用new生成对象竟然比clone方法快很多!原因是Apple的构造函数非常简单,而且
JVM对new做了大量的性能优化,而clone方式只是一个冷僻的生成对象方式,并不是主流,它主要用于构造
函数比较复杂,对象属性比较多,通过new关键字创建一个对象比较耗时的时候
*/
/**
* 注意
* 克隆对象并不比直接生成对象效率高
*/
}
/**
* 建议134: 推荐使用“望闻问切”的方式诊断性能
*/
private void suggest134() {
/**
* 注意
* 性能诊断遵循“望闻问切”,不可过度急躁
*/
}
/**
* 建议135: 必须定义性能衡量标准
*/
private void suggest135() {
/*
制定性能衡量标准的原因有两个:
1)性能衡量标准是技术与业务之间的契约
2)性能衡量标准是技术优化的目标
性能优化是无底线的,性能优化得越厉害带来的副作用也就越明显,例如代码的可读性差,可扩展性降低等
*/
/**
* 注意
* 一个好的性能衡量标准应该包括以下KPI(Key Performance Indicators)
* -核心业务的响应时间
* -重要业务的响应时间
*/
}
/**
* 建议136: 枪打出头鸟—解决首要系统性能问题
*/
private void suggest136() {
/*
在一个系统出现性能问题的时候,很少会出现只有一个功能有性能问题,系统一旦出现性能问题,也就意味
着一批的功能都出现了问题,在这种情况下,我们要做的就是统计出业务人员认为重要而且缓慢的所有功能,
然后按照重要优先级和响应时间进行排序,并找出前三名,而这就是我们要找的“准出头鸟”
“准出头鸟”找到了,然后再对这三个功能进行综合分析,运用“望闻问切”策略,找到问题的可能根源,然后
只修正第一个功能的性能缺陷,再来测试检查是否解决了这个问题,紧接着是第二个、第三个,循环之。可
能有疑问:为什么这里只修正第一个缺陷,而不是三个一起全部修正?这是因为第一个性能缺陷才是我们真
正的出头鸟,经验表明性能优化项目中超过80%的只要修正了第一个缺陷,其他的性能问题就会自行解决或
非常容易解决,已经不成为问题了
*/
/**
* 注意
* 解决性能优化要“单线程”小步前进,避免关注点过多导致精力分散
*/
}
/**
* 建议137: 调整jvm参数以提升性能
*/
private void suggest137() {
/*
下面提供了四个常用的JVM优化手段,提供参考:
1)调整堆内存大小
在JVM中有两种内存:栈内存(Stack)和堆内存(Heap),栈内存的特点是空间比较小,速度快,用来
存放对象的引用及程序中的基本类型;而堆内存的特点是控件比较大,速度慢,一般对象都会在这里生成、
使用和消亡
栈空间是由线程开辟,线程结束,栈空间由JVM回收,因此它的大小一般不会对性能有太大的影响,但是
它会影响系统的稳定性,在超过栈内存的容量时,系统会报StackOverflowError错误。可以通过“java
-Xss <size>”设置内存大小来解决此类问题
堆内存的调整不能太随意,调整得太小,会导致Full GC频繁执行,轻则导致系统性能急速下降,重则导
致系统根本无法使用:调整得太大,一则是浪费资源(当然,若设置了最小堆内存则可以避免此问题),
二则是产生系统不稳定的情况,例如在32位的机器上设置超过1.8GB的内存就有可能产生莫名其妙的错误。
设置初始化堆内存为1GB(也就是最小堆内存),最大堆内存为1.5GB可以用如下的参数:
java -Xmx1536 -Xms1024m
2)调整堆内存中各分区的比例
JVM的堆内存包括三部分:新生区(Young Generation Space)、养老取(Tenure Generation Space)、
永久存储区(Permanent Space),其中新生成的对象都在新生区,它又分为伊甸区(Eden Space)、
幸存0区(Survivor 0 Space)和幸存1区(Survivor 1 Space),当程序中使用了new关键字时,首先
在伊甸区生成该对象,如果伊甸区满了,则用垃圾回收器进行回收,然后把剩余的对象移动到幸存区(0区
或1区),可如果幸存区也满了呢?垃圾回收器先进行回收,然后把剩余的对象移动到养老区,那要是养老
区也满了呢?此时就会触发Full GC(这是一个非常危险的动作,JVM会停止所有的执行,所有系统资源都
会让位给垃圾回收器),会对所有的对象过滤一遍,检查是否有可以回收的对象,如果还是没有的话,就
抛出OutOfMemoryError错误,系统不干了
清楚了这个原理,那就可以思考一下如何提升性能了:若扩大新生区,势必会减少养老区,这就可能产生不
稳定的情况,一般情况下,新生区和养老区的比例为1:3左右,设置命令如下:
java -XX:NewSize=32 -XX:MaxNewSize=640m -XX:MaxPermSize=1280m -XX:newRatio=5
该配置指定新生代初始化为32MB(也就是新生区最小内存为32M),最大不超过640MB,养老区最大不超过
1280MB,新生区和养老区的比例为1:5
3)变更GC的垃圾回收策略
Java程序性能的最大障碍就是垃圾回收,我们不知道它何时会发生,也不知道它会执行多长时间,但是我们
可以想办法改变它对系统的影响,比如启用并行垃圾回收、规定并行回收的线程数量等,命令格式如下:
java -XX:+UserParallelGC -XX:ParallelGCThreads=20
这里启用了并行垃圾回收机制,并且定义了20个收集线程(默认的收集线程等于CPU的数量),这对多CPU的
系统是非常有帮助的,可以大大减少垃圾回收对系统的影响,提高系统性能
当然,垃圾回收的策略还有很多属性可以修改,比如UseSerialGC(启用串行GC,默认值)、
ScavengeBeforeFullGC(新生代GC优先于Full GC执行)、UserConcMarkSweepGC(对老生代采用并发标
记交换算法进行GC)等,这些参数需要在系统中逐步调试
4)更换JVM
*/
/**
* 注意
* 如果程序已经优化到了极致,但还是觉得性能比较低,那JVM的优化就要提到日程上来了。不过,由于
* JVM是系统运行的容器,所以稳定性也是必须考虑的,过度的优化可能就会导致系统故障频繁发生,导
* 致系统质量大幅下降
*/
}
/**
* 建议138: 性能是个大“咕咚”
*/
private void suggest138() {
/*
可以从四个方面分析Java系统的性能问题:
1)没有慢的系统,只有不满足业务的系统
如果有使用者告诉你,”这个系统太慢了“,也就是在间接地提醒您:系统没有满足业务需求,尚需努力
2)没有慢的系统,只有架构不良的系统
在做系统架构设计时,架构师有没有考虑并行计算?有没有考虑云计算技术?有没有负载均衡?...这些都
是解决我们性能问题的良方,只要架构设计得当,效率就不是问题
3)没有慢的系统,只有懒惰的技术人员
这里的技术人员涉及面很大,可以是开发人员,也可以是维护人员,甚至是应用软件的顾问人员等
4)没有慢的系统,只有不愿意投入的系统
这里的投入指的是资源,包括软硬件资源、人员资源及资金资源等,这不是项目组能够单独解决的问题,但
是它会严重影响系统的性能
*/
/**
* 注意
* 对现代化的系统建设来说,性能就是一个大”咕咚“--看清它的本质吧
*/
}
}