深入理解JVM虚拟机-垃圾收集器

垃圾收集器算法和垃圾收集器(新生代、老生代)

一.对象及引用

1.判断对象是否存活的方法:引用计数法和可达性分析算法

引用计数法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1,任何时刻计数器为0的对象就是不能够再被使用的,可以被回收了,但是该方法不能解决循环引用的问题,目前垃圾回收器不采用这种方法

可达性分析法:将一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明该对象是不可用的,可作为GC Roots的对象主要有:本地方法栈中引用的对象、虚拟机栈中引用的对象、方法区类变量、常量引用的对象。

引用的分类主要有强引用、软引用、弱引用和虚引用

(1)强引用:指在程序代码中普遍存在的类似"Object obj = new Object()"的引用,只要强引用存在,垃圾回收器永远不会回收掉被引用的对象。

(2)软引用:用于描述一些有用但是并非必须的对象,对于软引用关联的对象,在内存溢出之前,将会将它进行回收,如果回收后内存空间还不够,将会抛出内存溢出异常

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get(); //有时候会返回null

(3)弱引用:用来描述非必须对象的,被弱引用标记的对象只能生存到下一次垃圾收集发生之前

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null

(4)虚引用:每次垃圾回收都会回收,为一个对象设置一个虚引用的唯一目的是能在这个对象被收集器回收时收到一个系统通知

2.对象的自我拯救:在可达性分析算法中的对象呢被标记为null且System.gc()回收时,如果该类中重写了finalize()方法,可以在该方法中设置自救代码,不过finalize()方法只能调用一次

//改代码转载自http://chenjingbo.iteye.com/blog/1119059
package com.taobao.jvm;  
  
public class FinalizeEscape {  
      
    public static FinalizeEscape FC = null;  
  
    @Override  
    protected void finalize() throws Throwable {  
        super.finalize();  
        System.out.println("finalize method invoke");  
        FC = this;  
    }  
      
    public static void main(String[] args) throws Exception {  
        FC = new FinalizeEscape();  
          
        FC = null;  
        System.gc();  
        Thread.sleep(500);  
        if(FC != null)  
            System.out.println("i am alive");  
        else   
            System.out.println("i am dead");  
          
        FC = null;  
        System.gc();  
        Thread.sleep(500);  
        if(FC != null)  
            System.out.println("i am alive");  
        else   
            System.out.println("i am dead");  
    }  
}  

该方法一般不使用

3.方法区中关于类的卸载条件:该类的实例已经被回收,加载该类的类加载器已经被回收,该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

二.垃圾收集算法

常见的垃圾收集算法有复制算法、标记-清除算法、标记-整理算法,分代收集算法

标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,主要有效率问题和空间问题,效率不高而且容易产生空间碎片以提前触发GC

复制算法:将内存划分为两块A、B,每次只使用其中的一块,当其中一块用完了,将该块应该存活的对象复制到另一块 ,然后将该块的内存空间全部回收,这样就不会出现空间不连续的情况,可以解决效率问题,但是空间会有浪费

标记-整理算法:和标记-清除一样,但是后续步骤不是直接对可回收的对象进行清除,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存

分代收集算法:根据对象存活周期不同将内存划分为几块,例如新生代和老生代,不同的块采用不同的垃圾收集算法

HotSpot默认Eden:Survivor大小比例=8:1,浪费整个新生代的10%,垃圾回收时,将eden的对象和survivor的对象移动到另一块survivor中,如果另一块survivor中的内存不够,这里涉及到空间分配担保,空间分配担保是在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,Minor GC可以确保是安全的,如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否会允许担保失败,如果允许,则会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则会尝试进行一次Minor GC,尽管这次Minor GC有风险,如果小于或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC,如果该次高于平局值就会担保失败,失败后重新发起一次Full GC

三.HotSpot算法实现

1.枚举根节点:使用OopMap的数据结构来直接检查哪些地方存放着对象引用,在类加载完成的时候,HotSpot把对象内什么偏移量是什么类型的数据计算出来,在JIT编译的时候,也会在特定位置记录下栈和寄存器哪些位置是引用,这样GC扫描的时候就可以直接得知这些信息

2.安全点:在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots枚举,线程需要到安全点才能GC,有抢先式中断和主动式中断两种方式,抢先式中断:GC所有线程中断有线程不在安全点则恢复线程使"它"跑到中断点,在安全点设置标志,线程轮询该标志,到安全点就停下

3.安全区域:如果线程不"走"了但是没"走"到安全点,安全区域是指在一段代码中,引用关系不会发生变化,在这个区域的任何地方开始GC都是安全的,线程执行到安全区域,JVM进行GC时就不用管了,当线程离开时会检查是否完成了根节点枚举,如果完成线程就继续执行,否则必须等待可以安全离开的信号为止

四.垃圾收集器的分类

   并行:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态

   并发:用户线程和垃圾收集线程同时运行,不一定是并行的,可能交替执行

Serial:复制算法 暂停用户线程 单线程回收 缩短用户线程的停顿时间

ParNew:复制算法 暂停用户线程 多线程回收 缩短用户线程的停顿时间

Parallel Scavenge:复制算法 暂停用户线程 达到一个可控制的吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

Serial Old:标记-整理 暂停用户线程 单线程回收 (老年代版本,与Parallel Scavenge搭配使用,或者作为CMS的后备方案)

Parallel Old:标记-整理 暂停用户线程 多线程回收 吞吐量优先

CMS:一款获取最短回收停顿时间为目标的收集器,基于标记-清理算法,主要分为4个步骤:

初始标记、并发标记、重新标记、并发清除:初始标记和重新标记需要"stop the world"

初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段发生GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记阶段因用户程序继续运作而导致标记对象产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

缺点:1.对CPU资源敏感 2.并发清理阶段产生的浮动垃圾无法处理 3."标记-清除"会产生内存碎片

G1收集器:主要以Region为单位,淡化新生代和老生代,分为以下几个步骤:

(1)初始标记 (2)并发标记 (3)最终标记 (4)筛选回收

关于内存分配和回收策略:1.对象优先在Eden分配 2.大对象直接进入老年代 3.长期存活的对象进入老年代 4.动态对象年龄判定

猜你喜欢

转载自blog.csdn.net/qq_27378875/article/details/81184368