Java垃圾回收机制相对于C++的巨大改进,避免了因为程序员忘记释放内存而造成的内存溢出错误。
背景
Java除了8种基本类型,其它都是引用类型。Java运行时的数据存储区有堆(Heap)和栈(Stack),一般栈中存放非static自动变量、函数参数、表达式临时结果和函数返回值,还有对象类型的(指针)句柄,栈中的实体数据的分配和释放均是由系统自动完成的。一般堆中存放对象数据类型,堆中的实体数据是由程序显示分配的,开发者只需要在用堆分配的时候创建就行了,何时释放如何释放,都由JVM来做,不需要程序代码来显示的释放。
两种回收策略
早期实现GC(Garbage Collection)的是引用计数机制,当有新的指向该对象的引用时,计数器加1,引用移除时,计数器减1,当计数器为0时,认为可以进行垃圾回收。但是这种方式当出现循环引用的时候就不行了。
JVM的处理有两种机制:
- “mark and sweep标记清除”这种机制下,每个对象都有标记信息,用于表示该对象是否可达。当垃圾回收时,Java程序暂停运行。JVM从根(ROOT)出发,找到可达对象,并标记(mark),随后,JVM需要扫描整个堆,找到剩余对象,并清空这些对象所占据的内存空间。
- “copy and sweep复制清除”这种机制下,堆被分为两个区域,对象总存活于两个区域中的一个。当垃圾回收启动时,Java程序暂停运行,JVM从根(ROOT)出发,找到可达对象,将可到达的对象复制到空白区域中并紧密排列,修改由于对象移动所造成的引用地址变化。最后,直接清空对象原先存活的整个区域,使其成为新的空白区域。
PS. 可以看到,”copy and sweep”需要更加复杂的操作,但也让对象可以紧密排列,避免”mark and sweep”中可能出现的空隙。在新建对象时,”copy and sweep”可以提供大块的连续空间。因此,如果对象都比较”长寿”,那么适用于”mark and sweep”。如果对象的”新陈代谢”比较活跃,那么适用于”copy and sweep”。
分代回收
JVM中上面两种机制是通过分代回收(generational collection)混合在一起的。每个对象记录有它的世代(generation)信息。所谓的世代,是指该对象所经历的垃圾回收的次数。世代越久远的对象,在内存中存活的时间越久。根据对Java程序的统计观察,世代越久的对象,越不可能被垃圾回收(富人越富,穷人越穷)。因此,当我们在垃圾回收时,要更多关注那些年轻的对象。
GC分代机制
其中的永久世代(permanent generation)中存活的是Class对象。这些对象不会被垃圾回收(在Java8中已经移除了永久代,新加了一个叫元数据区的native内存区)。年轻世代(young generation)和成熟世代(tenured generation)需要进行垃圾回收。年轻世代中的对象世代较近,而成熟世代中的对象世代较久。
年轻世代又进一步分为三个区域:eden(伊甸): 新生对象存活于该区域。新生对象指从上次GC后新建的对象。from(survivor1),to(survivor2):这两个区域大小相等,相当于copy and sweep中的两个区域。
当新建对象无法放入eden区时,将出发minor collection。JVM采用copy and sweep的策略,将eden区与from区的可到达对象复制到to区。经过一次垃圾回收,eden区和from区清空,to区中则紧密的存放着存活对象。随后,from区成为新的to区, to区成为新的from区。如果进行minor collection的时候,发现to区放不下,则将部分对象放入成熟世代。另一方面,即使to区没有满,JVM依然会移动世代足够久远的对象(15次拷贝后任然存在的对象)到成熟世代。如果成熟世代放满对象,无法移入新的对象,那么将触发major collection。JVM采用mark and sweep的策略,对成熟世代进行垃圾回收。
如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。
JVM对于循环引用的垃圾回收:如果其他所有对象都没有引用这两个对象,即使这两个对象相互引用,也会被GC因为JVM是从一个根对象开始查找引用的,没有任何路径可以被根对象引用的闭环也会被GC的。
补充
java内存溢出的原因:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体。