美文网首页
java垃圾回收

java垃圾回收

作者: Crazy贵子 | 来源:发表于2018-09-27 09:05 被阅读0次

    在java开发中,每个运行程序都会产生对象,JVM分配这些对象都需要一定的内存空间。由于java拥有垃圾收集器(GC),让我们不必专门去写内存回收代码。java的垃圾回收指的是回收内存,针对的java对象,所以涉及到JVM内存结构。此外,这些也是值得注意的问题:

    • GC是怎么判断哪些对象需要回收
    • 采取哪些措施回收,什么时候回收
    • 落实到GC中是什么样子

    首先看下JVM的内存结构,主要分为堆、栈、PC和方法区(在java 8后方法区不复存在,取而代之的是元数据区mataSpace,存放在本地内存,所以与之相关的Klass也被移除了,以下分析基于HotSpot虚拟机)。因为对象主要存在堆和方法区中(利用逃逸技术可以将一些对象标量分解后在栈上分配),GC关注的也是这些区域。

    JVM内存结构

    这里是一张更详细的图片

    堆又分为年轻代和老年代,而年轻代中分为三部分Eden:FromSpace:ToSpace=8:1:1,FromSpace和ToSpace是等价的,两者身份是可以互换的。老年代空间比年轻代的要大。在创建对象时首先被分配到新生代,一般来说,大部分对象都是创建完后不久就不再使用,很快会被清理掉,如果经历了GC还在使用的就移到更持久的区域,年轻代内存划分成这样和回收的策略有关。

    JVM为了区分哪些对象是需要回收的有如下方法:

    • 引用计数法
    • 可达性分析算法

    在hotSpot中采用的是可达性分析算法。引用计数法是指当对象创建时,如果有新的引用指向它则加一,否则减一,当持有的引用数量为0时则标记为清理对象。这种方法在对象相互引用时无法识别出是否为待清理对象,比如:

    public class Person {
        
        public Person friend;
        
        // ……
    }
    
    Person 阿珍 = new Person(); // 1
    Person 阿强 = new Person(); // 2
    阿珍.person = 阿强; // 3
    阿强.person = 阿珍; // 4
    阿珍 = null; // 5
    阿强 = null; // 6
    

    按照常理,阿珍和阿强这两个对象的引用已经没有了,应该是要被清理的,但引用计数法则判定不出。1和4操作后阿珍对象引用数为2,同理阿强也为2,5和6操作都只释放了1个引用,此时已经不能通过引用来访问到阿珍和阿强两个对象了,但由于两者相互引用导致引用数不为0,GC也就不会回收这两个对象,造成内存泄漏。

    可达性分析采用的是图论的方法,对象之间的引用可记录为一张图,从根节点(GC Roots)出发,遍历这些对象,如果有不连通的对象,则标记为可回收对象。在JVM中,这些引用可以作为GC Roots:

    • 栈中的对象引用
    • 方法区中的静态对象引用和常量对象引用
    • 本地方法栈JNI的对象引用

    刚才的例子如果采用可达性分析则如下图所示:

    内存分配1 内存分配2 GC

    将对象引用设为null后:

    GC

    寻找到可回收的对象后,GC就可以进行回收工作了,以下是回收算法:

    • 标记-清除
    • 标记-整理
    • 复制

    标记-清除

    将标记为可回收的对象直接清除 ,但这样会造成内存碎片产生,像老鼠打洞一样,这里空一点那里空一点,不利于内存再分配。

    标记-整理

    相当于在标记-清除的基础上添加了整理,把存活下来的对象往一个方向堆,类似于消消乐,消失的那部分会有其他对象从上面掉下来填充(这里的方向就是向下)。

    复制

    复制算法将内存划为大小相同的两部分,先用A内存,对A内存进行清除时遍历存活对象,将存活的对象全部复制到B,然后一口气将A内存清空,下一次对B内存清理时亦是如此。

    因为大部分对象是朝生夕死,所以JVM的垃圾收集器根据其存活时间长短将堆划分为年轻代和老年代。可以预见,年轻代对象回收发生频率很高,所以采取复制算法比较好,一次性复制存活对象和清除回收对象,简单粗暴。而老年代是存活的对象比较多,需要回收的对象少,采用复制算法开销会比较大,所以采用标记-整理算法。不同区域采用不同的算法,这就是java的分代回收机制。

    正是如此,年轻代才会将内存划分为8:1:1的布局。当Eden快满的时候,进行一次Minor GC,将存活对象复制到FromSpace,下次Minor GC的时候将FromSpaceEden的存活对象复制到ToSpace。这两个Survivor总有一个是空的,如果在复制的过程中发现有些对象达到了晋升老年代的条件(当这两个存活去切换了一定次数之后,默认是15次,可以使用-XX:MaxTenuringThreshold控制)则将其移动到老年代。为了加快Eden的对象回收和内存分配,HotSpot JVM采用了bump-the-point和TLAB(Thread-Local Allocation Buffers)两种技术。由于Eden是连续的, bump-the-point跟踪最后创建的一个对象,查看其后面是否有充足的内存,TLAB针对的是多线程,它将Eden划分为若干段,每个线程使用独立的一段。这两种技术结合起来可以快速地分配内存。如果对象比较大(比如大数组),可能会直接分配到老年代。当老年代快满的时候会触发一次Major GC。

    这些都是对象回收的策略,根据这些策略,GC使用如下几种收集器对其进行管理。

    HotSpot JVM1.6的垃圾收集器

    相连收集器的可以配合使用。

    • 新生代收集器:Serial、ParNew、Parallel Scavenge

    • 老年代收集器:CMS、Serial Old、Parallel Old

    根据并行和串行以及年轻代和老年代的划分,看下各个收集器的特点:

    收集器 串行、并行 or 并发 新生代/老年代 算法 目标 使用场景
    Serial 串行 新生代 复制 响应速度优先 单CPU环境下的Client模式
    ParNew 并行 新生代 复制 响应速度优先 多CPU环境时在Server模式下与CMS配合
    Parallel Scavenge 并行 新生代 复制 吞吐量优先 在后台运算而不需要太多交互的任务
    Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后备预案
    Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务
    CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网站或B/S系统服务端上的java应用
    G1 并发 both 标记-整理 + 复制 响应度优先 面向服务端应用,将来替换CMS

    Serial收集器

    单线程运行,进行GC的时候其他线程暂停,简单高效,是Hospot运行在Client模式下默认使用的新生代垃圾收集器。

    ParNew

    是Serial收集器的多线程版,是在Server模式下新生代的首选收集器,目前除了Serial收集器仅有它能和CMS(Concurrent Mark Sweep)配合工作。

    Parallel Scavenge

    多线程,关注GC时尽可能缩短用户线程的停顿时间,提高吞吐量(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + GC时间)),适用于不需要太多交互的后台运算。

    Serial Old

    单线程,使用标记-整理算法,在工作是其他线程需要停止。

    Parallel Old

    多线程,关注吞吐量,在注重吞吐量和CPU资源敏感的场合可以与Parallel Scavenge配合使用。

    CMS

    多线程并发运行,使用标记-清除算法,致力于获取最短停顿时间(缩短垃圾回收时间)。

    CMS执行过程为:初始化标记->并发标记->预清理->可控清理->重新标记->并发清除->并发重设状态等待下次CMS的触发(先2次标记,1次预处理,1次重新标记,1次清除)

    • 初始化标记:仅标记GC Roots能关联到的对象
    • 并发标记:进行GC Roots Tracing,整个过程耗时最长
    • 重新标记:为了修正用户在运行时导致标记变动那一部分对象的标记记录

    CMS运行需要额外的CPU和内存资源,所以在CPU内存紧张的情况下会采用Serial Old收集。同时因为采用的标记-清除算法,导致空间碎片产生。在CMS并发处理阶段由于用户线程还在运行,产生的新垃圾在本次清理中无法被清理,称为浮动垃圾,只能等待下CMS回收。

    G1

    多线程并行,采用标记-整理算法,运行在Server模式下

    连续内存空间 Region

    G1将整个堆内存划分为大小相同的独立区域(region),新生代和老年代在物理上不再分隔。当对象分配到一个region后它可以与整个堆的任意对象发生引用关系,进行扫描时需扫描整个堆,消耗很大。为了避免全堆扫描的发生,G1中每个Region会与一个Remembered Set关联,当对对象数据进行写操作时会产生一个Write Barrier暂时中断写操作,检查对象引用的对象是否处于不同的Region中,是则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中,扫描从GC Roots出发也就不会扫描全堆和有所遗漏了。

    G1可以建立可预测的时间模型,它跟踪各个Region中垃圾的价值大小(回收获得的空间与所付出的时间),在后台维护一个优先列表,优先回收价值最大的Region。

    G1的运行过程大致如下:

    • 初始标记:仅标记GC Roots能直接到达的对象
    • 并发标记:开始对heap中的对象标记,标记线程与应用程序线程并行执行,并收集各个Region的存活对象。
    • 最终标记:并发标记会将对象的修改记录到Remmembered Set Logs中,在本阶段将RSL整合到RS中。
    • 筛选回收:对Region中的回收价值进行排序,此阶段时间用户可控制。

    参考自:

    深入理解JVM(3)——7种垃圾收集器
    Java系列笔记(3) - Java 内存区域和GC机制
    Java Hotspot G1 GC的一些关键技术

    相关文章

      网友评论

          本文标题:java垃圾回收

          本文链接:https://www.haomeiwen.com/subject/iqbsoftx.html