道非道 非常道

勤思、谨言、慎行、厚积、薄发

统计

web

天圆

经济 政治 军事

键康

垃圾回收工作机制 学习自《Java程序员 上班那点事儿》

4.5 内存垃圾回收问题

那本谭浩强主编的Java入门教材说:

……

1、简单性

设计Java语言的出发点就是容易编程,不需要深奥的知识。Java语言的风格十分接近C++语言,但要比C++简单得多。Java舍弃了一些不常用的、难以理解的、容易混淆的成分,如运算符重载、多继承等。增加了自动垃圾搜集功能,用于回收不再使用的内存区域。这不但使程序易于编写,而且大大减少了由于内存分配而引发的问题。

……

这样类似的描述出现在众多的Java入门级教材中,非常容易让人们忽略了内存垃圾回收的问题,其实Java的垃圾回收问还是需要关注一下的。这个问题在招聘单位的笔试题中出现的频率也比较高,我们需要好好的研究一下Java的垃圾回收机制。

4.5.1 什么是内存垃圾,哪些内存符合垃圾的标准

我们在前面讲过了,堆是一个"运行时"数据区,是通过"new"等指令建立的,Java的堆是由Java的垃圾回收机制来负责处理的,堆是动态分配内存大小,垃圾收集器可以自动回收不再使用的内存空间。

也就是说,所谓的"内存垃圾"是指在堆上开辟的内存空间在不用的时候就变成了"垃圾"。

C++或其他程序设计语言中,必须由程序员自行声明产生和回收,否则其中的资源将消耗,造成资源的浪费甚至死机。但手工回收内存往往是一项复杂而艰巨的工作。因为要预先确定占用的内存空间是否应该被回收是非常困难的!如果一段程序不能回收内存空间,而且在程序运行时系统中又没有了可以分配的内存空间时,这段程序就只能崩溃。

Java和C++相比的优势在于,这部分"垃圾"可以被Java 虚拟机(JVM)中的一个程序发现并自动清除掉,而不用程序员自己想着"delete"了。

Java语言提供了一个系统级的线程,即垃圾收集器线程(Garbage Collection Thread),来跟踪每一块分配出去的内存空间,当JVM处于空闲循环时,自动回收每一块可以回收的内存。

4.5.1.1 垃圾回收工作机制

垃圾收集器线程它是一种低优先级的线程,它必须在一个Java程序的运行过程中出现内存空闲的时候才去进行回收处理。

垃圾收集器系统有其判断内存块是否需要回收的判断标准的。垃圾收集器完全是自动被执行的,它不能被强制执行,即使程序员能明确地判断出某一块内存应该被回收了,也不能强制执行垃圾回收程序进行垃圾回收。

程序员可以做的只有调用"System.gc()"来"建议"执行垃圾收集器程序,但是这个垃圾收集程序什么时候被执行以及是否被执行了,都是不不能控制的。但是虽然垃圾收集器是低优先级的线程,却在系统内存可用量过低时,它仍然可能会突发地执行来挽救系统。

4.5.1.2 哪些符合"垃圾"标准

如果想了解JVM的垃圾回收,就必须要知道JVM垃圾回收的标准。

垃圾收集器的"垃圾"标准:对象已经不能被程序中的其他程序所引用的时候,那么这个对象的内存空间已经没有用了。

比如当一个方法执行完毕时,在这个方法中声明的对象就超出其声明周期,这时候就可以被当作垃圾收集了,只有当这个方法被再次被调用时才会被重新创建。

例如:

……
public void function(){
OBJ obj=new OBJ();
……
}
……

另外还可以将对象的引用变量初始化为null值,也可以来暗示垃圾收集器来收集该对象。

例如:

……
OBJ obj=new OBJ();
Obj=null;
……

finalize()在该对象垃圾回收前调用

垃圾收集器跟踪每一个对象,把那些不可到达的对象占有的内存空间收集起来,并且在每次进行垃圾收集之前,垃圾收集器都会调用一下finalize()方法。Java语言允许程序员给任何对象添加finalize( )方法,但也不能过分依赖该方法对系统资源的回收和再利用,因为这个方法调用后的执行结果是不可预知的。对于任何给定对象,Java 虚拟机最多只调用一次 finalize 方法。

我们用这个程序来演示一下:

public class finalizeTest{
public static void main( String[] args ){
 finalizeTest ft=new finalizeTest();
 ft.loading();
 byte bs[]=new byte[1450000];
}
public void loading(){
 test t=new test();
 t.callme();
}
}
class test{
protected void finalize(){
System.out.println("call finalize");
}
public void callme(){
System.out.println("callme");
}
}
我们在命令行中键入如下命令:
java -Xmx1k finalizeTest
程序运行结果如图 3 10所示。
                                      

这时候,我们将JVM所许可使用的最大内存设置成"1k",当内存被占满前JVM会首先去进行内存回收,于是失去活动的"test"对象被回收,在回收前调用了"finalize()"。

4.5.2 JVM垃圾回收的相关知识

JVM使用的是分代垃圾回收的方式,主要是因为在程序运行的时候会有如下特点:

大多数对象在创建后很快就没有对象使用它了。

大多数在一直被使用的对象很少再去引用新创建的对象。

因此就将Java对象分为"年轻"对象和"年老"对象,JVM将内存堆(Heap)分为两个区域,一个是"年轻"区,另一个是"老"区,Java将这两个区域分别称作是"新生代"和"老生代"。

"新生代"区域中,绝大多数新创建的对象都存放在这个区域里,此区域一般来说较小而且垃圾回收频率较高,同时因为"新生代"采用的算法和其存放的对象的特点,使该区域垃圾回收的效率也非常高。

而"老生代"区域中存放的是在"新生代"中生存了较长时间的对象,这些对象将被转移到"老生代"区。这个区域一般要大一些而且增长的速度相对于"新生代"要慢一些,"老生代"垃圾回收的执行频率也会低很多。

由于JVM在垃圾回收处理时会消耗一定的系统资源,因此有时候通过JVM启动的时候添加相关参数来控制"新生代"区域的大小,来调整垃圾回收处理的频率非常有用。以便于我们更合理的利用系统资源。

"新生代"区域设置参数是"-Xmn",用这个参数可以制定"新生代"区域的大小。

我们来举一个例子说明:

我们就用系统自带的程序作为例子,在命令行上键入如下指令:

CD C:"java"demo"jfc"SwingSet2[回车]
C:"java"demo"jfc"SwingSet2>java -jar -verbose:gc
-Xmn4m XX:+PrintGCDetails SwingSet2.jar[回车]
上面加入了一个新的参数"XX:+PrintGCDetails",这个参数能够打印出GC的详细信息。屏幕输出如下(节选):
[GC [DefNew: 3469K->84K(3712K), 0.0007778 secs] 
23035K->19679K(28728K), 0.0009191 secs]
[GC [DefNew: 3284K->171K(3712K), 0.0007283 secs]
22878K->19766K(28728K), 0.0008669 secs]
[GC [DefNew: 3476K->260K(3712K), 0.0008504 secs]
23071K->19855K(28728K), 0.0009862 secs]
[GC [DefNew: 3502K->87K(3712K), 0.0009267 secs]
23096K->19682K(28728K), 0.0010610 secs]

我们需要解释一下输出的详细内容的意思,拿第一行输出来说:

"DefNew: 3469K->84K(3712K), 0.0007778 secs"是指"新生代"的垃圾回收情况,这里的意思是从占用3469K内存空间变为84K内存空间,用时0.0007778秒。

"23035K->19679K(28728K), 0.0009191 secs"是指总体GC的回收情况,整体堆空间占用从23035K降低到19679K的水平,用时0.0009191秒。

那么,这时候我们在将"新生代"的内存设为8M,并把堆的最大可控值设定为32M,再去执行,键入如下指令:

java -jar -verbose:gc -Xmn8m -Xmx32m 
XX:+PrintGCDetails SwingSet2.jar[回车]
得到的结果如下(节选):
[GC [DefNew: 6633K->6633K(7424K), 0.0000684 secs]
[Tenured: 18740K->18820K(24576K), 0.0636505 secs]
25374K->18820K(32000K), 0.0639274 secs]
[GC [DefNew: 6646K->6646K(7424K), 0.0002581 secs]
[Tenured: 18820K->18884K(24576K), 0.0651957 secs]
25467K->18884K(32000K), 0.0658804 secs]
[GC [DefNew: 6611K->6611K(7424K), 0.0000668 secs]
[Tenured: 18884K->18505K(24576K), 0.0931406 secs]
25496K->18505K(32000K), 0.0934295 secs]

这个结果说明:

"[DefNew: 6633K->6633K(7424K), 0.0000684 secs]"是指"新生代"的垃圾回收情况,这里的意思是从占用6633K内存空间变为6633K内存空间,用时0. 0000684秒。
"25374K->18820K(32000K), 0.0639274 secs"是指总体GC的回收情况,整体堆空间占用从25374K降低到18820K的水平,用时0. 0639274秒。
"[Tenured: 18740K->18820K(24576K), 0.0636505 secs]"是指"老生代"GC的回收情况,整体堆空间占用从18740K降低到18820K的水平,用时0.0009012秒。

通过这些参数的调整我们可以看到在处理垃圾收集问题时,从垃圾回收的频率是时间方面的变化,我们可以根据不同程序的不同情况予以调整。

最后有必要提一下GC的相关参数:

-XX:+PrintGCDetails 显示GC的详细信息
-XX:+PrintGCApplicationConcurrentTime 打印应用执行的时间
-XX:+PrintGCApplicationStoppedTime 打印应用被暂停的时间
注:":"后的"+"号表示开启此选项,如果是"-"号那么表示关闭此选项。

 

posted on 2009-07-07 13:23 星期五 阅读(364) 评论(0)  编辑  收藏 所属分类: JAVA SE


只有注册用户登录后才能发表评论。


网站导航: