第3章 垃圾收集器与内存分配策略
前言:
1、GC需要考虑回收的内存有哪些?
2、判断对象是否“死去”的算法有哪些?
3、什么是引用计数法?
4、什么是根搜索算法?
5、Java 的4种引用方式?
6、有哪些垃圾收集算法?
3.1 GC(Garbage Collection)
对于一般Java程序员开发的过程中,不需要考虑垃圾回收;但遇到内存溢出&泄露的问题,需要掌握调试&监控方法。
哪些内存需要回收?
线程共享的Java堆和方法区,其他线程私有的内存不需要考虑回收问题。
如何判定对象为垃圾对象(对象“已死”的判定算法)?
a. 引用计数法
b. 可达性分析法
如何回收垃圾对象(借助:垃圾收集算法)?
a. 回收策略(标记清除、复制、标记整理、分带收集算法)
b. 常见的垃圾回收器(Serial、Parnew、Cms、G1)
何时回收垃圾对象(不可达对象≠非死不可对象)?
3.2 对象“已死”的判定算法
由于程序计数器、Java虚拟机栈、本地方法栈都是线程独享,其占用的内存也是随线程生而生、随线程结束而回收。而Java堆和方法区则不同,线程共享,是GC的所关注的部分。
在堆中几乎存在着所有对象,GC之前需要考虑哪些对象还活着不能回收,哪些对象已经死去可以回收。
有两种算法可以判定对象是否存活:
引用计数算法:给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。但是它很难解决两个对象之间相互循环引用的情况。)
可达性分析算法(Jvm采用此方式):通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明此对象已死、可回收。
3.2.1 引用计数算法
3.2.2 可达性分析算法
在主流的商用程序语言(如我们的Java)的主流实现中,都是通过可达性分析算法来判定对象是否存活的。GCRoots的引用链,图示如下:
Java中可以作为GC Roots的对象包括:
虚拟机栈中引用的对象;
本地方法栈中Native方法引用的对象;
方法区静态属性引用的对象;
方法区常量引用的对象。
3.2.3 再谈引用:
强引用,类似 Object obj = new Object()这类引用,只要强引用还存在垃圾收集器永远不会收集掉被引用的对象。
软引用,在系统将要发生内存溢出异常之前将会把这些对象列入回收范围之中进行第二次回收。JDK1.2之后提供了SoftReference类来实现软引用。
弱引用,被弱引用关联的对象只能生存到下一次垃圾收集之前,无论当前内存是否足够,都会回收掉被弱引用关联的对象。
虚引用,不会对对象的生存时间造成影响,也无法通过虚引用来取得对象的实例,虚引用的作用是当对象被回收的时候收到一个系统通知。
3.2.4 对象Live or Dead?
对象判为死刑需要两次标记(不可达对象≠非死不可对象):
1、没有与GC Roots相连的引用链。
2、首先判断虚拟机是否需要执行finalize()方法,如果对象没用覆盖finalize方法或者finalize被虚拟机调用过,那么虚拟机都没必要执行finalize方法。否则对象被放置在一个叫做F-Queue的队列中,稍后虚拟机建立一个低优先级的Finalizer线程去执行finalize方法。如果在finalize方法里对象重新与GC Roots关联上,那么对象便复活。可以说finalize方法是对象逃脱死亡命运的最后一次机会,这种机会只有一次,因为同一个对象的finalize最多只会被虚拟机自动调用一次。
3.2.5 回收方法区
主要回收废弃常量和无用的类。
废弃常量:即没有任何地方引用这个常量,没有任何地方引用这个字面量。
无用类条件:(同时满足以下三个条件,类可以被回收):
1、该类的所有实例都已被回收。
2、加载该类的classLoader已被回收。
3、对应的class对象没有任何地方引用,没法通过反射访问该类。
3.3 垃圾收集算法
标记-清理法(Mark-Sweep):
复制算法(Copying):–适合“新生代”
标记整理算法(Mark-Compact):–适合“老年代”
分代收集算法(Generational Collection):商业JVM均采用此方式。
a. 标记-清理法:
首先对标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
标记和清除效率都比较低,还有就是会产生大量的空间碎片。
b. 复制算法:
将可用内存划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完后,就将还在着的对象复制到另外一块上,然后再把已使用的过的内存空间一次清理掉。
实现简单,运行高效,但是内存缩小了一半。常用于存活对象少的新生代。
c. 标记整理算法:
首先对需要回收的对象进行标记,对存活的对象都向一端移动;
然后直接清理掉端边界以外的内存。
d. 分代收集算法:
根据对象存活周期的不同将内存划分为几块,一般将Java堆分为新生代和老年代,这样可以根据各个年代的特点采用最适当的收集算法。
新生代每次垃圾收集有大量的对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代中因为对象存活率高,没有额外的空间对它进行分配担保,就必须使用标记清理或者标记整理算法来进行回收。
3.4 Hotspot收集算法实现
枚举根节点(GC Roots):
在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到直接获取那些地方存在着对象引用。
在OopMap的帮助下可以准确的完成GC Roots枚举。
安全点(Safe Point):
HotSpot在特定的位置记录了这些信息,生成了对弈的OopMap,这些位置称为安全点。
让所有的线程到达安全点的方式有抢占式中断和主动式中断。
抢占式中断:在GC发生时中断所有线程,如果线程不再安全点上就恢复线程直到到达安全点。
主动式中断(JVM采用此方式):对线程设置一个标志,各个线程主动去轮询这个标志,轮询标志和这个安全点重合,发现中断标志就中断挂起。
安全区域(Safe Region,扩展的Safe Point):
如果程序没有分配CPU时间,线程处于睡眠或者阻塞状态,这时候线程无法响应JVM的中断请求,这种情况就需要安全区域爱解决。安全区域是指一段代码之中引用不会发生变化。
在线程执行到安全区域的时候首先标识自己已进入安全区域,在GC时无需管标识自己为安全区域的线程,在线程要离开的时候要检查是否已经完成根节点枚举,如果完成就继续执行,否则必须受到可以离开的信号才可以离开。
3.5、垃圾收集器
收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。如果收集器中存在连线则说明收集器可以搭配使用。
–参照原文,略。
3.6 内存分配和回收策略
Java自动内存管理,分为两方面:
给对象分配内存(自动):new instance时;
回收给对象的内存(自动):gc instance时;
为什么要将堆内存分区?
对于一个大型的系统,当创建的对象及方法变量比较多时,即堆内存中的对象比较多,如果逐一分析对象是否该回收,效率很低。分区是为了进行模块化管理,管理不同的对象及变量,以提高 JVM 的执行效率。
堆内存分为哪几块?
Young Generation Space 新生区(也称新生代)
Tenure Generation Space养老区(也称旧生代)
Permanent Space 永久存储区
Minor GC 和 Full GC有什么区别?
Minor GC:新生代 GC,指发生在新生代的垃圾收集动作,因为 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般回收速度较快。
Full GC:老年代 GC,也叫 Major GC,速度一般比 Minor GC 慢 10 倍以上。
内存分配有哪些原则?
对象优先分配在 Eden(新生代内存):(原因:基于对象朝生夕死特性,便于频繁GC)
大对象直接进入老年代:(原因:避免大对象在Eden 和 2个Survivor之间来回拷贝)
长期存活的对象将进入老年代:(原因:老年代GC不频繁的特性)
动态对象年龄判定:不是固定依据门槛(比如15个 Minor GC周期)判定,改为动态判定。
空间分配担保:复制算法(Eden->Survivor内存不足场景时),拷贝到老年代内存中。
声明:本站部分文章内容及图片转载于互联 、内容不代表本站观点,如有内容涉及侵权,请您立即联系本站处理,非常感谢!