JVM 垃圾回收机制(GC)

这是什么?我为什么会写这个?我不是要准备考研复习的吗?

一、GC ?

Java garbage collection is an automatic process to manage the runtime memory used by programs. By doing it automatic JVM relieves the programmer of the overhead of assigning and freeing up memory resources in a program.
—— javapapers

“自适应的、分代的、复制、标记清理” 式垃圾回收器。

  Java 的垃圾回收机制是由垃圾收集器 Garbage Collection GC 来实现的,GC是 JVM 里的守护进程。它的特别之处是它是一个低优先级进程,但是可以根据内存的使用情况动态的调整他的优先级。因此,它是在内存中低到一定限度时才会自动运行,从而实现对内存的回收。这就是垃圾回收的时间不确定的原因
  为何要这样设计:因为 GC 也是进程,也要消耗 CPU 等资源,如果 GC 执行过于频繁会对 Java 的程序的执行产生较大的影响(java 解释器本来就不快),因此 JVM 的设计者们选着了不定期的 GC。

  • 触发 GC 的类型
      Java 虚拟机会把每次触发 GC 的信息打印出来来帮助我们分析问题,所以掌握触发 GC 的类型是分析日志的基础。
    • GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的 GC。
    • GC_CONCURRENT: 当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发 GC 操作来释放内存。
    • GC_EXPLICIT: 表示是应用程序调用 System.gc、VMRuntime.gc 接口或者收到 SIGUSR1 信号时触发的 GC。
    • GC_BEFORE_OOM: 表示是在准备抛 OOM 异常之前进行的最后努力而触发的 GC。

二、JVM 的堆内存(Heap Memory)?

  • JVM 的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而 Java 堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。
  • 堆区是在 JVM 启动时创建的,主要用来维护运行时数据,运行过程中创建的对象和数组都是基于这块内存空间,可以认为 Java 中所有通过 n e w \color{blue}{new} 创建的对象的内存都在此分配。
  • 非堆(Non-Heap Memory):存放类加载信息和其它 meta-data。非堆是 JVM 留给自己用的,包含方法区、JVM 内部处理或优化所需的内存(如 JIT Compiler(Just-in-time Compiler),即时编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码。

三、“垃圾分辨”——可达性分析 ?

  • 所有引用的对象想象成一棵,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。其余的对象则被视为“死亡”的“不可达”对象,或称“垃圾”。
    示例如下图: object5,object6 和 object7 便是不可达对象,视为“死亡状态”,应该被垃圾回收器回收。
    可达树
  • GC Root 本身一定是可达的,这样从它们出发遍历到的对象才能保证一定可达。那么,Java 里有哪些对象是一定可达呢?主要有以下四种:
    • 虚拟机栈(帧栈中的本地变量表)中引用的对象。
    • 方法区中静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中 JNI 引用的对象。
  • 例如:
(1)改变对象的引用,如置为null或者指向其他对象。 
   Object x = new Object();  // object1 
   Object y = new Object();  // object2 
   x = y;  // object1 变为垃圾 
   x == y == null;  // object2 变为垃圾 

(2)超出作用域 
   if(i==0) { 
      Object x = new Object();  // object1 
   }  // 括号结束后object1将无法被引用,变为垃圾
   
(3)类嵌套导致未完全释放 
   class A { 
      A a; 
   } 
   A x = new A();  // 分配一个空间 
   x.a = new A();  // 又分配了一个空间 
   x=null;  // 将会产生两个垃圾 
   
(4)线程中的垃圾 
   class A implements Runnable {   
     void run () { 
       //.... 
     } 
   } 
   // main 
   A x = new A();  // object1 
   x.start(); 
   x = null;  // 等线程执行完后object1才被认定为垃圾 

四、垃圾清理方式 ?

  • 所有 GC Roots 不可达的对象都称为垃圾

  • 1️⃣标记-清理 (Mark-Sweep)
      第一步,所谓“标记”就是利用可达性遍历堆内存,把“存活”对象和“垃圾”对象进行标记,得到的结果如上图;
      第二步,既然“垃圾”已经标记好了,那我们再遍历一遍,把所有“垃圾”对象所占的空间直接 清空 即可。

结果如下:
在这里插入图片描述

  • 2️⃣标记-紧凑 (Mark-Compact)
      上面的方法会产生内存碎片,那好,我在清理的时候,把所有 存活 对象扎堆到同一个地方,让它们待在一起,这样就没有内存碎片了。
    结果如下:
    在这里插入图片描述
    以上两种方案适合存活对象多,垃圾少的情况

  • 3️⃣复制算法 (Copying)
      这种方法比较粗暴,直接把堆内存分成两部分,一段时间内只允许在其中一块内存上进行分配,当这块内存被分配完后,则执行垃圾回收,把所有存活对象全部复制到另一块内存上,当前内存则直接全部清空。
    结果如下:
    在这里插入图片描述
      起初时只使用上面部分的内存,直到内存使用完毕,才进行垃圾回收,把所有存活对象搬到下半部分,并把上半部分进行清空。
      这种方案适合 存活对象少,垃圾多的情况,这样在复制时就不需要复制多少对象过去,多数垃圾直接被清空处理。

五、Java 分代回收机制

  堆内存分为两大部分:新生代和老年代。比例为 1:2。老年代主要存放应用程序中生命周期长的存活对象。新生代又分为三个部分:一个 Eden 区和两个 Survivor 区,比例为 8:1:1。Eden 区存放新生的对象。Survivor 存放每次垃圾回收后存活的对象。

  • 刚刚创建的对象。在代码运行时会持续不断地创造新的对象,这些新创建的对象会被统一放在一起。因为有很多局部变量等在新创建后很快会变成不可达的对象,快速死去 ,因此这块区域的特点是存活对象少,垃圾多 。形象点描述这块区域为:新生代( Young Generation)
    • ?Eden:任何新进入运行时数据区域的实例都会存放在此;
    • ?Survivor S0:Eden 区存满了之后,经过垃圾回收没有被清除的实例,就搬到了 S0;
    • ?Survivor S1:同理,Survivor S0 区存满了之后,就从 S0 搬到了S1;
    • ?Survivor S0 <–> ?Survivor S1:配合着 GC 来回倒。
  • 存活了一段时间的对象。在 S0 和 S1 之间来回倒撑过15次之后(15岁成年)。我们把这些存活时间较长的对象放在一起,它们的特点是存活对象多,垃圾少 。形象点描述这块区域为:
    • ?老年代(Old Generation / tenured)
  • ?永久层:Permanent 存放运行时数据区的方法区

在这里插入图片描述

为什么不是一块 Survivor 空间而是两块?
  这里涉及到一个新生代和老年代的存活周期的问题,比如一个对象在新生代经历15次(仅供参考)GC,就可以移到老年代了。问题来了,当我们第一次 GC 的时候,我们可以把 Eden 区的存活对象放到 Survivor S0 空间,但是第二次 GC 的时候,Survivor S0 空间的存活对象也需要再次用 Copying 算法,放到 Survivor S1空间上,而把刚刚的Survivor S0 空间和 Eden 空间清除。第三次 GC 时,又把Survivor S1 空间的存活对象复制到 Survivor S0 空间,然后清空 Survivor S1 和 Eden 区,如此反复。

为什么 Eden 空间和 Survivor 空间要分成 8:1:1
  新创建的对象都是放在 Eden 空间,这是很频繁的,尤其是大量的局部变量产生的临时对象,这些对象绝大部分都应该马上被回收,能存活下来被转移到 survivor 空间的往往不多。所以,设置较大的 Eden 空间和较小的 Survivor 空间是合理的,大大提高了内存的使用率,缓解了 Copying 算法的缺点。当然这个比例是可以调整的。

不同的世代使用不同的 GC 算法

  • Minor collection(Minor GC):
    新生代中每次垃圾回收都要回收大部分垃圾对象,也就是说需要复制存活对象的操作次数较少,采用复制算法(Copying) 效率最高。并将原本 Survivor 区内经过多次垃圾收集仍然存活的对象移动到 Tenured 区。

  • Major collection(Full GC):
    老年代则会进行 Full GC,老年代的特点是每次回收都只回收少量对象,一般使用的是标记-紧凑(Mark-Compact) 算法。

六、Stop the World

  因为垃圾回收的时候,需要整个的引用状态保持不变,否则刚刚判定是垃圾,等我稍后回收的时候它又被引用了,这就全乱套了。所以,GC 的时候,其他所有的程序执行处于暂停状态,卡住了。
幸运的是,这个卡顿是非常短(尤其是新生代),对程序的影响微乎其微 (关于其他 GC 比如并发 GC 之类的,在此不讨论)。
  所以 GC 的卡顿问题由此而来(这就是华为方舟编译器在安卓上解决的问题)

参考资料 ?

  1. 理解 Java 垃圾回收机制
  2. JVM 的工作原理、层次结构以及 GC 工作原理
  3. Java 技术之垃圾回收机制

☁️Good Night, Babies. If you love ?

发布了26 篇原创文章 · 获赞 22 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Run_Bomb/article/details/89265525