美文网首页面试
Java 技术之垃圾回收机制

Java 技术之垃圾回收机制

作者: 老羊_肖恩 | 来源:发表于2017-10-11 22:22 被阅读27次

    Java堆内存

    Java堆是被所有线程共享的一块内存区域,所有对象实例和数组都在堆上进行内存分配。为了进行高效的垃圾回收,虚拟机把堆内存划分成新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)3个区域。

    新生代

    新生代由 Eden 与 Survivor Space(S0,S1)构成,Eden 与 Survivor Space 的内存大小比例默认为8:1,大多数情况下,对象在Eden中分配,当Eden没有足够空间时,会触发一次Minor GC。Survivor Space是新生代和老年代的缓冲区域。当新生代发生GC(Minor GC)时,会将存活的对象移动到S0内存区域,并清空Eden区域,当再次发生Minor GC时,将Eden和S0中存活的对象移动到S1内存区域。
      存活对象会反复在S0和S1之间移动,当对象从Eden移动到Survivor或者在Survivor之间移动时,对象的GC年龄自动累加,当GC年龄超过默认阈值15时,会将该对象移动到老年代

    老年代

    老年代用于存放经过几次Minor GC之后依旧存活的对象。当老年代的空间不足时,会触发Major GC/Full GC,速度一般比Minor GC慢10倍以上。

    永久代

    在JDK8之前的HotSpot实现中,类的元数据如方法数据、方法信息(字节码,栈和变量大小)、运行时常量池、已确定的符号引用和虚方法表等被保存在永久代中,32位默认永久代的大小为64M,64位默认为85M,可以通过参数-XX:MaxPermSize进行设置,一旦类的元数据超过了永久代大小,就会抛出OOM异常。虚拟机团队在JDK8的HotSpot中,把永久代从Java堆中移除了,并把类的元数据直接保存在本地内存区域(堆外内存),称之为元空间。

    “垃圾”

    所谓“垃圾”,就是指所有不再存活的对象。常见的判断是否存活有两种方法:引用计数法和可达性分析。

    引用计数法

    为每一个创建的对象分配一个引用计数器,用来存储该对象被引用的个数。当该个数为零,意味着没有人再使用这个对象,可以认为“对象死亡”。但是,这种方案存在严重的问题,就是无法检测“循环引用”:当两个对象互相引用,即时它俩都不被外界任何东西引用,它俩的计数都不为零,因此永远不会被回收。而实际上对于开发者而言,这两个对象已经完全没有用处了。因此,Java 里没有采用这样的方案来判定对象的“存活性”。

    可达性分析

    目前Java采用的对象存活性判断方案。基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。其余的对象则被视为“死亡”的“不可达”对象,或称“垃圾”。
      GC Roots 本身一定是可达的,这样从它们出发遍历到的对象才能保证一定可达。那么,Java 里有哪些对象是一定可达呢?主要有以下四种:

    • 虚拟机栈(帧栈中的本地变量表)中引用的对象。
    • 方法区中静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中 JNI 引用的对象。

    那么,如何快速枚举GC Roots?GC Roots主要在全局性的引用(常量或类静态属性)与执行上下文(本地变量表中的引用)中,很多应用仅仅方法区就上百兆,如果进行遍历查找,效率会非常低下。
      在HotSpot中,使用一组称为OopMap的数据结构进行实现。类加载完成时,HotSpot把对象内什么偏移量上是什么类型的数据计算出来存储到OopMap中,通过JIT编译出来的本地代码,也会记录下栈和寄存器中哪些位置是引用。GC发生时,通过扫描OopMap的数据就可以快速标识出存活的对象。

    垃圾回收算法

    标记-清理

    标记-清理算法大致分成两步:1. “标记” 就是利用可达性遍历堆内存,把“存活”对象和“垃圾”对象进行标记;2. “清理” 既然“垃圾”已经标记好了,那我们再遍历一遍,把所有“垃圾”对象所占的空间直接清空即可。
      算法缺点:效率问题,标记和清除过程效率都很低(递归与全堆对象遍历);空间问题,收集之后会产生大量的内存碎片,不利于大对象的分配。

    标记-整理

    在清理的时候,把所有存活对象移动到一端,然后直接清理边界以外的内存,这样就不会有碎片。

    复制

    复制算法将可用内存划分成大小相等的两块A和B,每次只使用其中一块,当A的内存用完了,就把存活的对象复制到B,并清空A的内存,不仅提高了标记的效率,因为只需要标记存活的对象,同时也避免了内存碎片的问题,代价是可用内存缩小为原来的一半。这种方案适合存活对象少,垃圾多的情况。

    Java 的分代回收机制

    新生代-复制 回收机制

    对于新生代区域,由于每次 GC 都会有大量新对象死去,只有少量存活。因此采用 复制 回收算法,GC 时把少量的存活对象复制过去即可。其工作原理如下:

    1. 首先,Eden区最大,对外提供堆内存。当 Eden 区快要满了,则进行 Minor GC,把存活对象放入 Survivor A 区,清空 Eden 区;
      Eden区被清空后,继续对外提供堆内存;
    2. 当 Eden 区再次被填满,此时对 Eden 区和 Survivor A 区同时进行 Minor GC,把存活对象放入 Survivor B 区,同时清空 Eden 区和Survivor A 区;
    3. Eden区继续对外提供堆内存,并重复上述过程,即在 Eden 区填满后,把 Eden 区和某个 Survivor 区的存活对象放到另一个 Survivor 区;
    4. 当某个 Survivor 区被填满,且仍有对象未被复制完毕时,或者某些对象在反复 Survive 15 次左右时,则把这部分剩余对象放到Old 区;
    5. 当 Old 区也被填满时,进行 Major GC,对 Old 区进行垃圾回收。
    老年代-标记整理 回收机制

    老年代一般存放的是存活时间较久的对象,所以每一次 GC 时,存活对象比较较大,也就是说每次只有少部分对象被回收。

    相关文章

      网友评论

        本文标题:Java 技术之垃圾回收机制

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