长期更新,不喜勿喷......
思路
- 减少大对象数量,不得产生的时候,尽量要大对象朝生夕死,一次GC就被干掉。对于JVM来说大对象是致命的,大对象很容易让老年代爆了,以至于不得不 Full GC,试想你这个大对象要是太频繁长剑,Full GC 的次数就控制不住了
字符串优化手段
字符串基础部分请看:
字符串优化手段也没几个,注意下就行了:
- 能写"a"+"b"+"c"的尽量用这种字面量直接写
- 能用StringBuilder做拼接尽量用StringBuilder做拼接,性能对象看下文
- 能写String.intern()的尽量写
字符串拼接性能对比
String name = "a"+"b"+"c" 这样写当然最优的啦,这样编译期就直接优化成abc了,但是我们没法做高频测试啊,下面主要测试这2种字符串拼接手段
// 1
String name1 = "AA";
String name2 = name1 + "bb";
// 2
StringBuilder build = new StringBuilder();
String name = build.append("AA").toString();
测试 1:
1,0000 次str+"abc--
long startTime = System.currentTimeMillis();
String name = "";
for (int i = 0; i < 10000; i++) {
name = name + "ABC";
}
long endTime = System.currentTimeMillis();
System.out.println("Time:" + (endTime - startTime));
JVM配置:-Xms256m -Xmx256m -XX:+DoEscapeAnalysis -XX:+PrintGCDetailslog:YGC 飞了,耗时也挺长,这才1W次哎,我都没敢写1000W ✧(≖ ◡ ≖✿)
[GC (Allocation Failure) [PSYoungGen: 65536K->800K(76288K)] 65536K->808K(251392K), 0.0014111 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 66336K->736K(76288K)] 66344K->752K(251392K), 0.0021167 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 66272K->681K(76288K)] 66288K->705K(251392K), 0.0046012 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 66217K->767K(76288K)] 66241K->791K(251392K), 0.0017707 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 66303K->702K(76288K)] 66327K->726K(251392K), 0.0006732 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 66193K->739K(86016K)] 66217K->763K(261120K), 0.0007820 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 85731K->138K(86016K)] 85755K->803K(261120K), 0.0010923 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 85101K->72K(86016K)] 85766K->761K(261120K), 0.0004159 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 85049K->118K(85504K)] 85739K->808K(260608K), 0.0043978 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 85050K->169K(85504K)] 85739K->859K(260608K), 0.0004352 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 84137K->129K(85504K)] 84827K->818K(260608K), 0.0067090 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 84080K->185K(85504K)] 84770K->874K(260608K), 0.0004318 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 84153K->138K(85504K)] 84842K->828K(260608K), 0.0004235 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 84022K->143K(86016K)] 84712K->832K(261120K), 0.0003806 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 84623K->89K(85504K)] 85312K->779K(260608K), 0.0011092 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Time:251
内存快照:字符数组爆了,占35M内存,这显然是字符串贡献的,Stringbuilder对象4766个,String19745个,这数量就很多了,还是在N多次GC之后
测试 2:
1,0000 次StringBuiler.append()
long startTime = System.currentTimeMillis();
String name = "";
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10000; i++) {
builder.append("abc");
}
name = builder.toString();
long endTime = System.currentTimeMillis();
System.out.println("Time:" + (endTime - startTime));
log:没有GC,耗时只有1毫秒,真是不对比不知道差距呀 ( *︾▽︾)
Time:1
内存快照:String对象和StringBuilder对象差不多,但是没有引发GC就知道差距有多大了,最明显的字符数组小多了,只有4M大小,这就是差距
总结:
看过[JVM 面试题【中级】]中String部分的都知道,字符串拼接中只有有一个是引用对象,那会就会最终生成一个String对象,在这个过程中,JVM 实际上会创建出一个StringBuilder对象来处理字符串拼接,这就是测试1 GC次数多的根本原因,一次字符串拼接中间会创建好几个关联对象对来,多次执行这么样的操作,谁也受不了
而我们直接使用StringBuilder的话,不管中间经历多少次哦拼接,StringBuilder 内部只会根据字符的增加对字符数组做动态扩容,不会生成大量多余的中间对象而引起频繁GC
但是StringBuilder还是有优化手段的,拼接还是会导致字符数组动态扩容,动态扩容就会新创建字符数组,char[] 的性能这块还有可以优化点的
如果我们知道这次拼接最大的字符数,我们可以使用StringBuilder代长度的构造器,这样就可以避免数组动态扩容带来的性能损失了
意义:
肯定有人会拿1W次说事,说我代码怎么可能会写这么多次,也就是偶尔用一次罢了,那这种优化意义何在 ヾ(@⌒ー⌒@)ノ
其实不然,大家也许没注意过,谁的代码里面没有循环、遍历、基于事件响应的频繁调用、各种按钮事件、UI重绘、递归,这些可都是会执行很多次的,加一起也许比我测试用例的1W次都多的多
你要是在这些地方里,代码写的不注意,那性能肯定有问题,别的不说,你就不要抱怨为啥GC这么多
养成良好的代码习惯,尤其在这次会频繁执行的地方要格外仔细,细细斟酌,实际上是对提高系统性能有非常大的意义
String.intern()
intern 方法会把String对象中的字符串同步到字符串常量池中去,并返回该字符串在常量池中的地址。若此时字符串常量池中没有该字符串对象,那么就把调用 intern 方法的字符串对象的地址写到字符串常量池中
看这段代码
String name1 = new String("aa");
String name2 = new String("aa");
String name3 = new String("aa");
String name4 = new String("aa");
String name5 = new String("aa");
String name6 = new String("aa");
对应这6个对象来说,虽然内容一样,但是这6个对象都是强引用,都在堆内存中占用内存资源
String name1 = new String("aa").intern();
String name2 = new String("aa").intern();
String name3 = new String("aa").intern();
String name4 = new String("aa").intern();
String name5 = new String("aa").intern();
String name6 = new String("aa").intern();
对应这6个对象来说,经过intern()之后,统一使用常量池中字符串对象地址,这样堆内存可以节约5个对象空间,对于的对象在GC时就会处理掉了
intern() 对于那些有大量字符串的场景非常有意义,比如社交网站,北京市、海淀区这样的,明显可以降低内存压力,虽然会有GC,但是也比内存爆炸强的多
缓存命中带来的性能问题
这个问题就是缓存行衍生出来的问题,在并发量巨大、数据数量打的后端程序中问题会比较严重,一定要注意缓存命中问题,尤其是后端同学真的要对于这个及其敏感,写好了速度很快,用差了会有性能问题的
例子1:
首先,假设我们有一个 64M 长的数组,设想一下下面的两个循环:
const int LEN = 64*1024*1024;
int *arr = new int[LEN];
for (int i = 0; i < LEN; i += 2) arr[i] *= i;
for (int i = 0; i < LEN; i += 8) arr[i] *= i;
一般来说,按照预计,第二个循环要比第一个循环少 4 倍的计算量,速度也应该快 4 倍的。但实际跑下来并不是,在我的机器上
第一个循环127ms第二个循环121
速度居然一样,原因就是缓存命中了。CPU 会以一个 Cache Line 64Bytes 最小时单位加载,也就是 16 个 32bits 的整型,所以,无论你步长是 2 还是8,都差不多,你计算的数据前后都在一个缓存行中被CPU加载进来,而后面的乘法其实是不怎么耗 CPU 时间的,耗时的加载内存操作耗时都是一样的
例子2:
我们以一定的步长increment 来访问一个连续的数组
for (int i = 0; i < 10000000; i++) {
for (int j = 0; j < size; j += increment) {
memory[j] += j;
}
}
从 [1024] 以后,耗时显注上升。我机器的 L1 Cache 是 32KB, 8 Way 的,前面说过,8 Way 的一个组有 64 个 Cache Line,也就是 4096 个字节,而 1024 个整型正好是 4096 Bytes。所以,一旦过了 8 Way + 4096 Bytes 这个界,每个步长都无法命中 L1 Cache,每次都是 Cache Miss,所以,导致访问时间一下子就上升了
这个操作的有的CPU缓存分析会直接会缓存加载满的,但是不一定,要看你机器的CPU
例子3:
我们对一个二维数组的两种遍历方式,一个逐行遍历,一个是逐列遍历,这两种方式在理论上来说,寻址和计算量都是一样的,执行时间应该也是一样的
const int row = 1024;
const int col = 512;
int matrix[row][col];
//逐行遍历
int sum_row=0;
for(int r=0; r<row; r++) {
for(int c=0; c<col; c++){
sum_row += matrix[r];
}
}
//逐列遍历
int sum_col=0;
for(int c=0; c<col; c++) {
for(int r=0; r<row; r++){
sum_col += matrix[r];
}
}
逐行遍历:0.081ms逐列遍历:1.069ms
十几倍的差距,究其原因,就是逐列遍历对于 CPU Cache 的运作方式并不友好,每次都缓存miss,每次都得从内存加载数据,不像逐行,利用缓存命中,可以几次操作之后才从内存加载一次数据
例子4:
多线程环境下缓存行问题依然严重,不考虑加锁,2个线程同时对数据读写数据
void fn (int* data) {
for(int i = 0; i < 10*1024*1024; ++i){
data += rand ();
}
}
int p[32];
int *p1 = &p[0];
int *p2 = &p[1];
thread t1(fn, p1);
thread t2(fn, p2);
对于 p[0] 和 p[1] :560ms对于 p[0] 和 p[30]:104ms
这是因为 p[0] 和 p[1] 在同一条 Cache Line 上,而 p[0] 和 p[30] 则不可能在同一条 Cache Line 上 ,CPU 的缓冲最小的更新单位是 Cache Line,所以,这导致虽然两个线程在写不同的数据,但是因为这两个数据在同一条 Cache Line 上,就会导致缓存需要不断进在两个 CPU 的 L1/L2 中进行同步,从而导致了 5 倍的时间差异。
例子5:
多线程环境下要是多个 volatile 数据也会有缓存行问题,2个线程分别操作处于同一个缓存行的 volatile 数据,你会发现6个线程并发都跑不过一个线程,就是因为 volatile 要频繁往内存中刷新数据,甚至不用多个 volatile,1个 volatile 都会带来性能问题
所以 volatile 真不能乱用,这里就不写代码了