面试常见之JVM垃圾回收机制GC详解

Posted 你这家伙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试常见之JVM垃圾回收机制GC详解相关的知识,希望对你有一定的参考价值。

作为 Java 语言最重要的特性之一的自动垃圾回收机制,也是基于 JVM 实现的。那么,自动垃圾回收机制到底是如何实现的呢?

1.GC是干啥的?

进行资源的回收

1.1.对于 C/C++ 而言

对于C/C++语言是没有GC机制的,对于内存的管理会比较麻烦,需要手动在合适的时机进行释放,而且一旦内存申请了没有释放,当我们后面程序再次申请内存的时候,发现内存已经被占用了,那么就会导致“内存泄漏”问题(非常严重),既然GC这么好

那么问题来了~
①. 既然GC这么好,为什么C/C++使用GC呢?
俗话说的好:“世界上没有免费的午餐”,是需要成本滴~

  • 首先资源付出的成本(CPU,内存……)支持垃圾回收机制能进行工作
  • GC对于内存的回收即时性没有那么高(虽然能在一定的时间内进行回收,但也不是不用了立刻就回收,有一定的延迟)
  • GC中存在一个致命的问题:STW问题(stop the word)

什么是STW问题?
就是当GC工作的时候,正常的业务代码可能无法执行,必须要等到GC工作完毕。对于服务器来讲,一旦服务器出发了GC,就可能导致正常代码无法执行(会出现卡顿),因为服务器可能会不断接收到请求,那么就会造成请求的积压,可能导致请求到一半就停止,或者是刚刚接收到请求而无法处理请求,直接造成用户体验的影响。

②. C/C++不是调用free/dalete函数就释放了吗?为什么还会出现“内存泄漏“问题?
答:上面提到需要在合适的时机进行释放,虽然内存内存的申请时机一般都是明确的,但是这个合适的释放时机是比较模糊的

1.2.对于 Java 而言

有GC机制,只管申请内存,无需关注内存的释放,而且可以很大程度上规避内存泄漏的问题(也不是完全没有)

2.GC要回收哪些内存?

1.PC程序计数器 / 栈 :都不要 GC 回收,因为他的释放时机很明确,随着线程的创建而申请内存,随着线程的销毁而释放。

2.常量池 / 方法区 :GC也不需要太关心,因为它占用的空间比较小,数据也很少会失去作用,如果对其进行GC,回收内存的性价比也不高(比如:打扫一个干净的小房间,即便你在怎么去打扫,也扫不出多少灰尘)

3.堆:GC主要回收的区域,堆的内存区域也很大,申请又很频繁(因为每次new对象都会在堆上申请内存)

2.1.堆上的内存分布


所谓的 “回收内存” => “回收对象”
GC工作的本质其实就是回收对象,以对象为基本单位(更方便),把不需要使用的对象找出来,回收掉即可,回收对象就会释放内存。

3.如何标记垃圾?

所有的垃圾回收大体都是遵循以下两个步骤:

1.标记:找出哪个对象是垃圾
2.回收:把标记的垃圾对象释放

标记的方式有很多种,这里讲述两种:引用计数 和 可达性分析
一般来说像python php 语言会用到”引用计数“,而Java一般使用第二种”可达性分析“。

3.1.引用计数

引用 的方法很简单,判定的效率也比较高,大部分情况都是一个不错的算法,即给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已”死“

引用计数的缺陷——”循环引用“

主流的JVM中没有选用引用计数法来管理内存,主要是因为引用计数法无法解决对象的循环引用问题
如:

class A{
    A ret = null;
}
public class Demo2 {
    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new A();
        a1.ret = a2;
        a2.ret = a1;
    }
}

分析:

3.2.可达性分析

Java不采用”引用计数法"来判断对象是否已”死“,而是采用"可达性分析"来判断对象是否存活。
算法核心:通过一些列成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为“引用链”,当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达),证明此对象时不可用的。

上图如果看不太懂,可以看下图,如:

**总结:**可达性分析,就是从GC Roots开始进行遍历,看到哪些对象是能够被访问到的(某个对象在遍历过程中只要被访问一次,也可以被访问多次),那么剩下的就是垃圾。实际对象之间的相互引用关系不是简单的树形结构,而是“图”结构

可达性分析可视为GC Roots的对象包括以下几种

  • 栈上的局部变量表中的引用(每个程序有多个线程,每个线程有自己的栈,每个栈又有很多层栈帧,每层栈帧又有很多局部变量,这些内容试试GC Roots的一部分)
  • 常量池中引用指向的对象
  • 方法区中静态引用指向的对象

Java对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。

  • 强引用:既能用于找到对象,也能决定对象的生死
  • 软引用:能用于找到对象,能够一定程度上的决定对象的生死(能保对象一时,内存如果充裕,不会回收软引用指向的对象,但是如果内存紧缺了,就会回收该对象)
  • 弱引用:只能用于找对象,不能决定对象的生死
  • 虚引用:不能用于找对象,也不能够决定对象的生死,只能在对象消亡之前,收到通知做一些善后工作

3.3.回收方法区垃圾

目前很多框架大量使用到反射,方法去会被加载很多类,那么此时也是需要回收的

方法区(永久代)的垃圾回收主要内容:废弃常量和无用的类(类对象(类的卸载)=>(和类的加载对应))

判定一个类是否是"无用类"则相对复杂很多。类需要同时满足下面三个条件才会被算是"无用的类" :

  • 1.该类的所有实例已经被报备回收了
  • 2.加载该类的类加载器也已经被回收了
  • 3.该类的类对象没有在任何地方使用(其他的代码也没有在通过反射来使用这个类对象)

JVM可以对同时满足上述3个条件的无用类进行回收,也仅仅是"可以"而不是必然。在大量使用反射、动
态代理等场景都需要JVM具备类卸载的功能来防止永久代的溢出。

讲完了垃圾是如何标记的,下面我们谈谈垃圾又是如何回收的呢

4.如何回收垃圾

4.1.标记 - 清除算法

将需要回收的对象全部标记出来,标记完成后将所有已标记的对象统一回收


标记 - 清除算法的缺陷

  1. 空间问题:我们不难发现上图清除垃圾之后,可分配的内存空间并不是连续的,这就是产生了“内存碎片问题”,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集
  2. 效率问题:标记和清除这两个过程的效率都不高

4.2.标记 - 复制算法

能够解决内存碎片问题
"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效

标记 - 赋值算法的缺陷

  1. 如果要保留的对象比较少,问题不大;如果要保留的对象很多,复制开销就比较大
  2. 需要有足够的内存空间(可以一分为二),空间不够,也无法施展赋值操作

4.3.标记 - 整理算法

a) 没有内存碎片
b)空间不是很充裕的时候也能进行回收

标记 - 整理算法的缺陷

  • 效率比较低:当对象比较多的时候,要频繁搬运

5.如何分类垃圾

实际上在进行垃圾回收分类的时候,会根据不同的场景来分别使用不同的垃圾回收算法,展示的垃圾回收器不是仅仅使用单一的回收算法,会根据不同的情况来进行综合运用

5.1.分代收集算法

如何进行分代回收:所谓的分代指的是年龄(单位不是时间),而是看这个对象能够躲过垃圾回收器标记未垃圾的次数,垃圾回收器会创建一些线程,移至堆程序中的对象进行标记(周期性,标记的过程为“可达性分析”)

某个对象在遍历之后是否被标记为垃圾,得取决于代码的具体逻辑:有的对象可能在代码中用完就丢掉了;有的在下一轮可达性分析的时候,就会立刻发现这个对象;有的对象在代码中用的比较持久;有的对象在几个“可达性分析”周期下来之后,都会发现这个对象还挺有用,就会保留下来。(比如:一般来说公司没半年或一年会进行绩效考核,当第一轮下来时候,其中有的员工会因为绩效太低而被裁员,而有的员工在几轮考核下来,绩效仍然是排在前面,那就说明这个人的能力很大,就会保留下来,同时,这个员工经历的“绩效考核”轮次越多,那么这个员工的资历就越老。)

对于分代收集算法:

  • 如果一轮可达性分析下来,对象是有用的,那么年龄+1;
  • 如果一轮可达性分析下来,对象是没有用的,就直接标记成垃圾准备回收;
    往往在一轮扫描下来的大部分对象都是垃圾,只有少数会留下来

堆上的区域的划分(GC角度)

JVM对象的一生:

  1. 对象在创建的时候,出生在“新生代 的 伊甸区”
  2. 大部分的对象都是“朝生夕死”,生命周期比较短,伊甸区中可能无时无刻都在诞生出很多新对象,但是这些对象都活不过一轮GC。
  3. 如果伊甸区的对象活过了第一轮GC,就会被放到生存区(伊甸区到生产区,就是复制算法)
  4. 生存区中的对象也要继续经历后续GC轮次的考验(两个生存区会来回进行GC)
  • 如果生存区的对象没有活过后去的GC,也还是会被直接回收
  • 如果下一次GC也货过去,仍然使用复制算法,把活下来的对象,拷贝到另外一个生存区中
  1. 如果在生存去中,经历了若干次GC之后,仍然存活,那就把这个对象放到老年代
  2. 当对象进入老年代之后,GC的考察频率就没有那么高了(经验认为,如果一个对象已经持续使用很久了,那么就会继续的持续下去)
    但是不乏有特殊对象可以直接进入老年代(就好比到一个亲戚公司入职)

垃圾回收过程中的一些术语:

  1. Partical GC 只回收内存一部分区域
  • 只针对新生代GC, Minor GC(比较频繁,速度比较快)
  • 只针对老年代GC ,Major GC(没有那么频繁,速度比较慢)
  1. Full GC 回收全部内存区域

可以理解为Full GC => Major GC + Minor GC

GC常见面试题

  1. 引用计数
  2. 可达性分析
  3. 分代回收,新生代(伊甸区+生存区)+老年代
  4. 一个对象的一生
  5. 三种常见的回收算法

以上是关于面试常见之JVM垃圾回收机制GC详解的主要内容,如果未能解决你的问题,请参考以下文章

Java面试之——GC垃圾回收机制

JVM 架构解释 + 垃圾回收机制 详解(基于JDK8版本)

JVM的垃圾回收机制详解和调优

jvm基础--GC垃圾回收机制

Java虚拟机(JVM)与垃圾回收机制(GC)的详解

Java垃圾回收机制(GC)详解