面试常见之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.标记 - 清除算法
将需要回收的对象全部标记出来,标记完成后将所有已标记的对象统一回收
标记 - 清除算法的缺陷
- 空间问题:我们不难发现上图清除垃圾之后,可分配的内存空间并不是连续的,这就是产生了“内存碎片问题”,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集
- 效率问题:标记和清除这两个过程的效率都不高
4.2.标记 - 复制算法
能够解决内存碎片问题
"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效
标记 - 赋值算法的缺陷
- 如果要保留的对象比较少,问题不大;如果要保留的对象很多,复制开销就比较大
- 需要有足够的内存空间(可以一分为二),空间不够,也无法施展赋值操作
4.3.标记 - 整理算法
a) 没有内存碎片
b)空间不是很充裕的时候也能进行回收
标记 - 整理算法的缺陷
- 效率比较低:当对象比较多的时候,要频繁搬运
5.如何分类垃圾
实际上在进行垃圾回收分类的时候,会根据不同的场景来分别使用不同的垃圾回收算法,展示的垃圾回收器不是仅仅使用单一的回收算法,会根据不同的情况来进行综合运用
5.1.分代收集算法
如何进行分代回收:所谓的分代指的是年龄(单位不是时间),而是看这个对象能够躲过垃圾回收器标记未垃圾的次数,垃圾回收器会创建一些线程,移至堆程序中的对象进行标记(周期性,标记的过程为“可达性分析”)
某个对象在遍历之后是否被标记为垃圾,得取决于代码的具体逻辑:有的对象可能在代码中用完就丢掉了;有的在下一轮可达性分析的时候,就会立刻发现这个对象;有的对象在代码中用的比较持久;有的对象在几个“可达性分析”周期下来之后,都会发现这个对象还挺有用,就会保留下来。(比如:一般来说公司没半年或一年会进行绩效考核,当第一轮下来时候,其中有的员工会因为绩效太低而被裁员,而有的员工在几轮考核下来,绩效仍然是排在前面,那就说明这个人的能力很大,就会保留下来,同时,这个员工经历的“绩效考核”轮次越多,那么这个员工的资历就越老。)
对于分代收集算法:
- 如果一轮可达性分析下来,对象是有用的,那么年龄+1;
- 如果一轮可达性分析下来,对象是没有用的,就直接标记成垃圾准备回收;
往往在一轮扫描下来的大部分对象都是垃圾,只有少数会留下来
堆上的区域的划分(GC角度)
JVM对象的一生:
- 对象在创建的时候,出生在“新生代 的 伊甸区”
- 大部分的对象都是“朝生夕死”,生命周期比较短,伊甸区中可能无时无刻都在诞生出很多新对象,但是这些对象都活不过一轮GC。
- 如果伊甸区的对象活过了第一轮GC,就会被放到生存区(伊甸区到生产区,就是复制算法)
- 生存区中的对象也要继续经历后续GC轮次的考验(两个生存区会来回进行GC)
- 如果生存区的对象没有活过后去的GC,也还是会被直接回收
- 如果下一次GC也货过去,仍然使用复制算法,把活下来的对象,拷贝到另外一个生存区中
- 如果在生存去中,经历了若干次GC之后,仍然存活,那就把这个对象放到老年代
- 当对象进入老年代之后,GC的考察频率就没有那么高了(经验认为,如果一个对象已经持续使用很久了,那么就会继续的持续下去)
但是不乏有特殊对象可以直接进入老年代(就好比到一个亲戚公司入职)
垃圾回收过程中的一些术语:
- Partical GC 只回收内存一部分区域
- 只针对新生代GC, Minor GC(比较频繁,速度比较快)
- 只针对老年代GC ,Major GC(没有那么频繁,速度比较慢)
- Full GC 回收全部内存区域
可以理解为Full GC => Major GC + Minor GC
GC常见面试题
- 引用计数
- 可达性分析
- 分代回收,新生代(伊甸区+生存区)+老年代
- 一个对象的一生
- 三种常见的回收算法
以上是关于面试常见之JVM垃圾回收机制GC详解的主要内容,如果未能解决你的问题,请参考以下文章