目录
[toc]
1.1垃圾收集器
1.1.1哪些内存需要回收
Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随着线程而生,随线程而灭,在这几个区域内就不需要过多的考虑垃圾回收,因为方法结束或者线程结束,内存自然而然的随着回收了。而Java堆和方法区不一样,这部分的内存和回收都是动态的。垃圾收集器所关注的主要是这部分内存。
1.1.2什么时候回收
要确定哪些内存需要回收,就需要知道占用内存的对象是否还存活。通常有两种方法去判断:
1.引用计数法(Reference Counting)
给对象中添加一个引用计数器,每一个地方引用它,计数器数值+1,当引用失效时,数值-1.当计数器数值为0时,代表对象不可能再被引用,这时候就认为对象已死。但是,主流的java虚拟机没有选用引用计数算法来管理内存,最主要的原因是它无法解决相互循环引用的问题。
循环引用例子:如下代码testGC()方法中,对象objA和对象objB都有字段instance,赋值令objA.instance=objB;objB.instance=objA
除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是他们因为互相引用者对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。
public class ReferenceCountingGC {
public Object instance=null;
private final int _1Mb=1024*1024;
private byte[] bigSize = new byte[_1Mb*2];
public static void testGC(){
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance=objB;
objB.instance=objA;
objA = null;
objB = null;
//假设此处发生gc
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
运行结果:
[GC (System.gc()) [PSYoungGen: 9339K->696K(76288K)] 9339K->704K(251392K), 0.0007566 secs] [Times: user=0.00 >sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 696K->0K(76288K)] [ParOldGen: 8K->643K(175104K)] 704K->643K(251392K), [Metaspace: 3480K->3480K(1056768K)], 0.0041741 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 76288K, used 655K [0x000000076af80000, 0x0000000770480000, 0x00000007c0000000)
eden space 65536K, 1% used [0x000000076af80000,0x000000076b023ee8,0x000000076ef80000)
from space 10752K, 0% used [0x000000076ef80000,0x000000076ef80000,0x000000076fa00000)
to space 10752K, 0% used [0x000000076fa00000,0x000000076fa00000,0x0000000770480000)
ParOldGen total 175104K, used 643K [0x00000006c0e00000, 0x00000006cb900000, 0x000000076af80000)
object space 175104K, 0% used [0x00000006c0e00000,0x00000006c0ea0f58,0x00000006cb900000)
Metaspace used 3487K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
从结果可以清楚的看到,GC日志中9339K->704K,意味着已经回收了它们,假若虚拟机用的是引用计数法,根本不会回收,因为此时引用计数器的值都不为0.说明虚拟机不是通过引用计数法来判断对象是否存活的。
2.可达性分析算法
主流的商用程序语言,java/c#的主流视线中,都是通过可达性分析算法来判断对象是否存活的,这个算法通过一系列的称为"GC Roots"的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,即此对象不可达,则证明此对象是不可用的。
GC Roots
包含下面几种:
- 虚拟机栈(栈中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般的Native方法)引用的对象
引用
判断对象的存活,都与引用有关。定义为:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表着一个引用。
引用类型:
- 强引用:在程序中普遍存在,如‘Object a = new Object()’这类的引用,只要强引用还存在垃圾回收器永远不会回收掉被引用的对象
- 软引用:描述一些还有用但是非必需的对象。可以用SoftReference类来实现软引用。对于软引用相关联的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围内进行第二次回收。
- 弱引用:描述非必需对象,强度比弱引用更弱可以用WeakReference类来实现弱引用。被弱引用关联的对象只能生存到下一次垃圾回收之前。当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
- 虚引用:最弱的引用关系。为一个对象设置虚引用唯一目的就是在这个对象回收时能得到一个系统通知。通过PhantomReference类来实现虚引用。
回收方法区
1.1.3 如何回收
1.1.3.1 垃圾回收算法
标记-清除算法
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如同它的名字一样,该算法分为“标记”和“清除”两个阶段:
首先标记出所有需要被回收的对象,在标记完成后统一回收所有被标记的对象。不足:一个是效率问题,标记和清除的效率都不高;另一个是空间问题,标记清除后产生大量不连续的碎片,空间碎片过多导致以后再分配较大对象时,无法找到足够的联系的内存而不得不提前触发一次垃圾收集动作。
复制算法
为了解决效率问题,复制算法出现了,它将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完,就将还存活的对象复制到另一半内存中,然后再把已使用过的内存清理掉。这一就能解决内存碎片等复杂情况。缺点,将内存缩小为原来的一半,代价太高。现在的商业虚拟机基本用这种收集算法来收集新生代。
新生代中的对象98%是朝生夕死,所以并不需要按照1:1的比例划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor.Hotspot虚拟机上默认的Eden和Survivor空间占比为8:1,也就是每次新生代的可用内存为总内存的(80%+10%),只有10%内存会被浪费。回收过程是将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor中, 最后清理掉Eden和刚才用过的Survivor空间。但是,当Survivor空间不够用时,需要依赖其他内存(老年代)来进行分配担保。
标记-整理算法
复制算法一大弊端,在对象存活率高的时候需要进行过多的复制操作,效率将会变低,所以老年代一半不用复制算法。根据老年代的特点,有人提出了“标记-整理算法”:标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所以存活对象向一端移动,然后清理掉边界外的内存。
1.1.3.2 分代收集算法
将java堆分为新生代和老年代,跟进各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,那就用复制算法,只需要少量的复制成本就能完成收集。而老年代因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”算法或者“标记-整理”算法。
1.1.4Hotspot虚拟机的算法实现
在HotSpot虚拟机对对象存活判断算法和垃圾收集算法的实现,必须对算法的执行效率有严格的考量,才能保证虚拟机高效运行。
从可达性分析算法执行过程中,必须停顿所以执行线程(stop the world),因为这项分析工作必须在一个能确保一致性的快照中进行-指的是在整个分析期间,整个执行系统看起来被冻结在某个时间点上,不可以出现分析过程中对象的引用关系在不断变化的情况,否则会影响该算法的准确性。当执行系统停顿下来后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,HotSpot虚拟机通过一组称为OopMap的数据结构来存储这些信息。这样,GC在扫描的时候就可以直接得知这些信息。
1.1.4.1安全点
在OopMap的协助下,虚拟机可以快速的完成GC Roots枚举,但是,如果为每条指令都生成OopMap,将会消耗大量的额外空间,这样GC的成本将会变得很高。因此,引入安全点的概念,实际上,HotSpot虚拟机并没有为每条指令生成OopMap,而是在特定的位置记录了这些信息,这些位置被称为安全点。当程序执行时并非所在所有地方都能停下来开始GC,只有到达安全点时才能暂停。
对于安全点,需要考虑的问题是如何在发生GC时让所以线程到最近的安全点上再停顿下来。有两种方案:
- 抢先试中断:在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机使用该方式来暂停线程从而响应GC事件。
- 主动式中断:当GC需要中断线程时,不直接对线程操作,而是简单的设置一个标志,各个线程执行时主动轮询该标志,发现中断标志为真时就自己中断挂起,所以,轮询标志的地方和安全点事重合的。
1.1.4.2安全区域
看似安全点已经完美解决了如何进入GC的问题,但是还有一种情况,若程序不在执行时,也就是说线程没有分配CPU时间,比如线程处于sleep状态或者Blocked状态,这时候线程就无法响应JVM的中断请求,走到安全点区挂起。所以这时候需要引入安全区域来解决此问题。
安全区域是指在一端代码片段之中,引用关系不会发生变化,在这个区域中,任何地方开始GC都是安全的。它是安全点的扩展。