JVM 垃圾回收的一些知识( 新生代老年代 / MinorGC和MajorGC / 可达分析与回收算法 )

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/j550341130/article/details/82150956

前言

JVM的自动垃圾回收处理的是不再使用的对象/数组, 这些对象/数组都是存储在堆内存中, 堆内存相关介绍请看这里:
https://blog.csdn.net/j550341130/article/details/82152054


新生代/老年代

在 Java 中, 堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ).
新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor.
这里写图片描述

默认比例:
1. 新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 );
2. 新生代 Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 )

新生代 是基本上所有对象创建/保存的区域, 一般新建的对象会分配到Eden区 ( 另一部分是大对象, 它们会直接放入老年代, 这是为了保留新生代的内存复制效率, 大对象标准可以使用-XX:PretenureSizeThreshold 来设置 ).

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务, 所以无论什么时候, 总是有一块 Survivor 区域是空闲着的. 因此, 新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间.

每次GC时, 把Eden存活的对象和From Survivor中存活且没超过年龄阈值的对象复制到To Survivor中, To Survivor变为From Survivor, 原From Survivor清空变成To Survivor.
对象在Survivor每熬过一轮Minor GC 年龄就增加1, 当年龄达到一定程度时就会被移动到老年代 ( 年龄阈值默认为15, 可以通过-XX:MaxTenuringThreshold来设置).

所以, 老年代里的对象大部分都是不容易被回收的对象 ( 另一部分是直接放入老年代的大对象 ), 发生在老年代的MajorGC频率也小于新生代的MinorGC.


MinorGC和MajorGC

MinorGC(次要的) 是发生在新生代的GC, 采用了复制算法.
MajorGC(重要的) 是发生在老年代的GC, 一般也伴随着MinorGC, 它采用了标记-清除算法, 速度比Minor GC慢10倍以上.

当新生代Eden区没有足够的空间进行分配时, 虚拟机将发起一次Minor GC.
老年代空间不足时Full GC, 如果还是不足, 则抛出OOM异常.


可达分析与回收算法

GC怎么判断哪些对象是无效的, 其内存是需要回收的呢?
如果某对象不是引用可达的, 说用其需要被回收. 这里有两种可达分析方法:

  1. 引用计数法:
    为每一个创建的对象分配一个引用计数器, 用来存储该对象被引用的数量. 当引用数量是0时, 则该对象就时不可达对象, GC可以回收掉这些引用计数为0的对象. 此种方式有个缺点, 即两个对象仅存在互相引用关系时也会被认定是不可回收的, 实际上应该被回收. 这也是java没有采用它的原因.
  2. 可达性分析算法:
    可达性分析通过一系列的称谓”GC Roots”的对象作为起始点, 从这些节点开始向下搜索, 搜索所有走过的路径为引用链, 当一个对象到GC Roots没有任何引用链项链时, 则证明此对象时不可用的.
    可作为GC Roots的对象包括下面几种:
    (1) 虚拟机栈(栈帧中的本地变量表)中引用的对象
    (2) 方法区中类静态属性引用的对象
    (3) 方法区中常量引用的对象
    (4) 本地方法栈中JNI(即一般说的Native方法)引用的对象
    个人理解是除了可能会被GC回收的堆内存内对象之外的大多数对象.

判断出某些对象没有被引用时就需要回收了, JVM的GC使用了这两种回收算法:

  1. MajorGC使用的标记-清除算法:
    标记哪些要被回收的对象, 然后统一回收. 这种方法很简单, 但是会有两个主要问题:1.效率不高, 标记和清除的效率都很低;2.会产生大量不连续的内存碎片, 导致以后程序在分配较大的对象时, 由于没有充足的连续内存而提前触发一次GC动作.
  2. MinorGC使用的复制算法:
    复制算法将可用内存按容量划分为相等的两部分, 然后每次只使用其中的一块, 当一块内存用完时, 就将还存活的对象复制到第二块内存上, 然后一次性清楚完第一块内存, 再将第二块上的对象复制到第一块. 但是这种方式内存的代价太高, 每次基本上都要浪费一半的内存.
    JVM将该算法进行了改进, 内存区域不再按照1:1去划分, 而是将内存划分为8:1:1三部分, 较大那份内存为Eden区, 其余是两块较小的内存区叫Survior区. 每次都会优先使用Eden区, 若Eden区满, 就将对象复制到第二块内存区上, 然后清除Eden区, 如果此时存活的对象太多, 以至于Survivor不够时, 会将这些对象通过分配担保机制复制到老年代中.

参考:
https://blog.csdn.net/gyqjn/article/details/49848473
https://blog.csdn.net/wnl_csdn/article/details/73865511

猜你喜欢

转载自blog.csdn.net/j550341130/article/details/82150956