6、内存管理机制---垃圾收集器(上)

       1960年 MIT(麻省理工学院 Massachusetts Institute of Technology )Lisp语言 第一次使用动态内存分配和垃圾收集技术;

        那些内存需要被释放

        在什么时候释放

        怎样实现释放

 

       程序计数器、jvm栈、本地方法栈随 线程创建和释放(不由GC回收),栈中的栈帧随方法的进入和退出顺序执行入栈和出,每个栈帧的大小在编译时确定(无动态扩张情况);

       垃圾收集器对堆回收前,判断对象在后面的程序还要被调用,或者不再被调用,  判断方法:

1、引用计算法:在调用时,计数器值+1;调用结束时,计数器值-1;当计数器值为0时不能再被调用,适用大部分gc算法;但不能解决对象循环互调;

2、可达性分析算法  通过"GC Roots"对象作为起始点,从起始节点开始向下搜索,走过的路径称为引用链(reference chain),当一个对象与GC Roots没有链接时,则该对象是不可用的。

可以作为GC Roots对象包括:

  • jvm栈(栈中本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象 (单例模式 final A a;)
  • JNI(c++)中引用的对象

1.2之后对引用进行看扩充,将引用分为:

强引用(String Reference),类似 A a = new A(),只要a还会使用,收集器不会收集a空间

软引用(Soft Reference), 弱引用被回收后,还是不能消除内存溢出,溢出前回收 软引用空间

弱引用(Weak Reference),下次gc收集垃圾时,被回收

虚引用(Phantom Reference);关联对象,在对象被回收之前返回一个系统信息,不能通过虚引用取得实例,该空间已被回收;

 

对象的自我救赎finalize()

一个对象被回收前至少要经历两次标记,可达性分析后发现与GC Roots没有连接时,将进行第一次标记, 并筛选该对象是否需要执行finalize()方法;  如果该对象被判定有必要执行finalize方法则将放入F-Queue列队,由低级Finalizer线程去触发它,finalize()提供给对象最后一次不回收的机会,只要和引用链上任何一个对象建立关联即可:

public class Test { 
	public static  Test t=null;
	public void isAlive(){
		System.out.println("still here");
	}
	protected void finalize(){
		System.out.println("执行finalize方法");
		t = this;  //自我引用
	}
	public static void main(String...s) throws Exception{
		t = new Test();
		t=null;
		System.out.println("is here ?");
		System.gc();
		//执行gc()时触发finalize()方法,finalize方法优先级很低,不设置等待,就会出现没有执行就执行下一条语句了;
		Thread.sleep(500);
		t.isAlive();
		System.out.println(t.hashCode());
		//执行了finalize()方法,t复活了,但只能复活一次,若再执行:
		t=null;
		System.out.println("is here ?");
		System.gc();
		Thread.sleep(500);
	    //判定对象t是否还在heap中存在
		System.out.println(t==null);
	}
}

输出:

is here ?

执行finalize方法

still here            //heap中还存在

5629279

is here ?

true            //表示已被回收

原书作者不建议使用此方法复活对象;

 

回收方法区,jvm规范中讲过可以不要求jvm对方法区的垃圾回收,因为能释放的空间很少。永久代的垃圾收集主要有两部分:废弃常量和无用的类。

回收常量与回收heap类似,而回收无用的类比较复杂,判断是否为无用的类:

  • java  heap中不存在该类的任何实例
  • 该类的类类加载已经被回收
  • 该类的Class对象没有被任何地方引用

 

$垃圾收集算法:

标记-清除(Mark- sweep)

标记-整理(Mark- compact)

复制(Copying)

分代收集算法(老年代、新生代),新生代中98%的会被回收,将新生代分为Eden(大块)、两个survival;每次使用Eden和其中一个survival,当回收时,将Eden和survival中存活的对象一次性的复制到另外一块survival空间上,然后格式化Eden和刚才用过的survival空间,HotSpot默认Eden与survival大小比例8:1,新生代中的90%用来装载新生对象,10%用来转载存活的对象。

 

HotSpot的算法实现(详见下章): 

          枚举根节点:可达性分析必须在一个一致性的快照中进行-即整个分析期间,系统就像冻结了一样。否则如果一边分析,系统一边动态表化,得到的结果就没有准确性。这就导致了系统GC时必须停顿所有的Java执行线程。在HotSpot实现中,使用一组称为 OopMap 的数据结构来存放对象引用。OopMap会在类加载完成的时候,记录对象内什么偏移量上是什么类型的数据,在JIT编译过程中,也会在特定的位置记录下栈和寄存器哪些位置是引用。

 

          安全点:OopMap内容变化的指令非常多,HotSpot并不会为每条指令都产生OopMap,只是在特定的位置记录了这些信息,这些位置成为“安全点”(SafePoint)。程序执行时只有在达到安全点的时候才停顿开始GC。一般具有较长运行时间的指令才能被选为安全点,如方法调用、循环跳转、异常跳转等。接下来要考虑的便是,如何在GC时保证所有的线程都“跑”到安全点上停顿下来。这里有两种方案:

抢先式中断 (Preemptive Suspension) 和主动式中断 (Voluntary Suspension)。

抢先式中断会把所有线程中断,如果某个线程不在安全点上,就恢复让它跑到安全点上。几乎没有虚拟机采用这种方式。

主动式中断思想是设立一个GC标志,各个线程会轮询这个标志并在需要时自己中断挂起。这样,标志和安全点是重合的。

 

           安全区域:Safepoint机制可以保证某一程序在运行的时候,在不长的时间里就可以进入GC的Safepoint。但是如果程序没有分配CPU时间,例如处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求。对于这种情况,只能用 安全区域 (Safe Region)来解决。安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中任意地方开始都是安全的。在线程执行到Safe Region中的代码时,就标记自己已经进入了Safe Region,这样JVM在发起GC时就跳过这些线程。在线程要离开Safe Region时,它要检查系统是否已经完成了枚举(或GC过程),如果完成了线程就继续执行,否则就等待。

 

 

猜你喜欢

转载自nickfover.iteye.com/blog/2138331