当前位置: 减弱器 >> 减弱器前景 >> 解析JVM之垃圾收集篇,超详细计算机
写在前面:
小伙伴儿们,大家好!上一次我们了解了JVM基础知识——全面解析JVM,超详细!今天来学习JVM垃圾回收相关内容,作为面试必问的知识点,来深入了解一波!
思维导图:
1,判断对象是否死亡
我们在进行垃圾回收(GarbageCollection,简称GC)之前肯定要先判断哪些是垃圾。
在堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再被任何途径使用的对象)。
对象死亡
1.1,引用计数算法
给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器+1,当引用失效时,计数器-1,任何时候当计数器为0的时候,该对象不再被引用。
引用计数器这个方法实现简单,判定效率也高。但是,当前主流的虚拟机都没有采用这个算法来管理内存,其中最主要的原因是它很难解决对象之间互相循环引用的问题。
所谓对象之间互相循环引用,如下面代码所示:除了对象objA和objB相互引用着对方之外,这两个对象之间再无任何引用。但是它们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知GC回收器回收他们。
1.2,可达性分析算法
这个算法的基本思想就是通过一系列的称为“GCRoots”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连的话,则证明此对象是不可用的。
可达性分析算法
在Java语言中,可作为GCRoots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象本地方法栈(Native方法)中引用的对象
2,再谈引用
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。
JDK1.2之前,Java中引用的定义很传统:如果
reference
类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)。
2.1,强引用
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。类似于“Objectobj=newObject()”这类的引用,如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出
OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2.2,软引用
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。
2.3,弱引用
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
2.4,虚引用
虚引用顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在世纪程序设计中一般很少使用弱引用与虚引用,使用软用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
3,废弃常量以及无用类
3.1,如何判断一个常量是废弃常量?
运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?
假如在常量池中存在字符串abc,如果当前没有任何String对象引用该字符串常量的话,就说明常量abc就是废弃常量,如果这时发生内存回收的话而且有必要的话,abc就会被系统清理出常量池。
3.2,如何判断一个类是无用的类?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。方法区主要回收无用的类,类需要同时满足下面3个条件才能算是“无用的类”:
该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。加载该类的ClassLoader已经被回收。该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
4,垃圾收集算法
4.1,标记--清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
效率问题:标记和清除两个过程的效率都不高;空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-清除算法
4.2,复制算法
为了解决效率问题,“复制”收集算法出现了。它将可用内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。实现简单,运行高效。
复制算法
4.3,标记--整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
标记-整理算法
4.4,分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
5,垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的HotSpot虚拟机就不会实现那么多不同的垃圾收集器了。
常见的垃圾收集器
5.1,Serial收集器
Serial收集器是最基本、历史最悠久的垃圾收集器了。从名字上看是串行的意思,这个收集器是一个单线程的新生代收集器。它的“单线程”的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(StopTheWorld),直到它收集结束。
Serial收集器
Serial采取“复制算法”实现,如果是在单CPU环境下,Serial收集器没有线程交互的开销,理论上是可以获得最高的单线程执行效率,STW的时间也可以控制在几十到几百毫秒内,这个时间是完全可以接受的。
与其他单线程收集器相比它的优点就是:它简单而高效(与其他收集器的单线程相比)。简单而高效Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是个不错的选择。
5.2,ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。ParNew收集器虽然有多线程优势,但在单CPU和多CPU环境下,效果并不一定会比Serial好,至少在单CPU环境下是肯定不如的Serial的。由于线程交互开销的时间,效果并不如人意,多线程的好处在于更高效率地利用CPU,提高CPU的吞吐量,让CPU空闲的时间减少。
新生代采用复制算法,老年代采用标记-整理算法。
ParNew收集器它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。5.3,ParallelScavenge收集器
ParallelScavenge收集器也是使用复制算法的多线程收集器,它看上去几乎和ParNew都一样。那么它有什么特别之处呢?
ParallelScavenge收集器
转载请注明:http://www.aideyishus.com/lkcf/2295.html