为何要进行垃圾收集?
使用Java语言编写程序,在大部分情况下都不需要程序员自己考虑内存的分配与回收,只需要向JVM申请空间即可。有句话说的好:“哪有什么岁月静好,只是有人在替你负重前行罢了!”。JVM替程序员干了这个活了,由于内存空间有限,而一些对象使用了之后就不会再使用了,若不对这些对象占据的内存进行回收,内存吃紧后势必会造成内存溢出(包括但不限于Java堆溢出)。
垃圾收集干了什么?
垃圾收集就是将不再需要的对象占据的空间给回收,腾出空间给新的对象使用。
在Java中如果想要操作一个对象,一般是通过指向这个对象的引用来完成。在栈空间中也只会保存指向储存在java堆中的对象实例的引用。如果没有任何的引用指向这个实例,那么这个实例就可以被宣判‘死亡’了。因为这个时候已经没有办法再使用这个实例。
对于int、float等基本数据类型如果是属于临时变量则在方法退出时就会被回收,它们占据的是栈空间。一种特殊但也很常见的情况:在循环体内部申请的变量,它们对于循环体外是不可见的,对于下一个循环也是不可见的。即你不可以在循环体外部访问这个对象也不可以在下一个循环中访问这个对象,除非在循环体外部保存其引用。
public static void main(String[] args){
while (true){
long a=100L;
}
}
以上的代码并不会造成栈内存溢出,说明在进入下一个循环时,这个a变量就已经失效了。
以上说的是基本数据类型,它们存在于栈空间中。但在java堆中的对象实例数据显然不会因为栈帧出栈就被回收了。它们需要被标记为‘死亡’状态才会被垃圾收集器回收。
如何判断对象已‘死亡’?
在之前就提到,当没有任何引用指向这个对象时它就已经名副其实的’死亡’了。那么如何知道这个对象没有任何引用指向它了呢?有两个经典的方案:
- 引用计数法
- 可达性分析
引用计数法
即在示例数据中保存一个计数器,每当有新的引用指向它时就将计数器+1,当引用失效时就将它-1。但是单纯采取此方法并不够完善,列如:当两个对象互相保存了对方的引用时,由于计数器都为2(本身加上另一个对象里保存的引用),此时并不会回收。
这个时候如果我们将AB均置空时则会发现,已经无法在使用这两个对象了,但这两个对象的计数器仍然不为0,如果按照简单的计数器规则也就意味着它们不会被回收。
所以,在使用引用计数器方法时还需要搭配其它的规则来使用。
可达性分析法
创建一个根节点集合(GC roots),所有创建的新对象一开始都可以通过根节点集合里的元素向下遍历到。随着引用关系的变化,有一部分对象就无法到达了,这些无法到达的元素就可以被判定‘死亡’。
可作为GC roots的对象:
- 在虚拟机栈(本地变量表)中引用的对象。
- 在方法区的静态变量引用的对象。
- 方法区中常量引用的对象,如字符串常量池里的引用。
- 本地方法栈中本地方法引用的对象。
- JVM内部的引用。如系统类加载器、常驻异常对象、基本数据类型对应的Class对象。
- 所有被同步锁持有的对象
- 反映JVM内部情况的JMXBean、本地代码缓存等。
在JVM中很多都对对象划分了’代’有老年代和新生代。为了提高回收效率于是将它们放在不同的区域。针对某一代的对象进行回收时,可能会碰到跨’代’引用,这在’观察者模式’中非常常见,被观察者通常处在老年代中,而每当新对象需要观察它时便出现了跨代引用。为了正确回收这些对象,必须将这些关联起来的对象一起临时性的加入GC roots,否则很有可能出现错误。
当然不论引用计数法还是可达性分析法都各有其优势所在。在目前主流的JVM中一般都支持第二种方案。
方法区内存回收
JVM的内存架构中分为虚拟机栈、本地方法栈、程序计数器、堆、方法区。对于虚拟机栈以及本地方法栈的回收都很简单,出了栈自然会被回收。而程序计数器显然是不会被回收的,堆内存的回收之前就说了,那么还剩下一个方法区内存的回收。要讨论对它的回收首先就得了解,方法区的内存是用来保存什么的。
方法区保存的数据:
- 类型信息
- 类型常量池
- 字段信息
- 方法信息
- 类变量即静态变量
- 指向类加载器的引用
- 指向Class实例的引用
- 方法表
由以上就可以看出来,方法区保存的是有关类的数据,所以方法区的回收效率是十分低的,判断一个类已经失效的条件十分苛刻。需要满足以下三个条件:
- 该类的所有实例以及派生子类的实例均已经被回收。
- 该类的类加载器已经被回收。
- 该类的Class对象没有在任何地方被引用。
这三个条件很难达成,特别是第二条,除非在可替换类加载器的场景中,类加载器通常不会被回收。所以,在JVM规范中并不强制要求对这个区域进行回收,但是为了降低方法区在经常出现新的类的场景中的内存压力如动态生成JSP、OSGI等,通常需要JVM实现类卸载的功能。
OK,下一篇开始总结垃圾收集算法。