1.垃圾回收的意义
在C++中,new出来的对象所占用的内存(堆)在程序运行结束之前一直被占用着,这就导致这块内存不能被其他对象使用;而在java中,当new 出来的对象没有被引用变量引用时,这个对象所占用的内存将会成为垃圾。JVM的一个系统级线程会自动释放该内存。垃圾回收就意味着程序不再需要的对象将会是“无用信息”,这些信息将被丢弃。当一个对象不再被使用时,JVM会回收该对象所占用的内存,以便这块内存给后来的新对象使用。事实上,除了释放没用的对象,拉圾回收也可以清除“内存记录碎片”。由于new一个对象和JVM回收释放丢弃该对象时,内存中会出现碎片。
碎片是分配给对象的内存块之间的空闲内存洞(即用于分隔出各个内存块)。碎片整理将所占用的堆内存移动堆的一端,JVM将整理出来的内存分配给新的对象。
java JVM优点:能自动释放内存空间,减轻编程的负担。
java JVM缺点:1、垃圾回收会影响程序的性能,java JVM必须追踪运行程序中所有的对象,而且最终要释放没用的对象。这一过程需要花费时间处理。
2、垃圾回收算法不完备性,不能保证100%收集到所有的废弃堆内存。
2.垃圾收集的算法分析
垃圾回收算法一般都是干2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该内存能被程序再次使用
大多数垃圾回收算法使用了根集(root set)这个概念;所谓根集就是正在运行的java程序能访问的引用变量集合。程序可以使用引用变量访问对象的属性和方法。垃圾回收首先需要确定从根开始知道哪些是可达的(可达对象都是活动对象,它们不能作为垃圾被回收),哪些是不可达的(即没有引用变量引用的对象,已沦为垃圾)。以下为几个常用的算法。
2.1引用计数法(是从对象为出发点)
引用计数法是唯一没有使用根集的垃圾回收法,该算法使用“引用计数器来区分存活对象和不再使用对象”。一般来说,堆中的每个对象对应一个引用计数器。每当创建一个对象并赋值给一个变量时,引用计数器置为1.当对象被赋给任意变量时,引用计数器每次都加1,当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。
优点:基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行
缺点:引用计数器增加了程序执行的开销,因为每次对象赋给新的变量,计数器就加1,每次出了作用域就减1.
2.2 tracing算法(Tracing Collector 是从引用变量集合为出发点,即根集)
tracing(追踪)算法是为了解决引用计数法的问题而提出的,它使用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。在扫描识别过程中,基于tracing算法的垃圾收集也称为“标记和清除(mark-and-sweep)”垃圾收集器
2.3compacting算法(Compacting Collector,基于tracing基础上,移动了对象)
为了解决堆碎片问题,基于tracing的垃圾回收吸收了compacting(压缩)算法的思想,在清除的过程中,算法将所有的对象移到堆的一端,堆的另一端就变成了相邻的空闲内存区,收集器会对它移动的所有对象的所有引用进行更新,使得这些引用 在新的位置能识别原来的对象。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。
注:句柄是一个整数值,即一个4字节(64位程序中为8字节)长的数值,是整个windows系统中用于对应用程序的唯一标识符,类似id,以标识应用程序中的不同对象和同类对象中的不同的实例(对象),如一个窗口,按钮,图标,流动条等,应用程序能够通过句柄访问相应对象的信息。但句柄不是一个指针。
句柄的由来:windows之所以要设立句柄,根本上内存管理机制的问题--虚拟机。简而言之数据的地址需要变动,变动以后就需要有人来记录管理变动(就好像户籍管理一样),因此系统用句柄来记载数据地址的变更。
2.4 copying算法(Coping Collector)
该算法的提出是为了克服句柄的开销,以及解决堆碎片的垃圾回收。它开始时把堆分成一个对和多个空闲区,程序从对象区为对象分配空间,当对象满了,基于coping算法的垃圾回收就从根集中扫描活动对象,并将每个活动对象复制到空闲区(使得活动对象所占的内存之间没有空闲间隔),这样空闲区变成了对象区,原来的对象区变成了空闲区,程序会在新的对象区中分配内存。
一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象区与空闲区,在对象区与空闲区的切换的切换过程中,程序暂停执行。
2.5 generation算法(Generation Collector)
stop-and-copy垃圾收集器的一个缺陷是收集器必须复制所有的活动对象,这增加了程序等待时间,这是coping算法低效的原因。在程序设计中有这样的规律:多数对象存在的时间比较短,少数的存在时间比较长,因此,generation算法将堆分成2个或多个,每个子堆作为对象的一代(generation)。由于多数对象存在的时间比较短,随着程序丢弃不使用的对象,垃圾收集器将从最年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行存活下来的对象移到下一高代的子堆中,由于老一代的子堆不会经常被回收,因而节省了时间。
2.6 adaptive算法(Adaptive Collector)
在特定的情况下,一些垃圾收集算会优于其他算法,基于Adaptive算法的垃圾收集器就是监控当前堆的做事情情况,并将选择适当算法的垃圾收集器
3 System.gc()方法
命令行参数透视垃圾收集器的运行
不管JVM使用的是哪种垃圾回收的算法,都可调用System.gc(),将在调用System.gc()之前创建出来的对象实例进行垃圾回收。可在命令行使用参数-verbosegc查看java使用的堆内存的情况。它的格式如下:
1)cd 到程序的 bin目录下
2)java -verbosegc + 包名.classFile
如:
在命令行中运行如图所示:
在这个例子中,一个新的对象被创建,由于它没有使用,所以该对象迅速的变为不可达,程序编译后,执行命令:
java -verbosegc Demo.GcDemo后结果为:
[GC 281K->256K(53824K), 0.0009978 secs]
[Full GC 256K->161K(53824K), 0.0074452 secs]
[Full GC 256K->161K(53824K), 0.0074452 secs]
注:箭头前后的数据281k与256k分别表示垃圾收集GC前后所有存活对象使用的内存容量,说明有281k-256k=25k的内存被释放出来了,括号内的53824K为堆内存的总容量,收集所需要花的时间是0.0074452秒。(这个时间在每次执行的时候都有所不同)
需要注意的是:调用System.gc()也仅仅是一个请求(建议)。JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。
4. finalize()方法
在JVM垃圾回收器收集一个对象之前,一般要求程序调用适当的方法释放资源,但在没有明确释放资源的情况下,Java提供了缺省机制来终止该对象释放资源,这个方法就是finalize()(完成,结束),它的原型为:
protected void finalize() throws Trowable
在finalize()方法返回之后,对象消失,垃圾收集开始执行。原型中的throw Throwable表示它可以抛出任何类型的异常。
之所以要使用finalize(),是存在着垃圾回收器不能处理的特殊情况。假定你的对象(并非是用new创建出来的对象实例),而是通过方法,或其他途径获取的对象,此对象会占一块“特殊”的内存区域,那么这个时候java允许在类中定位一个finalize()方法。
特殊的区域例如:
1) 由于在分配内存的时候可能采用了类似C语言的做法,而非Java通常用的new做法,这种情况主要发生在native method中,比如native method调用了 c/c++方法malloc()函数系列来分配存储空间,但是除非调用了free()函数,否则这些内存空间将不会得到释放,那么这个时候就可能造成内存泄漏。但是由于free()方法是在c/c++中的函数,所以finalize()中可以用本地方法来调用它,以释放这些“特殊”的内存空间。
2)又或者打开的文件资源,这些资源不属于垃圾回收器的回收范围。
内存泄漏:当创建出来的对象实例没有被引用变量引用时,此时该对象被沉为垃圾,但对象又没被回收掉,该现像称为内存泄漏。内存泄漏bug一般存在于“打开的文件资源”,没有关闭流,以及非new创建出来的对象没有调用native method调用finalize()方法。
换言之,finalize()的主要用途是释放一些其他做法开辟的内存空间,因为在JAVA中没有提供像“析构”函数或者类似概念的函数,要做一些类似清理工作的时候,必须自己动手创建一个执行清理工作的普通方法,也就是override Object这个类中的finalize()方法。例如,假设某一个对象在创建过程中会将自己绘制到屏幕上,如果不是明确的从屏幕上将其擦掉,它可能永远都不会被清理。如果在finalize()加入某一种擦除功能,当GC工作时,finalize()得到了调用,图像就会被擦除。要是GC没有发生,那么这个图像就会一直被保存下来。
一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用finalize()方法进行一些必要的清理工作。只有到下一次再进行垃圾回收动作时,才会真正释放这个对象所占用的内存空间。