垃圾收集算法的核心思想
Java语言建立了垃圾收集机制用以跟蹤正在使用的对象和发现并回收不再使用(引用)的对象该机制可以有效防范动态内存分配中可能发生的两个危险因内存垃圾过多而引发的内存耗尽以及不恰当的内存释放所造成的内存非法引用
垃圾收集算法的核心思想是对虚拟机可用内存空间即堆空间中的对象进行识别如果对象正在被引用那么称其为存活对象反之如果对象不再被引用则为垃圾对象可以回收其占据的空间用于再分配垃圾收集算法的选择和垃圾收集系统参数的合理调节直接影响着系统性能因此需要开发人员做比较深入的了解
触发主GC(Garbage Collector)的条件
JVM进行次GC的频率很高但因为这种GC占用时间极短所以对系统产生的影响不大更值得关注的是主GC的触发条件因为它对系统影响很明显总的来说有两个条件会触发主GC:
①当应用程序空闲时即没有应用线程在运行时GC会被调用因为GC在优先级最低的线程中进行所以当应用忙时GC线程就不会被调用但以下条件除外
②Java堆内存不足时GC会被调用当应用线程在运行并在运行过程中创建新对象若这时内存空间不足JVM就会强制地调用GC线程以便回收内存用于新的分配若GC一次之后仍不能满足内存分配的要求JVM会再进行两次GC作进一步的尝试若仍无法满足要求则 JVM将报out of memory的错误Java应用将停止
由于是否进行主GC由JVM根据系统环境决定而系统环境在不断的变化当中所以主GC的运行具有不确定性无法预计它何时必然出现但可以确定的是对一个长期运行的应用来说其主GC是反复进行的
减少GC开销的措施
根据上述GC的机制程序的运行会直接影响系统环境的变化从而影响GC的触发若不针对GC的特点进行设计和编码就会出现内存驻留等一系列负面影响为了避免这些影响基本的原则就是尽可能地减少垃圾和减少GC过程中的开销具体措施包括以下几个方面:
()不要显式调用Systemgc()
此函数建议JVM进行主GC虽然只是建议而非一定但很多情况下它会触发主GC从而增加主GC的频率也即增加了间歇性停顿的次数
()尽量减少临时对象的使用
临时对象在跳出函数调用后会成为垃圾少用临时变量就相当于减少了垃圾的产生从而延长了出现上述第二个触发条件出现的时间减少了主GC的机会
()对象不用时最好显式置为Null
一般而言为Null的对象都会被作为垃圾处理所以将不用的对象显式地设为Null有利于GC收集器判定垃圾从而提高了GC的效率
()尽量使用StringBuffer而不用String来累加字符串(详见blog另一篇文章JAVA中String与StringBuffer)
由于String是固定长的字符串对象累加String对象时并非在一个String对象中扩增而是重新创建新的String对象如Str=Str+Str+Str+Str这条语句执行过程中会产生多个垃圾对象因为对次作+操作时都必须创建新的String对象但这些过渡对象对系统来说是没有实际意义的只会增加更多的垃圾避免这种情况可以改用StringBuffer来累加字符串因StringBuffer是可变长的它在原有基础上进行扩增不会产生中间对象
()能用基本类型如IntLong就不用IntegerLong对象
基本类型变量占用的内存资源比相应对象占用的少得多如果没有必要最好使用基本变量
()尽量少用静态对象变量
静态变量属于全局变量不会被GC回收它们会一直占用内存
()分散对象创建或删除的时间
集中在短时间内大量创建新对象特别是大对象会导致突然需要大量内存JVM在面临这种情况时只能进行主GC以回收内存或整合内存碎片从而增加主GC的频率集中删除对象道理也是一样的它使得突然出现了大量的垃圾对象空闲空间必然减少从而大大增加了下一次创建新对象时强制主GC的机会
gc与finalize方法
⑴gc方法请求垃圾回收
使用Systemgc()可以不管JVM使用的是哪一种垃圾回收的算法都可以请求Java的垃圾回收需要注意的是调用Systemgc()也仅仅是一个请求JVM接受这个消息后并不是立即做垃圾回收而只是对几个垃圾回收算法做了加权使垃圾回收操作容易发生或提早发生或回收较多而已
⑵finalize方法透视垃圾收集器的运行
在JVM垃圾收集器收集一个对象之前 一般要求程序调用适当的方法释放资源但在没有明确释放资源的情况下Java提供了缺省机制来终止化该对象释放资源这个方法就是finalize()它的原型为
protected void finalize() throws Throwable
在finalize()方法返回之后对象消失垃圾收集开始执行原型中的throws Throwable表示它可以抛出任何类型的异常
因此当对象即将被销毁时有时需要做一些善后工作可以把这些操作写在finalize()方法里
java 代码
protected void finalize()
{
// finalization code here
}
⑶代码示例
java 代码
class Garbage{
int index;
static int count;
Garbage() {
count++;
Systemoutprintln(object +count+ construct);
setID(count);
}
void setID(int id) {
index=id;
}
protected void finalize() //重写finalize方法
{
Systemoutprintln(object +index+ is reclaimed);
}
public static void main(String[] args)
{
new Garbage();
new Garbage();
new Garbage();
new Garbage();
Systemgc(); //请求运行垃圾收集器
}
}
Java 内存洩漏
由于采用了垃圾回收机制任何不可达对象(对象不再被引用)都可以由垃圾收集线程回收因此通常说的Java 内存洩漏其实是指无意识的非故意的对象引用或者无意识的对象保持无意识的对象引用是指代码的开发人员本来已经对对象使用完毕却因为编码的错误而意外地保存了对该对象的引用(这个引用的存在并不是编码人员的主观意愿)从而使得该对象一直无法被垃圾回收器回收掉这种本来以为可以释放掉的却最终未能被释放的空间可以认为是被洩漏了
考虑下面的程序在ObjStack类中使用push和pop方法来管理堆栈中的对象两个方法中的索引(index)用于指示堆栈中下一个可用位置push方法存储对新对象的引用并增加索引值而pop方法减小索引值并返回堆栈最上面的元素在main方法中创建了容量为的栈并次调用push方法向它添加对象此时index的值为随后又次调用pop方法则index的值变为出栈意味着在堆栈中的空间应该被收集但事实上pop方法只是减小了索引值堆栈仍然保持着对那些对象的引用故个无用对象不会被GC回收造成了内存渗漏
java 代码
public class ObjStack {
private Object[] stack;
private int index;
ObjStack(int indexcount) {
stack = new Object[indexcount];
index = ;
}
public void push(Object obj) {
stack[index] = obj;
index++;
}
public Object pop() {
index;
return stack[index];
}
}
public class Pushpop {
public static void main(String[] args) {
int i = ;
Object tempobj;
//new一个ObjStack对象并调用有参构造函数分配stack Obj数组的空间大小为可以存个对象从开始存储
ObjStack stack = new ObjStack();
while (i < )
{
tempobj = new Object();//循环new Obj对象把每次循环的对象一一存放在stack Obj数组中
stackpush(tempobj);
i++;
Systemoutprintln(第 + i + 次进栈 + \t);
}
while (i > )
{
tempobj = stackpop();//这里造成了空间的浪费
//正确的pop方法可改成如下所指示当引用被返回后堆栈删除对他们的引用因此垃圾收集器在以后可以回收他们
/*
* public Object pop() {index ;Object temp = stack [index];stack [index]=null;return temp;}
*/
i;
Systemoutprintln(第 + ( i) + 次出栈 + \t);
}
}
}
如何消除内存洩漏
虽然Java虚拟机(JVM)及其垃圾收集器(garbage collectorGC)负责管理大多数的内存任务Java软件程序中还是有可能出现内存洩漏实际上这在大型项目中是一个常见的问题避免内存洩漏的第一步是要弄清楚它是如何发生的本文介绍了编写Java代码的一些常见的内存洩漏陷阱以及编写不洩漏代码的一些最佳实践一旦发生了内存洩漏要指出造成洩漏的代码是非常困难的因此本文还介绍了一种新工具用来诊断洩漏并指出根本原因该工具的开销非常小因此可以使用它来寻找处于生产中的系统的内存洩漏
垃圾收集器的作用
虽然垃圾收集器处理了大多数内存管理问题从而使编程人员的生活变得更轻松了但是编程人员还是可能犯错而导致出现内存问题简单地说GC循环地跟蹤所有来自根对象(堆栈对象静态对象JNI句柄指向的对象诸如此类)的引用并将所有它所能到达的对象标记为活动的程序只可以操纵这些对象;其他的对象都被删除了因为GC使程序不可能到达已被删除的对象这么做就是安全的
虽然内存管理可以说是自动化的但是这并不能使编程人员免受思考内存管理问题之苦例如分配(以及释放)内存总会有开销虽然这种开销对编程人员来说是不可见的创建了太多对象的程序将会比完成同样的功能而创建的对象却比较少的程序更慢一些(在其他条件相同的情况下)
而且与本文更为密切相关的是如果忘记释放先前分配的内存就可能造成内存洩漏如果程序保留对永远不再使用的对象的引用这些对象将会占用并耗尽内存这是因为自动化的垃圾收集器无法证明这些对象将不再使用正如我们先前所说的如果存在一个对对象的引用对象就被定义为活动的因此不能删除为了确保能回收对象占用的内存编程人员必须确保该对象不能到达这通常是通过将对象字段设置为null或者从集合(collection)中移除对象而完成的但是注意当局部变量不再使用时没有必要将其显式地设置为null对这些变量的引用将随着方法的退出而自动清除
概括地说这就是内存托管语言中的内存洩漏产生的主要原因保留下来却永远不再使用的对象引用
典型洩漏
既然我们知道了在Java中确实有可能发生内存洩漏就让我们来看一些典型的内存洩漏及其原因
全局集合
在大的应用程序中有某种全局的数据储存库是很常见的例如一个JNDI树或一个会话表在这些情况下必须注意管理储存库的大小必须有某种机制从储存库中移除不再需要的数据
这可能有多种方法但是最常见的一种是周期性运行的某种清除任务该任务将验证储存库中的数据并移除任何不再需要的数据
另一种管理储存库的方法是使用反向链接(referrer)计数然后集合负责统计集合中每个入口的反向链接的数目这要求反向链接告诉集合何时会退出入口当反向链接数目为零时该元素就可以从集合中移除了
缓存
缓存是一种数据结构用于快速查找已经执行的操作的结果因此如果一个操作执行起来很慢对于常用的输入数据就可以将操作的结果缓存并在下次调用该操作时使用缓存的数据
缓存通常都是以动态方式实现的其中新的结果是在执行时添加到缓存中的典型的算法是
检查结果是否在缓存中如果在就返回结果
如果结果不在缓存中就进行计算
将计算出来的结果添加到缓存中以便以后对该操作的调用可以使用
该算法的问题(或者说是潜在的内存洩漏)出在最后一步如果调用该操作时有相当多的不同输入就将有相当多的结果存储在缓存中很明显这不是正确的方法
为了预防这种具有潜在破坏性的设计程序必须确保对于缓存所使用的内存容量有一个上限因此更好的算法是
检查结果是否在缓存中如果在就返回结果
如果结果不在缓存中就进行计算
如果缓存所占的空间过大就移除缓存最久的结果
将计算出来的结果添加到缓存中以便以后对该操作的调用可以使用
通过始终移除缓存最久的结果我们实际上进行了这样的假设在将来比起缓存最久的数据最近输入的数据更有可能用到这通常是一个不错的假设
新算法将确保缓存的容量处于预定义的内存范围之内确切的范围可能很难计算因为缓存中的对象在不断变化而且它们的引用包罗万象为缓存设置正确的大小是一项非常复杂的任务需要将所使用的内存容量与检索数据的速度加以平衡
解决这个问题的另一种方法是使用javalangrefSoftReference类跟蹤缓存中的对象这种方法保证这些引用能够被移除如果虚拟机的内存用尽而需要更多堆的话
ClassLoader
Java ClassLoader结构的使用为内存洩漏提供了许多可乘之机正是该结构本身的复杂性使ClassLoader在内存洩漏方面存在如此多的问题ClassLoader的特别之处在于它不仅涉及常规的对象引用还涉及元对象引用比如字段方法和类这意味着只要有对字段方法类或ClassLoader的对象的引用ClassLoader就会驻留在JVM中因为ClassLoader本身可以关联许多类及其静态字段所以就有许多内存被洩漏了
确定洩漏的位置
通常发生内存洩漏的第一个迹象是在应用程序中出现了OutOfMemoryError这通常发生在您最不愿意它发生的生产环境中此时几乎不能进行调试有可能是因为测试环境运行应用程序的方式与生产系统不完全相同因而导致洩漏只出现在生产中在这种情况下需要使用一些开销较低的工具来监控和查找内存洩漏还需要能够无需重启系统或修改代码就可以将这些工具连接到正在运行的系统上可能最重要的是当进行分析时需要能够断开工具而保持系统不受干扰
虽然OutOfMemoryError通常都是内存洩漏的信号但是也有可能应用程序确实正在使用这么多的内存;对于后者或者必须增加JVM可用的堆的数量或者对应用程序进行某种更改使它使用较少的内存但是在许多情况下OutOfMemoryError都是内存洩漏的信号一种查明方法是不间断地监控GC的活动确定内存使用量是否随着时间增加如果确实如此就可能发生了内存洩漏