JVM垃圾回收总结

📅 2026/7/28 15:05:44
JVM垃圾回收总结
垃圾回收包含的内容不少但顺着下面的顺序捋清知识也并不难。首先要搞清垃圾回收的范围栈需要GC去回收吗然后就是回收的前提条件如何判断一个对象已经可以被回收这里只重点学习根搜索算法就行了之后便是建立在根搜索基础上的三种回收策略最后便是JVM中对这三种策略的具体实现。1.范围要回收哪些区域Java 方法栈本地方法区以及PC计数器随方法或线程的结束而自然被回收所以这些区域不需要考虑回收问题。 Java堆和方法区是GC回收的重点区域因为一个接口的多个实现类需要的内存不一样一个方法的多个分支需要的内存可能也不一样而这两个区域又对立于栈可能随时都会有对象不再被引用因此这部分内存的分配和回收都是动态的。2.前提如何判断对象已死1引用计数法引用计数法就是通过一个计数器记录该对象被引用的次数方法简单高效。但是解决不了循环引用的问题。比如对象A包含指向对象B的引用对象B也包含指向对象A的引用但没有引用指向A和B这时当前回收如果采用的是引用计数法那么对象A和B的被引用次数都为1都不会被回收。下面是循环引用的例子在Hotspot JVM下可以被正常回收可以证实JVM采用的不是简单的引用计数法。通过-XX:PrintGCDetails输出GC日志。package 垃圾回收; public class 循环引用的例子 { final static int MB1024*1024; byte[] sizenew byte[2*MB]; Object ref; public static void main(String[] args) { 循环引用的例子 objA new 循环引用的例子(); 循环引用的例子 objB new 循环引用的例子(); objA.refobjB; objB.refobjA; objAnull; objBnull; System.gc(); System.gc(); } }查看GC日志[Full GC (System) [Tenured: 2048K-366K(10944K), 0.0046272 secs] 4604K-366K(15872K), [Perm : 154K-154K(12288K)], 0.0046751 secs] [Times: user0.02 sys0.00, real0.00 secs]2根搜索通过选取一些根对象作为起始点开始向下搜索如果一个对象到根对象不可达时则说明此对象已经没有被引用是可以被回收的。可以作为根的对象有栈中变量引用的对象类静态属性引用的对象常量引用的对象等。因为每个线程都有一个栈所以我们需要选取多个根对象。附对象复活在根搜索中得到的不可达对象并不是立即就被标记成可回收的而是先进行一次标记放入F-Queue等待执行对象的finalize() 方法执行后GC进行二次标记复活的对象之后将不会被回收。因此是对象复活的唯一办法就是重写finalize() 方法并使对象重新被引用。例子package 垃圾回收; public class DeadToRebirth { private static DeadToRebirth hook; Override protected void finalize() throws Throwable { super.finalize(); DeadToRebirth.hookthis; } public static void main(String[] args) throws InterruptedException { DeadToRebirth.hook new DeadToRebirth(); DeadToRebirth.hooknull; System.gc(); Thread.sleep(500); if (DeadToRebirth.hook!null){ System.out.println(Rebirth!); } else { System.out.println(Dead!); } DeadToRebirth.hooknull; System.gc(); Thread.sleep(500); if (DeadToRebirth.hook!null){ System.out.println(Rebirth!); } else { System.out.println(Dead!); } } }运行结果要注意的两点是第一finalize() 方法只会被执行一次所以对象只有一次复活的机会。第二执行GC后,要停顿半秒等待优先级很低的finalize() 执行完毕。3.策略垃圾回收的算法1标记-清除没错这里的标记指的就是之前我们介绍过的两次标记过程。标记完成后就可以对标记为垃圾的对象进行回收了。怎么样简单吧。但是这种策略的缺点很明显回收后内存碎片很多如果之后程序运行时申请大内存可能会又导致一次GC。虽然缺点明显这种策略却是后两种策略的基础。正因为它的缺点所以促成了后两种策略的产生。2标记-复制 新生代采用将内存分为两块标记完成开始回收时将一块内存中保留的对象全部复制到另一块空闲内存中。实现起来也很简单当大部分对象都被回收时这种策略也很高效。但这种策略也有缺点可用内存变为一半了怎样解决呢聪明的程序员们总是办法多过问题的。可以将堆不按1:1的比例分离而是按8:1:1分成一块Eden和两小块Survivor区每次将Eden和Survivor中存活的对象复制到另一块空闲的Survivor中。这三块区域并不是堆的全部(一个Eden 和两个Survivor )而是构成了新生代。从下图可以看到这三块区域如何配合完成GC的具体的对象空间分配以及晋升请参加后面第6条补充。为什么不是全部呢如果回收时空闲的那一小块Survivor不够用了怎么办这就是老年代的用处。当不够用时这些对象将直接通过分配担保机制进入老年代。那么老年代也使用标记-复制策略吧当然不行老年代中的对象可不像新生代中的每次回收都会清除掉大部分。如果贸然采用复制的策略老年代的回收效率可想而知3标记-整理根据老年代的特点采用回收掉垃圾对象后对内存进行整理的策略再合适不过将所有存活下来的对象都向一端移动。新生代基本采用标记复制算法老年代采用标记整理算法cms采用标记清除。4.实现虚拟机中的收集器1新生代上的GC实现Serial单线程的收集器只使用一个线程进行收集并且收集时会暂停其他所有工作线程Stop the world。它是Client模式下的默认新生代收集器。ParNewSerial收集器的多线程版本。在单CPU甚至两个CPU的环境下由于线程交互的开销无法保证性能超越Serial收集器。Parallel Scavenge也是多线程收集器与ParNew的区别是它是吞吐量优先收集器。吞吐量运行用户代码时间/(运行用户代码垃圾收集时间)。另一点区别是配置-XX:UseAdaptiveSizePolicy后虚拟机会自动调整Eden/Survivor等参数来提供用户所需的吞吐量。我们需要配置的就是内存大小-Xmx和吞吐量GCTimeRatio。2老年代上的GC实现Serial OldSerial收集器的老年代版本。Parallel OldParallel Scavenge的老年代版本。此前如果新生代采用PS GC的话老年代只有Serial Old能与之配合。现在有了Parallel Old与之配合可以在注重吞吐量及CPU资源敏感的场合使用了。CMS采用的是标记-清除而非标记-整理是一款并发低停顿的收集器。但是由于采用标记-清除内存碎片问题不可避免。可以使用-XX:CMSFullGCsBeforeCompaction设置执行几次CMS回收后跟着来一次内存碎片整理。5.触发何时开始GCMinor GC(新生代回收)的触发条件比较简单Eden空间不足就开始进行Minor GC回收新生代。而Full GC老年代回收一般伴随一次Minor GC则有几种触发条件1老年代空间不足2PermSpace空间不足3统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间这里注意一点PermSpace并不等同于方法区只不过是Hotspot JVM用PermSpace来实现方法区而已有些虚拟机没有PermSpace而用其他机制来实现方法区。6.补充对象的空间分配和晋升1对象优先在Eden上分配。2大对象直接进入老年代虚拟机提供了 -XX:PretenureSizeThreshold参数大于这个参数值的对象则直接分配到老年代中。因为新生代采用的是标记-复制策略在Eden中分配大对象将会大致Eden 区和两个Survivor区之间大量的内存拷贝。3长期存活的对象将进入老年代对象在Survivor区中每熬过一次Minor GC年龄就增加1岁当它的年龄增加到一定程度默认为15岁时就会晋升到老年代中。转载自https://blog.csdn.net/dc_726/article/details/7934101#