zoukankan      html  css  js  c++  java
  • JVM GC系列 — GC算法

    一.前言

    从本篇文章开始,将开始一个新的系列JVM。JVM是一个非常庞大且复制的技术体系,但是对于程序猿的升级,走向更高阶所必要经历的,曾经也下决心要好好学习一番,然而毅力不足都中途放弃。

    GC的作用就是回收垃圾,但是要做到做点必须要解决两个问题:

    1. 如何确定哪些是垃圾
    2. 怎样回收垃圾

    这两个问题可谓是GC的核心,本篇文章将从算法角度学习GC是怎样解决这两个问题。


    ### 二.如何确定哪些是垃圾

    1.引用计数法

    在Java应用中,可被回收的对象必然是无用对象,即没有其他对象引用它或者其脱离了应用的中的对象整体:

    如上图,蓝色标记的对象之间相互引用,都是存活对象,GC不应该回收。而红色标记的对象,已经没有任何对象引用它,GC应该回收它。

    从以上可以确定,GC首先应该回收没有任何引用指向其的对象。如何表达没有任何引用?

    为每个对象维护一个计数器,该计数器表示指向其的引用。当没有引用其时,则计数器应该为0,GC则应该回收。这种算法被称为引用计数法

    如上图,当计数器为0时,表示该对象已经没有任何引用,可被垃圾收集器回收。该算法实现非常简单,且性能较佳,但是存在局限性。

    2.引用计数法的局限性

    引用计数法固然简单易于理解,但是它无法解决相互引用的问题。当两个以上的对象之间相互引用,但是它们已经脱离整个Java对象的整体时,引用计数法便无法表达出它们是可回收对象:

    如上图,当两个红色的标记的对象相互引用,此外没有任何对象引用它们,显然它们是可回收对象。但是引用计数法会造成无法被回收。

    假如有以下代码:

    void referenceCount() {
        ...
    
        A a = new A();
        B b = new B(a);
        a.setB(b);
        
        ...
    }
    

    A和B之间相互引用,但是此外没有任何对象引用A和B,那么当线程运行退出referenceCount方法时,A和B依然不能被回收。

    3.可达性分析算法

    由于引用计数法的局限性,显然它不适合作为JVM中定位可回收对象的算法。实际上再JVM中使用另一种方式表示对象是否可回收 — 可达性分析算法

    可达性分析算法:能从GC Roots找到一条到该对象的引用链路,则该对象就是存活对象,如何找不到,则该对象就是可回收对象。

    这里首先要明白什么是GC Roots:

    1. 栈(栈帧中的本地变量表)中引用的对象
    2. 方法区中的静态成员
    3. 方法区中的常量引用的对象
    4. 本地方法栈中JNI(一般说的Native方法)引用的对象

    Note:
    简而言之,可达性分析算法就是根搜索算法,类似于树结构的搜索。其中GC Roots就是根,其他对象就是树上的节点。在根和节点之间寻找路径。

    从图中可以看出,可达性分析算法能够解决相互引用的问题。从GC Roots开始到对象之间存在一条引用链路,则就是存活对象,反之即是死亡对象。


    ### 三.怎样回收垃圾

    通过以上的可达性分析算法,GC能够找到堆区中的可回收对象,但是具体怎样回收才能具有更高的效率呢?在JVM中存在以下几种回收算法:

    1. 标记清除
    2. 复制
    3. 标记整理

    以上三种算法都各有利弊,下面逐一介绍。

    1.标记清除算法

    顾名思义,该算法分为两个阶段,先标记出可回收对象,然后再清除这些对象。

    将堆区看成以上连输的方块片段,红色代表没有GC Roots引用的对象,蓝色代表有引用。

    标记清除算法,首先针对堆区对象进行标记,标记出没有GC Root是对象引用的对象。然后再将没有GC Roots引用的对象清除

    该算法的效率较高,只需要标记然后清除即可。但是从图中也可以看出问题所在,在清除后,会造成大量的不连续的内存碎片,当有大对象分配时,将又会触发GC,从而影响性能。

    2.复制算法

    为了解决标记清除算法的导致的内存碎片问题,复制算法因此而生。复制算法的思想是将内存分为两块,每次只使用其中一块,回收时将存活的对象复制到另一块中,然后将使用的那块完全清除,再使用新的复制那块

    经过复制算法后,不会再产生断断续续的内存碎片。每次垃圾回收之后,都能得到连续的大片内存。但是又产生了新的问题,内存是昂贵资源,将其分成两块,每次只使用其中一块,造成了资源浪费。但是实际中,对象基本上都是朝生夕死,能存活的对象少之又少,所以实际中一般不会按照1:1的比例分块。在Hostspot的年轻代默认按照8:1的进行划分,即Eden:Survivor=8:1。

    3.标记整理算法

    为了既解决标记清除算法带来的内存碎片问题,又能很好解决复制算法的内存牺牲问题。又出现标记整理算法。它类似标记清除,但是又增加了一个整理过程。即将存活对象往一端移动,整理内存碎片,形成连续内存

    通过标记清理,能够将回收垃圾对象,释放内存。通过整理压缩,能够形成连续内存,解决内存碎片。

    虽然标记整理算法解决了复制算法和标记清除算法带来的问题,但是整理压缩也是耗费性能,降低效率。

    4.分代算法

    以上的GC回收算法都各有利弊,实际使用中,根据内存区域的划分以及其特点,HotSpot不单一采用以上的算法,而是根据堆区不同分代,分别采用以上算法,这种算法被称为分代算法

    由于Java对象具有朝生夕死的特点,堆区一般划分为新生代(年轻代)和老年代(年老代)。新生代对象生存周期短暂,老年代对象生成周期较长:

    • 新生代对象存活周期短,每次只有少量对象存活,使用复制算法比较适宜。所以HotSpot中将新生代分为Eden和Survivor区域。
    • 老年代对象存活时间长,每次GC后,大部分对象都存活。所以使用标记整理算法。

    ### 总结

    本篇文章主要介绍GC中两个核心问题,第一:GC如何确定哪些对象是可回收的,在HotSpot中使用从GC Roots开始的可达性分析算法,定位对象是否存活。第二:在确定对象的存活与否后,采用何种策略回收对象。商业虚拟机中多数采用分代算法解决该问题。在了解了GC回收算法后,下篇文章再围绕这些算法学习HotSpot中的有哪些具体实现。

  • 相关阅读:
    Autofac-案例
    Autofac-DynamicProxy(AOP动态代理)
    AutoFac注册2-程序集
    MVC添加跨域支持Cros
    redis笔记3-基础知识与常用命令
    Redis笔记2-Redis安装、配置
    Redis笔记-八种数据类型使用场景
    ActionResult源码分析笔记
    .NET UrlRouting原理
    webapi使用ExceptionFilterAttribute过滤器
  • 原文地址:https://www.cnblogs.com/lxyit/p/10369714.html
Copyright © 2011-2022 走看看