首页 > 技术文章 > GC垃圾回收器

leeyuxin 2019-03-11 20:35 原文

java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”。jvm解决的两个问题:给对象分配内存以及回收分配给对象的内存。GC:将内存中不再被使用的对象进行回收。GC的作用域是JVM运行时数据区的方法区和堆。频繁收集Young区;较少收集Old区;基本不动Perm区。

 

1.概述

  垃圾收集(GC)需要完成的3件事:

  (1)哪些内存需要回收?

  (2)什么时候回收?

  (3)如何回收?

  GC类型:

  (1) Minor GC:针对新生代的GC

  (2) Major GC:针对旧生代的GC

  (3) Full GC:针对永久代、新生代、旧生代三者的GC

 

  为什么需要了解GC和内存分配?

  当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,就需要对这些“自动化”的技术实施必要的监控和调节。

  在java内存运行时区域的各个部分中,程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊的执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几部分区域的内存分配和回收都具有确定性,不需要考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样。只有在程序处于运行期时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存。

 

2.判断对象是否已死

  (1) 引用计数法

    给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。主流的java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因就是它很难解决对象之间相互循环引用的问题。

  (2) 可达性分析算法

    算法的基本思想就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

 

  在java语言中,可作为GC Roots的对象包括下面几种:

    a.虚拟机栈(栈帧中的本地变量表)中的引用的对象

    b.方法区中类静态属性引用的对象

    c.方法区中常量引用的对象

    d.本地方法栈中JNI(Native方法)引用的对象

  判断一个类是否是“无用的类”的条件为:

    a. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例

    b. 加载该类的ClassLoader已经被回收

    c. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

 

3. 垃圾收集算法

  (1) 标记--清除

    标记清除算法是最基础的算法,算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

    不足:一个是效率问题,标记和清除两个过程两个过程效率都不高;另一个是空间问题,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多可能导致之后需要分配大量内存对象时,无法找到足够的连续的内存而不得不提前触发另一次垃圾收集动作。

    

                                

 

 

  (2) 复制算法

    将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂的情况了,实现简单,运行高效,代价为将内存缩小为原来的一半。这种方法适用于回收新生代。

               

 

  (3) 标记--整理算法

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

    

 

  (4) 分代收集算法

    在新生代中,每次垃圾收集时都发现大批对象死去,只有少量存活,选择复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就需要使用标记清理或者标记整理的算法来回收。

 

4.垃圾收集器

  如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

  (1) Serial收集器

    它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。适用于新生代。

  (2) ParNew收集器

    ParNew收集器就是Serial收集器的多线程版本。

  (3) Parallel Scavenger收集器

    Parallel Scavenger收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

  (4) Serial Old收集器

    Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记清理”算法。

  (5) Parallel Old 收集器

    Parallel Old 是 Parallel Scavenger收集器的老年代版本,使用多线程和“标记整理”算法。

  (6) CMS 收集器

    CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记清除”算法实现的。整个步骤分为:

    a. 初始标记

    b. 并发标记

    c. 重新标记

    d. 并发清除

  (7) G1收集器

    优点:并行和并发,分代收集,空间整合,可预测停顿。

 

5. 内存分配

  (1) 对象优先在Eden分配

    大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

  (2) 大对象直接进入老年代

    大对象是指需要连续内存空间的java对象,最典型的大对象就是很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

  (3) 长期存活的对象将进入老年代

    虚拟机为每一个对象定义一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,将被移动到Survivor空间中,并设置对象年龄为1.当年龄增加到一定程度(默认为15岁),就会被晋升到老年代。

  (4) 动态对象年龄判定

    如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等到阈值年龄。

  (5) 空间分配担保

 

6. 内存分析工具

  (1) Jmc

    java 7u40发布中加入的性能监控工具。功能全面,包括CPU占用率、内存使用情况、I/O耗时等。常用于性能分析。

  (2) Jconsole

    jdk自带的可视化监视和管理控制台。监控某个进程的内存、线程、类等信息。可观察堆内存新生代、老年代的使用情况。

  (3) Jvisualvm

    jdk自带的可远程或本地监控内存泄漏,跟踪垃圾回收的工具。可以dump堆内存数据

  (4) Eclipse Memory Analyzer

    是分析堆数据的专业工具,可以定位内存泄漏的原因。

    

    

    

    

 

  

推荐阅读