JVM中的垃圾回收机制和垃圾收集器

一、什么是垃圾回收

java相较于c、c++语言的优势之一是自带垃圾回收器,垃圾回收是指不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,程序员唯一能做的就是通过调用System.gc 方法来建议执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。

二、为什么需要垃圾回收

如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。

三、java中的四种引用类型

  • 强引用
Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null;  //手动置null,可以通过System.gc方法进行回收处理

我们一般声明对象时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,就说明他不是垃圾,则不会被垃圾回收。

  • 软引用
    软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。

  • 弱引用
    弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次GC发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉该类对象。弱引用对象在回收时会被放入引用队列(ReferenceQueue)。

  • 虚引用
    虚引用被称为幽灵引用或幻象引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得对象实例。任何时候都可能被回收,一般用来跟踪对象被垃圾收集器回收的活动,起哨兵作用。必须和引用队列(ReferenceQueue)联合使用。

这些概念可能有点抽象,不过这四种引用具有不同的垃圾回收时机应该是很清楚的。我们会发现,引用的强度从强、软、弱、虚依次递减,越往后的引用所引用的对象越容易被垃圾回收。

四、垃圾识别机制

4.1 引用计数算法

引用计数算法是判断对象是否存活的算法之一:它给每一个对象加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能被使用的,即将被垃圾回收器回收。

缺点:

  • 他需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
  • 无法解决对象减互相循环引用的问题。即当两个对象循环引用时,引用计数器都为1,当对象周期结束后应该被回收却无法回收,造成内存泄漏。
public class GcTest {
    
    
    public static void main(String[] args) {
    
           
      MyObject myObject_1 = new MyObject();       
      MyObject myObject_2 = new MyObject();                
      myObject_1.instance = myObject_2;        
      myObject_2.instance = myObject_1;
      myObject_1 = null;        
      myObject_2 = null;  
      System.gc();    
      }  
      // 对象循环引用,用引用计数算法时,无法回收这两个对象         

4.2可达性分析算法

目前主流使用的都是可达性分析算法来判断对象是否存活。算法基本思路:以“GC Roots”作为对象的起点,从此节点开始向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

  • 哪些对象可作为GC Roots?

虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI(Native方法)引用的对象;
活跃线程的引用对象。

五、finalize()赋予对象重生

finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。

在可达性分析算法中被标记为不可达的对象,也不一定是一定会被回收,它还有第二次重生的机会。每一个对象在被回收之前要进行两次标记,一次是没有关联引用链会被标记一次,第二次是判断该对象是否覆盖finalize()方法,如果没有覆盖则真正的被定了“死刑”。

代码实现

public class FinalizeTest {
    public static FinalizeTest ft;
    /**
     * 用于判断对象是死亡还是存活
     */
    public static void judge(){
        if(ft == null){
            System.out.println("i am dead");
        }else{
            System.out.println("i am alive");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ft = new FinalizeTest();
 
        // 将引用指向null,那么对象就没有任何关联了
        ft = null;
        // 触发一次gc
        System.gc();
        // 因为Finalizer线程的优先级低,因此sleep 1秒后再看结果
        Thread.sleep(1000);
        //因为FinalizeTest对象覆盖了finalize方法,并在该方法中重新建立与引用的关联,所以对象会复活
        judge();
        //下面的代码和上面的一模一样,但是对象不会再复活了,因为finalize方法最多执行一次
        ft = null;
        System.gc();
        Thread.sleep(1000);
        judge();
    }
 
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("执行finalize方法");
        // 对象复活的关键:重新建立与引用的关联
        ft = this;
    }
}

六、四种垃圾回收算法

确认完垃圾之后肯定要想办法回收垃圾。回收垃圾主要有下面四种方法:标记清除算法、标记整理算法、复制算法、分代收集算法。

6.1标记清除算法

算法思路:算法分为“标记”和“清理”两个步骤,首先标记处所有需要回收的对象,在标记完成后再统一回收所有被标记的对象。

缺陷:

  • 标记和清理的两个过程效率都不高;
  • 容易产生内存碎片,碎片空间太多可能导致无法存放大对象。

适用于存活对象占多数的情况。

6.2标记整理算法

算法思路:标记过程和标记-清理算法一样,而后面的不一样,它是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

6.3复制算法

算法思路:将可用内存划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完后,就将还存活的对象复制到另一块去,然后再把已使用过的内存空间一次清理掉。

缺陷:
可用内存缩小为了原来的一半
算法执行效率高,适用于存活对象占少数的情况。

6.4分代收集算法

当前大多数垃圾收集都采用的分代收集算法,这种算法并没有什么新的思路,只是根据对象存活周期的不同将内存划分为几块,每一块使用不同的上述算法去收集。在jdk8以前分为三代:年轻代、老年代、永久代。在jdk8以后取消了永久代的说法,而是元空间取而代之。

在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清除-压缩”算法进行回收。

七、垃圾收集器


如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。虽然对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止没有出现一个最用的的垃圾收集器,更加没有万能的垃圾收集器,能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一任何场景下都适用的完美收集器存在,那么Java虚拟机就不会实现那么多不同的垃圾收集器了。

经典的一些收集器(serial,parnew, CMS,G1)

7.1新生代的收集器

7.1.1Serial收集器
  • 单一线程工作
  • Stop The World
  • 客户端模式默认收集器
  • 简单高效
  • 标记-复制算法
  • 对于内存资源受限的环境, 它是所有收集器里额外内存消耗最小的;
  • 已经基本不适用了
  • 客户端模式默认收集器
7.1.2ParNew收集器 是Serial收集器的并发版本
  • 可以多线程并行GC
  • 对cpu的依赖性比较高
  • 复制算法
  • *ParNew收集器在单核心处理器的环境中不会有比Serial收集器更好的效果
  • *并行处理
    并行(Parallel) : 并行描述的是多条垃圾收集器线程之间的关系, 说明同一时间有多条这样的线程在协同工作, 通常默认此时用户线程是处于等待状态。
    并发(Concurrent) : 并发描述的是垃圾收集器线程与用户线程之间的关系, 说明同一时间垃圾收集器线程与用户线程都在运行。 由于用户线程并未被冻结, 所以程序仍然能响应服务请求, 但由于垃圾收集器线程占用了一部分系统资源, 此时应用程序的处理的吞吐量将受到一定影响。
7.1.3Parallel Scavenge收集器
  • 关注的侧重点在吞吐量,也被称为吞吐量优先的处理器
    • *吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值 即 吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾处理时间)
  • 自适应的调节策略
    • *并行收集的多线程收集器
    • *通过复制算法进行垃圾清理
    • *虚拟机会根据当前系统的运行情况,收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
    • *适合在后台运算而不需要太多交互的分析任务,过多的停顿可能会影响交互体验

7.2用于老年代的收集器

7.2.1Serial Old
  • *Serial Old收集器 Serial的老年版本
  • *单线程的
  • *标记-整理算法
  • *供客户端模式下的HotSpot虚拟机使用
  • *如果在服务端模式下, 它也可能有两种用途:
  • *一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用
  • *另外一种就是作为CMS收集器发生失败时的后备预案, 在并发收集发生Concurrent Mode Failure时使用。
7.2.2Parallel Old收集器
  • 标记整理算法
  • 注重吞吐量或者处理器资源比较稀缺的场合
  • 并行的多线程
7.2.3CMS收集器
  • 最短回收停顿时间
  • 标记清除算法
  • 四个步骤
    • 初始标记
      Stop The World
      标记一下GC Roots能直接关联到的对象, 速度很快;
    • 并发标记
      从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行
    • 重新标记 Stop The World
      因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录 ,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短
    • 并发清除
      清理删除掉标记阶段判断的已经死亡的对象, 由于不需要移动存活对象, 所以这个阶段也是可以与用户线程同时并发的。
  • 并发收集,低停顿
  • 对处理器资源要求比较高
  • 无法处理浮动垃圾,有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
  • 大量的内存碎片,还需要配合标记-清除算法统一整理
7.2.4Garbage First(G1)收集器
  • 主要面向服务端的,
    服务端, 延迟小的好 . 但当功能为计算数据时,等个30秒再算也没毛病,这时吞吐量优先更合适.客户端,一般要给人用的,自然是低延迟好
    *G1整体是标记-整理算法,局部是标记-复制算法, 意味着不会产生空间碎片

  • 可预测停顿时间垃圾收集器

  • 是Region的内存布局形式
    G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
    根据不同类型的Region,选择不同的策略去进行收集管理。

  • 也是分代理论,但是不再是划分为老年代,新生代了

  • 可以管理全堆内存

  • 四个步骤 世界停止的时间比较短

    • 初始标记
      *Stop The World,对用户线程做另一个短暂的暂停
      *标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确的在可用的Region中分配新对象。
      *这个过程需要停顿线程,但是实在新生代收集线程的时候同步完成的,所以实际上没有额外的停顿
    • 并发标记
      *从GC Root开始对堆中对象进行可达性分析, 递归扫描整个堆
      里的对象图, 找出要回收的对象, 这阶段耗时较长, 但可与用户程序并发执行。 当对象图扫描完成以后, 还要重新处理SATB记录下的在并发时有引用变动的对象。
    • 最终标记
      *Stop The World
      *用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
    • 筛选回收
      *Stop the world
      *负责更新Region的统计数据, 对各个Region的回收价值和成本进行排序, 根据用户所期望的停顿时间来制定回收计划, 可以自由选择任意多个Region构成回收集
      然后把决定回收的那一部分Region的存活对象复制到空的Region中
      再清理掉整个旧Region的全部空间
      这里的操作涉及存活对象的移动, 是必须暂停用户线程, 由多条收集器线程并行完成的。
      *G1相比于PS/PSold
      最大的好处是停顿的时间可控,可预测
      *收益优先
      G1收集器的停顿预测模型是以衰减均值为理论基础来实现的,也就是说根据这个理论来判断哪个的收益更高。
      哪块内存中存放的垃圾数量最多, 回收收益最大 ,就去回收那一块
      *Humongous区域, 专门用来存储大对象,默认将其看为老年代
      如果大对象超过一个regions,那么会用多个Humogous区域与来存储

*G1整体是标记-整理算法,局部是标记-复制算法, 意味着不会产生空间碎片

https://blog.csdn.net/m0_59879385/article/details/127516655

猜你喜欢

转载自blog.csdn.net/qq_39813400/article/details/129530528