Java 性能优化系列之1[设计与程序优化]

性能

一般来说,性能通过以下几个方面来表现:

  • 执行速度
  • 内存分配
  • 启动时间
  • 负载承受能力

定量评测的性能指标:

  • 执行时间
  • CPU时间
  • 内存分配
  • 磁盘吞吐量
  • 网络吞吐量
  • 响应时间

调优的层面

  • 设计调优
  • 代码调优
  • JVM调优
  • 数据库调优
  • 操作系统调优

性能调优必须有明确的目标,不要为了调优而调优,如果当前程序并没有明显的性能问题,盲目地进行调整,其风险可能远远大于收益。

 

设计优化

1. 单例模式

对于系统的关键组件和被频繁使用的对象,使用单例模式可以有效地改善系统的性能

 

2. 代理模式

代理模式可以用来实现延迟加载,从而提升系统的性能和反应速度。

 

另外,可以考虑使用动态代理的方式 。 动态代理的方法有: JDK自带的动态代理, CGLIB, Javassist, 或ASM库。

 

3. 享元模式

好处:

1) 可以节省重复创建对象的开销

2) 对系统内存的需求减少

 

4. 装饰者模式

实现性能组件与功能组件的完美分离

 

5. 观察者模式

观察者模式可以用于事件监听、通知发布等场合。可以确保观察者在不使用轮询监控的情况下,及时收到相关的消息和事件。

 

6. Value Object 模式

将一个对象的各个属性进行封装,将封装后的对象在网络中传递,从而使系统拥有更好的交互模型,并且减少网络通信数据,从而提高系统性能。

 

7. 业务代理模式

将一组由远程方法调用构成的业务流程,封装在一个位于展示层的代理类中。

 

思考:

单例模式, 工厂模式和享元模式的差异?

 

 

 

常用优化组件和方法

1.缓冲

I/O 操作很容易成为性能瓶颈,所以,尽可能在 I/O 读写中加入缓冲组件,以提高系统的性能。

2. 缓存

缓存可以保存一些来之不易的数据或者计算结果,当需要再次使用这些数据时,可以从缓存中低成本地获取,而不需要再占用宝贵的系统资源。

Java缓存框架:

EHCache, OSCache,JBossCache

3. 对象复用 -- "池"

最熟悉的线程池和数据库连接池。

目前应用较为广泛的数据库连接池组件有C3P0 和Proxool.

4.并行替代串行

5. 负载均衡

跨JVM虚拟机,专门用于分布式缓存的框架--Terracotta, 使用Terracotta可以实现Tomcat的Session共享。

6. 时间换空间

7. 空间换时间

 

程序优化

1. 字符串优化处理

1)

 

[java]  view plain copy
 
  1. String str1 ="abc";  
  2. String str2 ="abc";  
  3. String str3 = new String("abc");  
  4.   
  5. System.out.println(str1==str2);  //true  
  6. System.out.println(str1==str3);  //false  
  7. System.out.println(str1==str3.intern()); //true  

2) subString() 方法的内存泄漏

 

如果原字串很长,截取的字串却有比较短,使用以下方式返回:

 

[java]  view plain copy
 
  1. new String(str1.substring(begin,end));  

3) 字符串分割和查找

 

可以使用的方法:

split()方法   -- 最慢, 写法简单

StringTokenizer 方法  -- 较快,写法一般

indexOf()和subString() 方法 - 最快, 写法麻烦

 

[java]  view plain copy
 
  1. package performance.program.string;  
  2.   
  3. import java.util.StringTokenizer;  
  4.   
  5. public class StringSplit {  
  6.   
  7.     public static void splitMethod(String str) {  
  8.   
  9.         long beginTime = System.currentTimeMillis();  
  10.         for (int i = 0; i < 10000; i++) {  
  11.             str.split(";");  
  12.         }  
  13.   
  14.         long endTime = System.currentTimeMillis();  
  15.         System.out.println("splitMethod use " + (endTime - beginTime));  
  16.     }  
  17.   
  18.     public static void tokenizerMethod(String str) {  
  19.   
  20.         long beginTime = System.currentTimeMillis();  
  21.   
  22.         StringTokenizer st = new StringTokenizer(str, ";");  
  23.         for (int i = 0; i < 10000; i++) {  
  24.             while (st.hasMoreTokens()) {  
  25.                 st.nextToken();  
  26.             }  
  27.             st = new StringTokenizer(str, ";");  
  28.         }  
  29.   
  30.         long endTime = System.currentTimeMillis();  
  31.         System.out.println("tokenizerMethod use " + (endTime - beginTime));  
  32.     }  
  33.   
  34.     public static void IndexMethod(String str) {  
  35.   
  36.         long beginTime = System.currentTimeMillis();  
  37.         String tmp = str;  
  38.         for (int i = 0; i < 10000; i++) {  
  39.             while (true) {  
  40.                 String splitStr = null;  
  41.                 int j = tmp.indexOf(";");  
  42.                 if(j<0break;  
  43.                     splitStr = tmp.substring(0,j);  
  44.                 tmp = tmp.substring(j+1);  
  45.             }  
  46.             tmp = str;  
  47.         }  
  48.   
  49.         long endTime = System.currentTimeMillis();  
  50.         System.out.println("IndexMethod use " + (endTime - beginTime));  
  51.     }  
  52.   
  53.     /** 
  54.      * @param args 
  55.      */  
  56.     public static void main(String[] args) {  
  57.         // TODO Auto-generated method stub  
  58.         String orgStr = null;  
  59.         StringBuffer sb = new StringBuffer();  
  60.         for (int i = 0; i < 1000; i++) {  
  61.             sb.append(i);  
  62.             sb.append(";");  
  63.         }  
  64.         orgStr = sb.toString();  
  65.         splitMethod(orgStr);  
  66.         tokenizerMethod(orgStr);  
  67.         IndexMethod(orgStr);  
  68.   
  69.     }  
  70.   
  71. }  


4) 使用ChartAt 代替  startsWith 和 endsWith

 

性能要求比较高时,可以使用这条。

5) StringBuffer 和 StringBuilder

String result = "String" + "and" + "string"+"append";

这段看起来性能不高的代码在实际执行时反而会比StringBuilder 来的快。

原因是Java 编译器本身会做优化。

但是不能完全依靠编译器的优化, 还是建议显示地使用StringBuffer 或StringBuffer对象来提升系统性能。

StringBuffer 和 StringBuilder 的差异是:

StingBuffer 几乎所有的方法都做了同步

StringBuilder 并没有任何同步。

所以StringBuilder 的效率好于StringBuffer, 但是在多线程系统中,StringBuilder 无法保证线程安全。

另外,预先评估StringBuilder 的大小,能提升系统的性能。

 

2. 核心数据结构

1) List 接口

3种List实现: ArrayList,  Vector, 和LinkedList

ArrayList 和Vector 使用了数组实现, Vector 绝大部分方法都做了线程同步, ArrayList 没有对任何一个方法做线程同步。(ArrayList 和Vector 看上去性能相差无几)

使用LinkedList 对堆内存和GC的要求更高。

如果在系统应用中, List对象需要经常在任意位置插入元素,则可以考虑使用LinkedList 替代ArrayList

对于ArrayList从尾部删除元素时效率很高,从头部删除元素时相当费时,

而LinkedList 从头尾删除元素时效率相差无几,但是从中间删除元素时性能非常槽糕。

 

  头部 中间 尾部
ArrayList 6203 3125 16
LinkedList 15 8781 16

 

 

List 遍历操作

 

  ForEach 迭代器 for 循环
ArrayList 63 47 31
LinkedList 63 47 无穷大

 

 

2) Map 接口

实现类有: Hashtable, HashMap, LinkedHashMap和TreeMap

HashTable 和HashMap  的差异

HashTable大部分方法做了同步, HashTable 不允许key或者Value 使用null值;(性能上看起来无明显差异)

 

3) Set 接口

 

4) 优化集合访问代码

1. 分离循环中被重复调用的代码

for(int i=0;i<collection.size();i++)

替换成

int  count = collection.size();

for(int i=0;i<count;i++)

 

5). RandomAccess接口

 

3. 使用NIO 提升性能

在Java 的标准I/O中, 提供了基于流的I/O 实现, 即InputStream 和 OutputStream. 这种基于流的实现是以字节为单位处理数据, 并且非常容易建立各种过滤器。

NIO是 New I/O 的简称, 与旧式的基于流的 I/O 方法相对, 它表示新的一套Java I/O 标准。是在Java 1.4 中被纳入到JDK中的, 特性有

- 为所有的原始类型提供 Buffer 缓存支持

-  使用Java.nio.charset.Charset 作为字符集编码解码解决方案

- 增加通道对象(Channel), 作为新的原始I/O 抽象

- 支持锁和内存映射文件的文件访问接口

- 提供了基于Selector 的异步网络I/O

与流式的I/O 不同, NIO 是基于块(Block 的), 它以块为基本单位处理数据。

 

 

 

4. 引用类型。

强引用、软引用、弱引用和虚引用

强引用:

a)强引用可以直接访问目标对象

b) 强引用所指向的对象在任何时候都不会被系统回收

c)强引用可能导致内存泄漏

 

软引用:

软引用可以通过java.lang.ref.SoftReference来使用。 一个持有软引用的对象,不会被JVM很快回收, JVM会根据当前的使用状况来判断何时回收.当堆使用率临近阈值时,才会去回收软引用的对象。只要有足够的内存,软引用便可能在内存中存活相对长一段时间。因此,软引用可以用于实现对内存敏感的Cache.

 

弱引用:

在系统GC时,只要发现弱引用,不管系统堆空间是否足够,都会将对象进行回收。

 

虚引用

一个持有虚引用的对象,和没有引用几乎是一样的,随时都可能被垃圾回收器回收。但试图通过虚引用的get()方法去跌强引用时,总是会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪回收过程。

 

WeakHashMap类及其实现

WeakHashMap 是弱引用的一种典型应用,它可以作为简单的缓存表解决方案。

 

改善系统性能的技巧

1. 慎用异常

try catch 应用与循环体之外

 

2. 使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中,速度较快。

其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。

局部变量的访问速度远远高于类的成员变量。

 

3. 位运算代替乘除法

 

4. 替换Switch .

这一条应该注意的是一个新的思路问题, 使用不同的方式代替switch 语句。

这里的例子性能测试起来, switch 的性能反而更好一些。

 

[java]  view plain copy
 
  1. package com.oscar999.performance.skill;  
  2.   
  3. public class SwitchReplaceSkill {  
  4.   
  5.     /** 
  6.      * @param args 
  7.      */  
  8.   
  9.     protected void oldMethod() {  
  10.         int re = 0;  
  11.         for (int i = 0; i < 100000000; i++) {  
  12.             re = switchInt(i);  
  13.         }  
  14.     }  
  15.   
  16.     public void newMethod() {  
  17.         int re=0;  
  18.         int[] sw= new int[]{0,3,6,7,8,10,16,18,44};  
  19.         for(int i = 0; i < 100000000; i++) {  
  20.             re = arrayInt(sw,i);  
  21.         }  
  22.     }  
  23.   
  24.     protected int switchInt(int z) {  
  25.         int i = z % 10 + 1;  
  26.         switch (i) {  
  27.         case 1:  
  28.             return 3;  
  29.         case 2:  
  30.             return 6;  
  31.         case 3:  
  32.             return 7;  
  33.         case 4:  
  34.             return 8;  
  35.         case 5:  
  36.             return 10;  
  37.         case 6:  
  38.             return 16;  
  39.         case 7:  
  40.             return 18;  
  41.         case 8:  
  42.             return 44;  
  43.         default:  
  44.             return -1;  
  45.         }  
  46.     }  
  47.     protected int arrayInt(int[] sw,int z)  
  48.     {  
  49.         int i=z%10+1;  
  50.         if(i>8||i<1)  
  51.         {  
  52.             return -1;  
  53.         }else{  
  54.             return sw[i];  
  55.         }  
  56.     }  
  57.   
  58.     public static void main(String[] args) {  
  59.         // TODO Auto-generated method stub  
  60.         SwitchReplaceSkill skill = new SwitchReplaceSkill();  
  61.         long begTime = System.currentTimeMillis();        
  62.         skill.oldMethod();  
  63.         long endTime = System.currentTimeMillis();        
  64.         System.out.println("Old Method use: "+(endTime-begTime));  
  65.           
  66.         begTime = System.currentTimeMillis();     
  67.         skill.newMethod();  
  68.         endTime = System.currentTimeMillis();     
  69.         System.out.println("New Method use: "+(endTime-begTime));  
  70.           
  71.     }  
  72.   
  73. }  


5. 一维数组代替二维数组

 

直接看例子:

 

[java]  view plain copy
 
  1. package com.oscar999.performance.skill;  
  2.   
  3. public class OneDime2TwoDime {  
  4.   
  5.     protected void oldMethod(){  
  6.         int[][] array = new int[1000][1000];  
  7.         int re = 0;  
  8.         for(int k=0;k<100;k++)  
  9.             for(int i=0;i<array[0].length;i++)  
  10.                 for(int j=0;j<array[0].length;j++)  
  11.                     array[i][j] = i;  
  12.           
  13.         for(int k=0;k<100;k++)  
  14.             for(int i=0;i<array[0].length;i++)  
  15.                 for(int j=0;j<array[0].length;j++)  
  16.                     re=array[i][j];       
  17.           
  18.     }  
  19.       
  20.     protected void newMethod(){  
  21.         int[] array = new int[1000000];  
  22.         int re = 0;  
  23.         for(int k=0;k<100;k++)  
  24.             for(int i=0;i<array.length;i++)  
  25.                 array[i] = i;  
  26.         for(int k=0;k<100;k++)  
  27.             for(int i=0;i<array.length;i++)  
  28.                 re = array[i];    
  29.     }     
  30.     /** 
  31.      * @param args 
  32.      */  
  33.     public static void main(String[] args) {  
  34.         // TODO Auto-generated method stub  
  35.         OneDime2TwoDime skill = new OneDime2TwoDime();  
  36.         long begTime = System.currentTimeMillis();        
  37.         skill.oldMethod();  
  38.         long endTime = System.currentTimeMillis();        
  39.         System.out.println("Old Method use: "+(endTime-begTime));  
  40.           
  41.         begTime = System.currentTimeMillis();     
  42.         skill.newMethod();  
  43.         endTime = System.currentTimeMillis();     
  44.         System.out.println("New Method use: "+(endTime-begTime));  
  45.     }  
  46.   
  47. }  

Old Method use: 453
New Method use: 281

 

一维数字用的时间少了很多。

 

6. 提取表达式

有些重复运算的部分可以提取出来。

 

[java]  view plain copy
 
  1. package com.oscar999.performance.skill;  
  2.   
  3. public class ExtraExpression {  
  4.   
  5.     protected void oldMethod(){  
  6.         double d = Math.random();  
  7.         double a = Math.random();  
  8.         double b = Math.random();  
  9.         double e = Math.random();  
  10.         double x,y;  
  11.         for(int i=0;i<10000000;i++)  
  12.         {  
  13.             x = d*a*b/3*4*a;  
  14.             x = e*a*b/3*4*a;  
  15.         }  
  16.     }  
  17.       
  18.     protected void newMethod(){  
  19.         double d = Math.random();  
  20.         double a = Math.random();  
  21.         double b = Math.random();  
  22.         double e = Math.random();  
  23.         double x,y,t;  
  24.         for(int i=0;i<10000000;i++)  
  25.         {  
  26.             t = a*b/3*4*a;  
  27.             x = d*t;  
  28.             x = e*t;  
  29.         }  
  30.     }  
  31.       
  32.     /** 
  33.      * @param args 
  34.      */  
  35.     public static void main(String[] args) {  
  36.         // TODO Auto-generated method stub  
  37.         OneDime2TwoDime skill = new OneDime2TwoDime();  
  38.         long begTime = System.currentTimeMillis();        
  39.         skill.oldMethod();  
  40.         long endTime = System.currentTimeMillis();        
  41.         System.out.println("Old Method use: "+(endTime-begTime));  
  42.           
  43.         begTime = System.currentTimeMillis();     
  44.         skill.newMethod();  
  45.         endTime = System.currentTimeMillis();     
  46.         System.out.println("New Method use: "+(endTime-begTime));  
  47.     }  
  48.   
  49. }  


Old Method use: 109
New Method use: 79

 

 

7.  展开循环

展开循环是一种在极端情况下使用的优化手段,因为展开循可能会影响代码的可读性和可维护性, 所以取舍之间, 就要根据实际状况来看了。

看例子:

 

[java]  view plain copy
 
  1. package com.oscar999.performance.skill;  
  2.   
  3. public class ExpandCycle {  
  4.     protected void oldMethod(){  
  5.         int[] array = new int[9999999];  
  6.         for(int i=0;i<9999999;i++){  
  7.             array[i]=i;  
  8.         }  
  9.     }  
  10.       
  11.     protected void newMethod(){  
  12.         int[] array = new int[9999999];  
  13.         for(int i=0;i<9999999;i+=3){  
  14.             array[i]=i;  
  15.             array[i+1]=i+1;  
  16.             array[i+2]=i+2;  
  17.         }  
  18.     }  
  19.     /** 
  20.      * @param args 
  21.      */  
  22.     public static void main(String[] args) {  
  23.         // TODO Auto-generated method stub  
  24.         ExpandCycle skill = new ExpandCycle();  
  25.         long begTime = System.currentTimeMillis();        
  26.         skill.oldMethod();  
  27.         long endTime = System.currentTimeMillis();        
  28.         System.out.println("Old Method use: "+(endTime-begTime));  
  29.           
  30.         begTime = System.currentTimeMillis();     
  31.         skill.newMethod();  
  32.         endTime = System.currentTimeMillis();     
  33.         System.out.println("New Method use: "+(endTime-begTime));  
  34.     }  
  35.   
  36. }  

Old Method use: 78
New Method use: 47

 

8. 布尔运算代替位运算

虽然位运算的速度远远高于算术运算,但是在条件判断时,使用位运算替代布尔运算却是非常错误的选择。

对于"a&&b&&c", 只要有一项返回 false, 整个表达式就返回 false.

 

[java]  view plain copy
 
  1. package com.oscar999.performance.skill;  
  2.   
  3. public class BooleanBit {  
  4.     protected void oldMethod(){  
  5.         boolean a = false;  
  6.         boolean b = true;  
  7.         int d = 0;  
  8.         for(int i=0;i<10000000;i++)  
  9.             if(a&b&"Java_Perform".contains("Java"))  
  10.                 d = 0;  
  11.     }  
  12.       
  13.     protected void newMethod(){  
  14.         boolean a = false;  
  15.         boolean b = true;  
  16.         int d = 0;  
  17.         for(int i=0;i<10000000;i++)  
  18.             if(a&&b&&"Java_Perform".contains("Java"))  
  19.                 d = 0;        
  20.     }  
  21.       
  22.     /** 
  23.      * @param args 
  24.      */  
  25.     public static void main(String[] args) {  
  26.         // TODO Auto-generated method stub  
  27.         BooleanBit skill = new BooleanBit();  
  28.         long begTime = System.currentTimeMillis();        
  29.         skill.oldMethod();  
  30.         long endTime = System.currentTimeMillis();        
  31.         System.out.println("Old Method use: "+(endTime-begTime));  
  32.           
  33.         begTime = System.currentTimeMillis();     
  34.         skill.newMethod();  
  35.         endTime = System.currentTimeMillis();     
  36.         System.out.println("New Method use: "+(endTime-begTime));  
  37.     }  
  38.   
  39. }  


Old Method use: 265
New Method use: 32

 

9. 使用 arrayCopy()

Java API 提高了数组复制的高效方法: arrayCopy().

这个函数是 native 函数, 通常native 函数的性能要优于普通的函数, 仅出于性能考虑, 在软件开发时, 应尽可能调用native 函数。

 

[java]  view plain copy
 
  1. package com.oscar999.performance.skill;  
  2.   
  3. public class ArrayCopy {  
  4.     protected void oldMethod(){  
  5.         int size = 100000;  
  6.         int[] array = new int[size];  
  7.         int[] arraydst = new int[size];  
  8.         for(int i=0;i<array.length;i++)  
  9.         {  
  10.             array[i]=i;  
  11.         }  
  12.         for(int k=0;k<1000;k++)  
  13.             for(int i=0;i<size;i++)  
  14.                 arraydst[i]=array[i];  
  15.     }  
  16.       
  17.     protected void newMethod(){  
  18.         int size = 100000;  
  19.         int[] array = new int[size];  
  20.         int[] arraydst = new int[size];  
  21.         for(int i=0;i<array.length;i++)  
  22.         {  
  23.             array[i]=i;  
  24.         }  
  25.         for(int k=0;k<1000;k++)  
  26.             System.arraycopy(array, 0, arraydst, 0, size);  
  27.     }  
  28.       
  29.     /** 
  30.      * @param args 
  31.      */  
  32.     public static void main(String[] args) {  
  33.         // TODO Auto-generated method stub  
  34.         ArrayCopy skill = new ArrayCopy();  
  35.         long begTime = System.currentTimeMillis();        
  36.         skill.oldMethod();  
  37.         long endTime = System.currentTimeMillis();        
  38.         System.out.println("Old Method use: "+(endTime-begTime));  
  39.           
  40.         begTime = System.currentTimeMillis();     
  41.         skill.newMethod();  
  42.         endTime = System.currentTimeMillis();     
  43.         System.out.println("New Method use: "+(endTime-begTime));  
  44.     }  
  45.   
  46.   
  47. }  

Old Method use: 156
New Method use: 63

 

 

10. 使用 Buffer 进行 I/O 操作

除NIO 外, 使用Java 进行I/O 操作有两种基本方式

1. 使用基于InputStream 和 OutoutStream的方式

2. 使用Writer 和Reader

无论使用哪种方式进行文件I/O , 如果能合理地使用缓冲, 就能有效提高I/O 的性能

 

[java]  view plain copy
 
  1. package com.oscar999.performance.skill;  
  2.   
  3. import java.io.BufferedOutputStream;  
  4. import java.io.DataOutputStream;  
  5. import java.io.FileOutputStream;  
  6.   
  7. public class BufferStream {  
  8.   
  9.     protected void oldMethod() {  
  10.         int count = 10000;  
  11.         try {  
  12.             DataOutputStream dos = new DataOutputStream(new FileOutputStream(  
  13.                     "testfile.txt"));  
  14.             for (int i = 0; i < count; i++)  
  15.                 dos.writeBytes(String.valueOf(i) + "\r\n");  
  16.             dos.close();  
  17.   
  18.         } catch (Exception e) {  
  19.             // TODO Auto-generated catch block  
  20.             e.printStackTrace();  
  21.         }  
  22.     }  
  23.   
  24.     protected void newMethod(){  
  25.         int count = 10000;  
  26.         try{  
  27.             DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("testfile.txt")));      
  28.             for (int i = 0; i < count; i++)  
  29.                 dos.writeBytes(String.valueOf(i) + "\r\n");  
  30.             dos.close();  
  31.         }catch(Exception e)  
  32.         {  
  33.             e.printStackTrace();  
  34.         }  
  35.           
  36.     }  
  37.   
  38.     /** 
  39.      * @param args 
  40.      */  
  41.     public static void main(String[] args) {  
  42.         // TODO Auto-generated method stub  
  43.         BufferStream skill = new BufferStream();  
  44.         long begTime = System.currentTimeMillis();  
  45.         skill.oldMethod();  
  46.         long endTime = System.currentTimeMillis();  
  47.         System.out.println("Old Method use: " + (endTime - begTime));  
  48.   
  49.         begTime = System.currentTimeMillis();  
  50.         skill.newMethod();  
  51.         endTime = System.currentTimeMillis();  
  52.         System.out.println("New Method use: " + (endTime - begTime));  
  53.     }  
  54.   
  55. }  


Old Method use: 516
New Method use: 0

 

 

11. 使用clone() 代替new 

对于重量级对象, 优于对象在构造函数中可能会进行一些复杂且耗时额操作, 因此, 构造函数的执行时间可能会比较长。Object.clone() 方法可以绕过对象构造函数, 快速复制一个对象实例。由于不需要调用对象构造函数, 因此, clone 方法不会受到构造函数性能的影响, 快速生成一个实例。

 

12. 静态方法替代实例方法

使用static 关键字描述的方法为静态方法, 在Java 中, 优于实例方法需要维护一张类似虚函数表的结构,以实现对多态的支持。 与静态方法相比, 实例方法的调用需要更多的资源。 因此,对于一些常用的工具类方法,没有对其进行重载的必要,那么将它们声明为static, 便可以加速方法的调用。

来源:http://blog.csdn.net/oscar999/article/details/44801677

你可能感兴趣的:(java,性能优化)