Java中的内存泄漏
1.Java内存回收机制
不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址。Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的。GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题。在Java语言中,判断一个内存空间是否符合垃圾收集标准有两个:一个是给对象赋予了空值null,以下再没有调用过,另一个是给对象赋予了新值,这样重新分配了内存空间。
2.Java内存泄漏引起的原因
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。
Java内存泄漏的根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。具体主要有如下几大类:
1、静态集合类引起内存泄漏:
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
例如
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。
2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
例如:
public static void main(String[] args)
{
Set set = new HashSet();
Person p1 = new Person(“唐僧”,”pwd1”,25);
Person p2 = new Person(“孙悟空”,”pwd2”,26);
Person p3 = new Person(“猪八戒”,”pwd3”,27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println(“总共有:”+set.size()+” 个元素!”); //结果:总共有:3 个元素!
p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3); //此时remove不掉,造成内存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println(“总共有:”+set.size()+” 个元素!”); //结果:总共有:4 个元素!
for (Person person : set)
{
System.out.println(person);
}
}
3、监听器
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
4、各种连接
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。
5、内部类和外部模块的引用
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如: public void registerMsg(Object b); 这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。
6、单例模式
不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏,考虑下面的例子:
class A{
public A(){
B.getInstance().setA(this);
}
….
}
//B类采用单例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter…
}
显然B采用singleton模式,它持有一个A对象的引用,而这个A类的对象将不能被回收。想象下如果A是个比较复杂的对象或者集合类型会发生什么情况
1 Exception优化
a. new Exception(…)会构建一个异常堆栈路径,非常耗费时间和空间,尤其是在递归调用的时候。创建异常对象一般比创建普通对象要慢30-100倍。自定义异常类时,层级不要太多。
b. 可以通过重写Exception类的fillInStackTrace方法而避免过长堆栈路径的生成
class MyException extends Exception {
/** * */
private static final long serialVersionUID = -1515205444433997458L;
public Throwable fillInStackTrace() {
return this;
}
}
c. 所以有节制地使用异常,不要将异常用于控制流程、终止循环等。只将异常用于意外和错误场景(文件找不到、非法输入格式等)。尽量复用之前创建的异常对象。
1.6 排序和查找优化
a. 除非数组或者链表元素很少,否则不要使用选择排序、冒泡排序和插入排序。使用堆排序、归并排序和快速排序。
b. 更推荐的做法是使用JDK标准API内置的排序方法,时间复杂度为O(nlog(n))
对数组排序用Arrays.sort(它的实现代码使用改良的快速排序算法,不会占用额外内存空间,但是不稳定)
对链表排序用Collections.sort(稳定算法,但会使用额外内存空间)
c. 避免对数组和链表进行线性查找,除非你明确知道要查找的次数很少或者数组和链表长度很短
对于数组使用Arrays.binarySearch,但前提是数组已经有序,并且数组如包含多个要查找的元素,不能保证返回哪一个的index
对于链表使用Collections.binarySearch,前提也是链表已经有序
使用哈希查找:HashSet、HashMap
import util.StopWatch;
/** * 循环优化: * 除了本例中将循环不变量移出循环外,还有将忙循环放在外层 * @author jxqlovejava * */
public class LoopOptimization {
public int size() {
try {
Thread.sleep(200); // 模拟耗时操作
}
catch(InterruptedException ie) {
}
return 10;
}
public void slowLoop() {
StopWatch sw = new StopWatch("slowLoop");
sw.start();
for(int i = 0; i < size(); i++);
sw.end();
sw.printEclapseDetail();
}
public void optimizeLoop() {
StopWatch sw = new StopWatch("optimizeLoop");
sw.start();
// 将循环不变量移出循环
for(int i = 0, stop = size(); i < stop; i++);
sw.end();
sw.printEclapseDetail();
}
public static void main(String[] args) {
LoopOptimization loopOptimization = new LoopOptimization();
loopOptimization.slowLoop();
loopOptimization.optimizeLoop();
}
}
slowLoop任务耗时(毫秒):2204
optimizeLoop任务耗时(毫秒):211
可以很清楚地看到不提出循环不变量比提出循环不变量要慢10倍,在循环次数越大并且循环不变量的计算越耗时的情况下,这种优化会越明显。
b. 避免重复计算
这条太常见,不举例了
c. 尽量减少数组索引访问次数,数组索引访问比一般的变量访问要慢得多
数组索引访问比如int i = array[0];需要进行一次数组索引访问(和数组索引访问需要检查索引是否越界有关系吧)。这条Tip经过我的测试发现效果不是很明显(但的确有一些时间性能提升),可能在数组是大数组、循环次数比较多的情况下更明显。
import util.StopWatch;
/** * 数组索引访问优化,尤其针对多维数组 * 这条优化技巧对时间性能提升不太明显,而且可能降低代码可读性 * @author jxqlovejava * */
public class ArrayIndexAccessOptimization {
private static final int m = 9; // 9行
private static final int n = 9; // 9列
private static final int[][] array = {
{ 1, 2, 3, 4, 5, 6, 7, 8, 9 },
{ 11, 12, 13, 14, 15, 16, 17, 18, 19 },
{ 21, 22, 23, 24, 25, 26, 27, 28, 29 },
{ 31, 32, 33, 34, 35, 36, 37, 38, 39 },
{ 41, 42, 43, 44, 45, 46, 47, 48, 49 },
{ 51, 52, 53, 54, 55, 56, 57, 58, 59 },
{ 61, 62, 63, 64, 65, 66, 67, 68, 69 },
{ 71, 72, 73, 74, 75, 76, 77, 78, 79 },
{ 81, 82, 83, 84, 85, 86, 87, 88, 89 },
{ 91, 92, 93, 94, 95, 96, 97, 98, 99 }
}; // 二维数组
public void slowArrayAccess() {
StopWatch sw = new StopWatch("slowArrayAccess");
sw.start();
for(int k = 0; k < 10000000; k++) {
int[] rowSum = new int[m];
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
rowSum[i] += array[i][j];
}
}
}
sw.end();
sw.printEclapseDetail();
}
public void optimizeArrayAccess() {
StopWatch sw = new StopWatch("optimizeArrayAccess");
sw.start();
for(int k = 0; k < 10000000; k++) {
int[] rowSum = new int[n];
for(int i = 0; i < m; i++) {
int[] arrI = array[i];
int sum = 0;
for(int j = 0; j < n; j++) {
sum += arrI[j];
}
rowSum[i] = sum;
}
}
sw.end();
sw.printEclapseDetail();
}
public static void main(String[] args) {
ArrayIndexAccessOptimization arrayIndexAccessOpt = new ArrayIndexAccessOptimization();
arrayIndexAccessOpt.slowArrayAccess();
arrayIndexAccessOpt.optimizeArrayAccess();
}
}
d. 将常量声明为final static或者final,这样编译器就可以将它们内联并且在编译时就预先计算好它们的值
e. 用switch-case替代冗长的if-else-if
public class IfElseOptimization {
public void slowIfElse() {
StopWatch sw = new StopWatch("slowIfElse");
sw.start();
for(int k = 0; k < 2000000000; k++) {
int i = 9;
if(i == 0) { }
else if(i == 1) { }
else if(i == 2) { }
else if(i == 3) { }
else if(i == 4) { }
else if(i == 5) { }
else if(i == 6) { }
else if(i == 7) { }
else if(i == 8) { }
else if(i == 9) { }
}
sw.end();
sw.printEclapseDetail();
}
public void optimizeIfElse() {
StopWatch sw = new StopWatch("optimizeIfElse");
sw.start();
for(int k = 0; k < 2000000000; k++) {
int i = 9;
switch(i) {
case 0:
break;
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 5:
break;
case 6:
break;
case 7:
break;
case 8:
break;
case 9:
break;
default:
}
}
sw.end();
sw.printEclapseDetail();
}
public static void main(String[] args) {
IfElseOptimization ifElseOpt = new IfElseOptimization();
ifElseOpt.slowIfElse();
ifElseOpt.optimizeIfElse();
}
}
f. 如果冗长的if-else-if无法被switch-case替换,那么可以使用查表法优化
1.8 集合类优化
a. 如果使用HashSet或者HashMap,确保key对象有一个快速合理的hashCode实现,并且要遵守hashCode和equals实现规约
b. 如果使用TreeSet或者TreeMap
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import util.StopWatch;
public class ReadFileDemos {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\jxqlovejava\\workspace\\PerformanceOptimization\\test.txt";
InputStream in = null;
BufferedInputStream bis = null;
File file = null;
StopWatch sw = new StopWatch();
sw.clear();
sw.setTaskName("一次性读取到字节数组+BufferedReader");
sw.start();
file = new File(filePath);
in = new FileInputStream(filePath);
BufferedReader br = new BufferedReader(new InputStreamReader(in));
char[] charBuf = new char[(int) file.length()];
br.read(charBuf);
br.close();
in.close();
sw.end();
sw.printEclapseDetail();
sw.clear();
sw.setTaskName("一次性读取到字节数组");
sw.start();
in = new FileInputStream(filePath);
byte[] buf = new byte[in.available()];
in.read(buf);// read(byte[] buf)方法重载
in.close();
for (byte c : buf) {
}
sw.end();
sw.printEclapseDetail();
sw.clear();
sw.setTaskName("BufferedInputStream逐字节读取");
sw.start();
in = new FileInputStream(filePath);
bis = new BufferedInputStream(in);
int b;
while ((b = bis.read()) != -1);
in.close();
bis.close();
sw.end();
sw.printEclapseDetail();
sw.clear();
sw.setTaskName("BufferedInputStream+DataInputStream分批读取到字节数组");
sw.start();
in = new FileInputStream(filePath);
bis = new BufferedInputStream(in);
DataInputStream dis = new DataInputStream(bis);
byte[] buf2 = new byte[1024*4]; // 4k per buffer
int len = -1;
StringBuffer sb = new StringBuffer();
while((len=dis.read(buf2)) != -1 ) {
// response.getOutputStream().write(b, 0, len);
sb.append(new String(buf2));
}
dis.close();
bis.close();
in.close();
sw.end();
sw.printEclapseDetail();
sw.clear();
sw.setTaskName("FileInputStream逐字节读取");
sw.start();
in = new FileInputStream(filePath);
int c;
while ((c = in.read()) != -1);
in.close();
sw.end();
sw.printEclapseDetail();
}
}
一次性读取到字节数组+BufferedReader任务耗时(毫秒):121
一次性读取到字节数组任务耗时(毫秒):23
BufferedInputStream逐字节读取任务耗时(毫秒):408
BufferedInputStream+DataInputStream分批读取到字节数组任务耗时(毫秒):147
FileInputStream逐字节读取任务耗时(毫秒):38122
b. 将文件压缩后存到磁盘,这样读取时更快,虽然会耗费额外的CPU来进行解压缩。网络传输时也尽量压缩后传输。Java中压缩有关的类:ZipInputStream、ZipOutputStream、GZIPInputStream和GZIPOutputStream
1.10 对象创建优化
a. 如果程序使用很多空间(内存),它一般也将耗费更多的时间:对象分配和垃圾回收需要耗费时间、使用过多内存可能导致不能很好利用CPU缓存甚至可能需要使用虚存(访问磁盘而不是RAM)。而且根据JVM的垃圾回收器的不同,使用太多内存可能导致长时间的回收停顿,这对于交互式系统和实时应用是不能忍受的。
b. 对象创建需要耗费时间(分配内存、初始化、垃圾回收等),所以避免不必要的对象创建。但是记住不要轻易引入对象池除非确实有必要。大部分情况,使用对象池仅仅会导致代码量增加和维护代价增大,并且对象池可能引入一些微妙的问题
c. 不要创建一些不会被使用到的对象
1.11 数组批量操作优化
数组批量操作比对数组进行for循环要快得多,部分原因在于数组批量操作只需进行一次边界检查,而对数组进行for循环,每一次循环都必须检查边界。
a. System.arrayCopy(src, si, dst, di, n) 从源数组src拷贝片段[si…si+n-1]到目标数组dst[di…di+n-1]
b. boolean Arrays.equals(arr1, arr2) 返回true,当且仅当arr1和arr2的长度相等并且元素一一对象相等(equals)
c. void Arrays.fill(arr, x) 将数组arr的所有元素设置为x
d. void Arrays.fill(arr, i, j x) 将数组arr的[i..j-1]索引处的元素设置为x
e. int Arrays.hashCode(arr) 基于数组的元素计算数组的hashcode
1.12 科学计算优化
Colt(http://acs.lbl.gov/software/colt/)是一个科学计算开源库,可以用于线性代数、稀疏和紧凑矩阵、数据分析统计,随机数生成,数组算法,代数函数和复数等。
1.13 反射优化
a. 通过反射创建对象、访问属性、调用方法比一般的创建对象、访问属性和调用方法要慢得多
b. 访问权限检查(反射调用private方法或者反射访问private属性时会进行访问权限检查,需要通过setAccessible(true)来达到目的)可能会让反射调用方法更慢,可以通过将方法声明为public来比避免一些开销。这样做之后可以提高8倍。
1.14 编译器和JVM平台优化
a. Sun公司的HotSpot Client JVM会进行一些代码优化,但一般将快速启动放在主动优化之前进行考虑
b. Sun公司的HotSpot Server JVM(-server选项,Windows平台无效)会进行一些主动优化,但可能带来更长的启动延迟
c. IBM的JVM也会进行一些主动优化
d. J2ME和一些手持设备(如PDA)不包含JIT编译,很可能不会进行任何优化
1.14 编译器和JVM平台优化
a. Sun公司的HotSpot Client JVM会进行一些代码优化,但一般将快速启动放在主动优化之前进行考虑
b. Sun公司的HotSpot Server JVM(-server选项,Windows平台无效)会进行一些主动优化,但可能带来更长的启动延迟
c. IBM的JVM也会进行一些主动优化
d. J2ME和一些手持设备(如PDA)不包含JIT编译,很可能不会进行任何优化
1.15 Profile
2.1 堆(对象)和栈(方法参数、局部变量等)。堆被所有线程共享,但栈被每个线程独享
2.2 空间消耗的三个重要方面是:
Allocation Rate(分配频率)、
Retention(保留率)和
Fragmentation(内存碎片)
Allocation Rate是程序创建新对象的频率,频率越高耗费的时间和空间越多。
Retention是存活的堆数据数量。这个值越高需要耗费越多的空间和时间(垃圾回收器执行分配和去分配工作时需要进行更多的管理工作)
Fragmentation:内存碎片是指小块无法使用的内存。如果一直持续创建大对象,可能会引起过多的内存碎片。从而需要更多的时间分配内存(因为要查找一个足够大的连续可用内存块),并且会浪费更多的空间因为内存碎片无法被利用。当然某些GC算法可以避免过多内存碎片的产生,但相应的算法代价也较高。
2.3 内存泄露
2.4 垃圾回收器的种类(分代收集、标记清除、引用计数、增量收集、压缩…)对Allocation Rate、Retention和Fragmentation的时间空间消耗影响很大
2.5 对象延迟创建
public class StopWatch {
private static final String DEFAULT_TASK_NAME = "defaultTask";
private String taskName;
private long start, end;
private boolean hasStarted, hasEnded;
// 时间单位枚举:毫秒、秒和分钟
public enum TimeUnit { MILLI, SECOND, MINUTE }
public StopWatch() {
this(DEFAULT_TASK_NAME);
}
public StopWatch(String taskName) {
this.taskName = StringUtil.isEmpty(taskName) ? DEFAULT_TASK_NAME : taskName;
}
public void start() {
start = System.currentTimeMillis();
hasStarted = true;
}
public void end() {
if(!hasStarted) {
throw new IllegalOperationException("调用StopWatch的end()方法之前请先调用start()方法");
}
end = System.currentTimeMillis();
hasEnded = true;
}
public void clear() {
this.start = 0;
this.end = 0;
this.hasStarted = false;
this.hasEnded = false;
}
/** * 获取总耗时,单位为毫秒 * @return 消耗的时间,单位为毫秒 */
public long getEclapsedMillis() {
if(!hasEnded) {
throw new IllegalOperationException("请先调用end()方法");
}
return (end-start);
}
/** * 获取总耗时,单位为秒 * @return 消耗的时间,单位为秒 */
public long getElapsedSeconds() {
return this.getEclapsedMillis() / 1000;
}
/** * 获取总耗时,单位为分钟 * @return 消耗的时间,单位为分钟 */
public long getElapsedMinutes() {
return this.getEclapsedMillis() / (1000*60);
}
public void setTaskName(String taskName) {
this.taskName = StringUtil.isEmpty(taskName) ? DEFAULT_TASK_NAME : taskName;
}
public String getTaskName() {
return this.taskName;
}
/** * 输出任务耗时情况,单位默认为毫秒 */
public void printEclapseDetail() {
this.printEclapseDetail(TimeUnit.MILLI);
}
/** * 输出任务耗时情况,可以指定毫秒、秒和分钟三种时间单位 * @param timeUnit 时间单位 */
public void printEclapseDetail(TimeUnit timeUnit) {
switch(timeUnit) {
case MILLI:
System.out.println(this.getTaskName() + "任务耗时(毫秒):" + this.getEclapsedMillis());
break;
case SECOND:
System.out.println(this.getTaskName() + "任务耗时(秒):" + this.getElapsedSeconds());
break;
case MINUTE:
System.out.println(this.getTaskName() + "任务耗时(分钟):" + this.getElapsedMinutes());
break;
default:
System.out.println(this.getTaskName() + "任务耗时(毫秒):" + this.getEclapsedMillis());
}
}
}