文章目录
五、垃圾回收(☆)
1.垃圾回收概述
1.什么是垃圾?
没有任何引用指向的一个对象或者多个对象(循环引用)
2.为什么需要GC?
- 如果不断进行内存分配而不进行垃圾回收,内存迟早会被消耗完;
- GC可以解决内存中的碎片化问题,从而能够为较大的对象分配足够的内存空间;
- 随着应用程序的不断完善,用户越来越多,更需要GC来进行性能优化。
3.垃圾回收的主要区域
- 方法区:主要回收常量池中废弃的常量(字面量及符号引用)及不再使用的类型
- 堆空间:回收垃圾(没有任何引用指向的一个对象或者多个对象(循环引用))
2.垃圾回收相关算法
判断对象存活方式:引用计数算法和可达性分析算法
1.标记阶段(表明什么对象需要回收)
1.引用计数算法
原理:每个对象都有一个引用计数器,来记录对象被引用的次数,每增加一次引用计数器+1;每减少一次引用计数器-1,当引用计数器记录值为0时,就表名该对象没有被任何对象引用,则表名该对象为垃圾。
优缺点:
优点:实现简单,垃圾对象便于识别;回收效率高;
缺点:出现循环引用的情况
-
无法处理循环引用的情况;从而造成内存泄露;
-
空间:因为需要计数器,所以造成额外的空间开销;
-
时间:需要频繁的加1或者减1操作,造成一定的时间开销;
举例:
2.可达性分析算法(根搜索算法)
GC Roots:一组活跃引用的根对象集合
基本原理:以根对象为起始点,从上到下搜索被根对象所连接的目标是否可以到达,内存中的存活对象都直接或者间接地与根对象集合相连,搜索所走过的路径被称为引用链,如果目标对象没有被任何引用链相连,则为不可到达,即为内存中的垃圾。
优点:可以解决循环引用的问题;
缺点:相较于引用计数算法来说,回收效率稍慢一些。
3. 可以被当做GC Roots的元素有哪些?(重点是堆外保存堆内对象的地址的那些区域)
- 虚拟机栈中引用的对象;如:各个线程中被调用的方法的局部变量等
- JNI(本地方法栈中引用的对象)
- 方法区(元空间)静态属性引用的对象:如静态变量
- 方法区(元空间)中常量引用的对象。如字符串常量池中引用的对象
- 所有被synchronized持有的对
- 基本数据类型对应的Class对象、异常类对象及系统类加载器
- 还需要一些“临时性”对象加入GC Roots结合中,如只针对新生代回收,则堆中新生代以外的引用对象也需要加入GC Roots集合中。
2.标记阶段(表明什么对象需要回收)
1.什么是对象的finalization机制
在gc 回收某个对象之前,会先调用对象的finalize()方法。Object类中的finalize()方法没有任何方法体,对象类可以重写这个方法。可以在对象销毁之前做一些操作。
finalize()的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存.所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作.
2.虚拟机中的对象有哪几种可能的状态(生存还是死亡)?
- 可触及的:能够与引用连相连接的对象;
- 可复活的:没有任何引用的对象,但是可以在finalize()中被复活的对象;
- 不可触及的:对象的finalize()被调用,但是没有被复活的对象。**finalize()**方法只能被调用一次。
3. 判断一个对象是否可以被回收所经历的两次标记过程(如何判断一个对象是否可以被回收?)(面试重点)
-
如果对象到GC Root没有引用链连接或者引用计数,则进行第一次标记;
-
进行筛选,判断是否有必要调用对象的 finalize() 方法
- 对象类没有重写finalize()方法或者已经调用过了finalize()方法,则为不可触及的, 直接回收;
- 对象重写了该方法但是还没调用过该方法,则回收前会先调用此方法;
-
执行二次标记:如果执行finalize()方法后,该对象与引用链上的任何一个对象建立连接,则为复活状态,否则判定为不可达状态。
3.清除阶段(如何回收垃圾)
1.标记-清除算法
- 原理:当需要进行GC时,会停止整个程序(STW),然后整个GC过程分为标记阶段和清除阶段
标记:从引用根对象开始遍历,标记所有被引用的对象,一般在对象头中记录为可达对象。
清除:对堆内存进行从头到尾的遍历,如果发现某个对象的对象头没有被标记为可达对象,则进行回收
-
优点:最基本的垃圾回收算法
-
缺点:
- 因为需要遍历,所以执行效率不高;
- 在进行GC时,需要STW,效率不高的话,会影响用户体验;
- 产生内存碎片化问题
2.复制算法(适合在新生代)
- 原理:将内存空间分为大小相等的两块,每次只使用其中的一块,垃圾收集时,将正在使用的内存中的存活的对象复制到另一块内存中,然后将正在使用的内存清空。交换两个内存的角色,完成垃圾回收。在堆中新生代分为eden,s0,s1,其中s0,s1就是用的复制算法
-
优点
- 执行效率高,省去了清除中的遍历问题
- 解决了内存碎片化的问题
-
缺点
- 空间浪费较明显,始终有一块内存无法使用;
- 对于G1这种分成很多region的垃圾回收器来说,复制意味着需要维持region之间对象的引用关系,空间和时间的开销比较大。
-
适合场景
- 比较适合于垃圾对象较多,存活对象较少的区域,如新生代中的Survivor0区和Survivor1区。不适合老年代中垃圾回收。
3.标记-整理(压缩)
- 原理
- 第一阶段和标记-清除算法的标记阶段一样,从根结点开始标记所有被引用的对象;
- 第二阶段将所有存活的对象压缩到内存的一端,按顺序排放,然后清除边界以外的内存空间
-
优点
- 解决了标记-清除内存碎片化的问题
- 解决了标记-复制空间浪费一半的问题
-
缺点
- 整理过程中需要移动对象,如果对象被其他对象所引用,则需要不断调整引用的地址;
- 效率相对另外两种算法较低;
-
适用场景:适合于老年代中的垃圾收集
4.三种垃圾回收算法对比
算法 | 标记-清除 | 复制 | 标记-压缩(整理) |
---|---|---|---|
执行效率 | 中等 | 最快 | 最慢 |
空间开销 | 少(存在内存碎片化 | 大(浪费一半空间) | 小(不会产生内存碎片化 |
移动对象 | 否 | 是 | 是 |
5.分代收集理论
目的:不同声明周期的对象可以采用不用的回收算法,以提高整体的回收效率。
HotSpot虚拟机中的回收策略
-
新生代
- 新生代特点:区域相对老年代小,对象声明周期短,回收频繁
- 针对这种情况应当采用复制算法,回收效率高,针对于空间利用率不高的问题,采用两个Survivor区得以缓解;
-
老年代
- 老年代特点:相对于新生代大,对象的声明周期长,回收不频繁。
- 针对这种情况采用标记-清除+标记-整理相结合的方法,首先标记阶段还是采用两者中的标记方法,即从根对象开始,依次标记所有的存活对象,然后采用清除算法,暂时容忍碎片化问题,等到碎片化问题影响到内存分配时,再采用整理算法,整理碎片化内存。
6.增量收集算法(实际就是每次只收集一部分)
如果一次性将所有垃圾进行回收,需要造成系统长时间的停顿,影响用户体验,可以让垃圾收集线程和用户线程交替执行,每次垃圾收集线程只收集一部分的内存空间,接着切换到应用程序,依次反复,直到垃圾收集完成。
优点:低延迟,用户体验更优;
缺点:吞吐量下降
7.分区收集算法
将整个堆空间划分为连续不等的小区间,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间。每一个小区间都独立使用,独立回收。
3.垃圾回收相关概念
1. System.gc()
会显示触发Full GC,但是无法保证对垃圾收集器的调用
2.内存溢出与内存泄露
1. 内存溢出(OOM-Out-Of-Memory)
没有空闲内存,并且垃圾收集器也无法提供更多内存。
2.没有空闲内存的原因
- 设置的堆内存太小,此时可以通过-Xms:和-Xmx来设置堆空间的起始大小和最大大小;
- 代码中创建了大对象,并且长时间不能被垃圾回收器所收集;
3.内存泄露:(非常重要)
- 严格意义:对象不会被程序用到,但是GC又没有办法回收掉该对象,此时就称为发生了内存泄露;
- 宽泛意义:实际情况中,一些不好的编程实践造成了对象的声明周期变得很长甚至导致OOM,(如本来可以定义为方法内部的局部变量,定义为类的成员变量甚至定义为静态变量(随着类的加载而加载,随着类的消亡而消亡)造成变量的声明周期加长,本来方法弹栈后可能就会被释放(没有发生逃逸))。
- 举例说明内存泄露(举出关于内存泄露的例子)
- 单例的生命周期和程序一样长,单例程序中如果存在对外部对象的引用,则外部对象是没有办法被回收的,会造成内存泄露
- 一些提供 close() 的资源未关闭而导致内存泄露(如数据库的连接必须手动close,否则不能被回收)
3.STW
无论那种垃圾收集器,在进行垃圾收集的过程中,都会使程序停顿,这称为STW,被STW中断的程序在完成GC后会自动恢复。
目的:为了保证数据的一致性(不能统计垃圾的时候还一边造垃圾)
4.垃圾回收并行与并发
1.程序的并行与并发
- 并发:从一段时间来看,有多个任务在执行,从单一的时间点上来看,只有一个任务在执行,时间上是CPU在快速切换任务交替执行;
- 并行:当系统有多个CPU时,一个cpu可以执行一个进程,另一个cpu可以执行另一个进程,两个进程不会互相抢cpu资源,可以同时进行,此称为并行。
- 对比:并发指的是同一个时间段,多个任务发生了,且抢占cpu资源;并行指的是在用一个时间点,多个任务发生了,不抢占cpu资源;
2.垃圾回收的并行与并发
-
并行:多条垃圾回收线程并行执行,此时需要停顿用户线程
-
串行:只有一条垃圾回收线程,当内存不够时,程序暂停,启动垃圾回收,回收完,再启动程序的线程。
用户线程与垃圾回收线程同时执行,垃圾回收线程在执行时,不会停顿用户程序的执行。
用户程序在继续进行,而垃圾收集线程运行在另一个CPU,如:CMS和G1。
5.强引用(Strong Reference)
- 定义:在java程序中通过new创建了一个对象,并将其赋值给一个变量,该变量就称为指向该对象的一个强引用
- 适用场景:99%以上的都是强引用。
- 垃圾回收:强引用的对象都是可触及的,GC不会回收掉被强引用的对象(强引用,不回收)。
6. 软引用(Soft Reference)
- 定义:描述的是一些还在用,但是非必须的对象。
- 适用场景:通常用来实现内存敏感的缓存,如高速缓存就用到软引用。
- 垃圾回收:内存不足即回收
7. 弱引用(Weak Reference)
- 定义:弱引用也是描述那些非必须的对象,与软引用的区别在于GC时,对于软引用来说需要判断当前内存是否不足,不足的话才进行回收,而对于弱引用不需要进行判断直接回收。
- 适用场景:保存那些可有可无的缓存数据。
- 垃圾回收:发现即回收
补充面试题:你用过weakHashMap吗?
采用weakHashMap进行存储后,当内存不足时,能够对该部分进行回收,因为内存的Entry<k,v>类继承了弱引用类。
8.虚引用(Phantom Reference)
- 定义:所有引用中最弱的一个,为一个对象设置虚引用关联的目的是跟踪垃圾回收的过程,被回收后可以发出相应的通知。
- 适用场景:实现跟踪对象的垃圾回收过程
- 垃圾回收:跟踪对象回收过程
4.垃圾回收器
1.垃圾回收主要性能指标
- 吞吐量:用户程序运行时间/用户程序运行时间+垃圾回收时间
- 吞吐量越高越好,这样会提升用户体验,认为只有应用程序在执行。
- 暂停时间:执行垃圾收集时,程序被暂停的时间。
- 暂停时间越低越好,对于交互式应用程序,暂停时间越长,越容易出现卡顿现象,影响用户体验。
- 现在标准:在最大吞吐量优先的情况下,降低停顿时间。
2.垃圾回收器概述(分类)
串行 (STW时只有一个垃圾回收线程):Serial 及Serial Old
并行(STW时有多个垃圾回收线程):ParNew、Parallel Scavenge及Parallel Old (其中old指的是老年代)
并发(垃圾回收线程和用户线程并发执行):CMS及G1
3. 垃圾回收器的组合关系
新生代:Serial, ParNew Parallel Scavenge
老年代:Serial Old Parallel Old和CMS
新生代和老年代:G1
组合关系
补充:
- jdk9中移除了Serial+CMS和ParNew+Serial Old这两种组合,上图红色虚线部分
- jdk14中,删除了CMS垃圾回收器。
目前组合
目前的组合:Serial +Serial Old; Parallel Scavenge+Parallel Old和G1
4. Serial垃圾回收器(串行回收)
1.Serial垃圾回收器
采用复制算法、串行收集和Stop The World的方式来对新生代进行垃圾收集。是HotSpot虚拟机在客户端模式下默认的新生代垃圾回收器。
2.Serial Old垃圾回收器
采用标记-整理算法、串行收集和Stop The World的方式来对老年代进行垃圾收集。是HotSpot虚拟机在客户端模式下默认的老年代垃圾回收器。
3.回收过程
4.回收优势
因为是单线程,因此没有线程交换的开销,相对于其他垃圾收集器的单线程相比,简单而高效。
5.参数设置
-XX:useSerialGC/useSerialOldGC设置
5.ParNew垃圾回收器(并行回收)
1.回收模式
采用复制算法、并行回收和STW的机制进行垃圾回收,是JVM在服务端下的默认新生代垃圾回收器。
2.组合搭配
可以和Serial Old搭配使用(JDK9中移除);可以和CMS配合使用(JDK14中删除)
3.优势
对于新生代,回收次数频繁,因此采用并行方式更加高效。
4.参数设置
-XX:useParNewGC
6.Parallel垃圾回收器(吞吐量优先)
1.Parallel Scavenge垃圾回收器(新生代)
采用复制算法、并行回收及STW的机制进行垃圾收集。
2.Parallel Scavenge垃圾回收器与ParNew的区别
Parallel Scavenge的目标是达到一个可控吞吐量,而且具备自适应调节策略。
3.Parallel Old垃圾回收器(老年代)
采用标记-整理、并行回收及STW的机制进行垃圾收集,在JDK8中,默认Parallel Scavenge+Parallel Old为垃圾回收器。
4.适用场景
高吞吐量则可以高效的利用cpu的时间,来快速实现计算。主要适合在后台运行而不需要太多交互的任务
7.CMS(Concurrent Mark Sweep)垃圾回收器(低延时,老年代)
1.概念
CMS(Concurrent Mark Sweep)垃圾回收器是HotSpot虚拟机第一款并发垃圾回收器,实现了垃圾收集线程和用户线程同时工作。是基于标记-清除算法,并发回收的老年代垃圾收集器,只搭配ParNew和Serial使用,在jdk14时,CMS垃圾回收器被删除。
2.工作原理
- 初始标记:仅仅只是标记出GC Roots能够直接关联到的对象,存在STW机制(很短暂的STW)。
- 并发标记:从GC Root直接关联到的对象开始,遍历整个对象图的过程,该阶段不需要停顿用户线程。
- 重新标记:修正并发标记阶段,因用户线程运行而导致标记产生变动的那一部分对象的标记记录,存在STW机制,会产生浮动垃圾。
- 并发清除:清除标记为已经死亡的对象,释放内存空间。
3.优缺点
-
优点:并发收集;低延迟;
-
缺点:
- 会产生内存碎片,当老年代需要为大对象分配内存时,不得不提前触发Full GC;
- 无法处理"浮动垃圾",可能导致"并发失败",从而引发Full GC
- 对CPU资源比较敏感:CMS默认的垃圾回收线程数为(处理器核心数+3)/4,对于处理器核心数比较少的情况,垃圾回收线程就占比较大,影响执行速度。
浮动垃圾:因为在并发清理阶段,垃圾收集线程和用户线程在并发执行,用户线程运行过程中,会产生新的垃圾,而这部分垃圾是发生在标记阶段之后的,所以只能等到下一次GC时,才能够进行回收,这时候需要预留出一定的内存空间,在下图中,a=null后b和c就是垃圾了但是在这一轮垃圾回收中判定不是垃圾,但是会在下次垃圾回收中回收。
4.使用场景:适用于强交互的应用
5.参数设置
- -XX:+UseConcMarkSweepGC:设置使用CMS垃圾收集器;
- -XX:CMSinitialingOccupanyFraction:设置堆内存使用率阈值;jdk5,默认为68%;jdk6.默认为92%。
- -XX:ParallelCMSThreads:设置CMS的线程数量
6.关于CMS的面试题
- 为什么说CMS是一款低延时的垃圾回收器?
因为在初始标记阶段和重新标记阶段,会发生STW,但是此部分时间很短,而在并发标记和并发清除阶段,虽然占据的时间比较长,但是在此期间,垃圾回收线程和用户线程并发执行,因此为低延时垃圾回收器
- CMS会产生内存碎片化问题,为什么不用标记整理算法呢?
因为CMS在并发标记和并发清除阶段,用户线程和垃圾回收线程并发执行,而标记整理算法中存在对象在内存中的定向移动,用户线程在执行过程中,如果发生移动,会造成安全问题。因此无法采用标记-整理算法。
8.G1垃圾回收器
1.概述
- 目标是在停顿时间可控的情况下尽可能的提高吞吐量的垃圾收集器
- 将整个堆内存分为多个region区,跟踪各个Region区的垃圾堆积的价值大小,然后后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值大的region.
- 基于复制算法和标记-整理算法的并行垃圾回收器,为jdk9之后默认的垃圾回收器。
2.回收过程(整体)
整体分为:年轻代GC(Young GC), 并发标记老年代,混合回收过程,如果G1失效,Full GC作为后备机制
- 当伊甸园区满之后,开始进行年轻代回收,移动存活对象到Survivor区或者Old区;
- 当堆内存到达一定阈值(默认45%)时,启动并发标记老年代;
- 并发标记结束后,进行混合回收,收集老年代时,一次只需回收一部分老年代对象,(因为存在时间限制)。
3.优缺点
1.优点
- 并发与并行
并行性:在G1垃圾回收期间,可以有多条垃圾回收线程同时进行回收,有效利用多核计算能力。
并发性:在G1并发标记阶段,允许垃圾收集线程和用户线程并发执行。
- 分代收集
G1同时兼顾了年轻代和老年代。因为G1垃圾收集器将堆空间分为不同的Region区,这些区域包含了逻辑上的新生代和老年代。
- 不存在内存碎片化问题
因为G1垃圾回收器以Region为基本内存回收单元,Region之间采用复制算法,但是从整体上来说,是采用标记-整理算法,因此可以避免内存碎片问题。
- 可预测的停顿时间模型(软实时)
使使用者明确在一个长度为M的时间片段内,用于垃圾回收的时间不超过N
主要是因为G1跟踪各个Region的垃圾堆积价值的大小,然后后台维护一个优先级列表,在有限的时间内,优先收集价值大的Region。
2.缺点(不适合小内存应用)
由于垃圾收集产生的内存占用相对大,小内存应用上性能不如CMS。
4.面向服务端的垃圾收集器,主要针对配备多核CPU和大容量内存的机器(低延时,大内存)。
5.Region相关介绍
- 大小:G1将整个堆大小分为约2048个Region,每个Region大小相同,且为2的n次幂(一般在1M-32M之间),且在JVM的生命周期内不会改变。
- 分布:一个Region有可能属于伊甸园区,S区或者Old区。G1还在堆内存中存放了一个Humogous区,简称H区,当对象大小超过1.5倍的Region区时,将其称为大对象,放在H区。(问题:如果是1.2倍Region大的对象放在哪儿?)
- 设置H区的原因?(防止大对象直接进入老年代)
原因在于,如果不设置H区,那么大的对象则会直接进入到老年代,但是如果这个对象的声明周期比较短,则会长时间存在,并且占据着较大内存,所以将其存放在H区,如果对象大于一个H区大小,则会存在在连续的H区中。如果整个H区都装不下,则会触发Full GC.
- 内存分配
Region区采用指针碰撞的方式进行内存分配,并且每个Region内部存在TLAB(线程本地分配缓冲区)。
6.Remembered Set(记忆集)
- 存在必要性:因为存在跨Region引用的存在,判断存活时,如果挨个遍历每个Region的话,势必造成效率降低,于是引进了记忆集。
- 原理:每个Region都有一个记忆集,当进行引用数据写入时,先判断是否存在其他Region的引用关系,如果存在的话,则将该引用关系对应的对象写入记忆集的卡表(CardTable)中,然后在GC过程中将Rset加入到GC Roots中
7.回收过程(具体)
- 年轻代GC(与之前讲的一样,只不过是基于分区思想)
JVM优先分配对象到伊甸园区,当伊甸园区满了之后,开始进行年轻代GC,然后,存活下来的对象进入到S区,当S区的对象分代年龄达到阈值后,进入到老年代中,清理过程中的算法采用复制算法。
-
并发标记老年代(深入理解java虚拟机中此部分为G1的收集过程)
- 初始标记:只标记GC Roots直接关联到的对象,此过程需要STW(短暂STW);
- 并发标记:从GC Root关联到的对象开始,递归遍历整个对象图,此时垃圾收集线程和用户线程并发执行
- 最终标记(类似于CMS重新标记):由于在并发标记阶段,用户线程在执行,需要对标记结果进行修正,此时需要STW.
- 筛选回收:根据用户设置停顿时间,采用复制算法进行清理(将决定回收的Region中的存活对象复制到空的Region中,然后清空旧的Region空间),此过程需要STW。
- 混合回收
//TODO
8.垃圾回收器总结
9.如何选择垃圾回收器
- 优先调整堆的大小,让JVM能够适应;
- 如果是内存比较小,选择Serial +Serial Old垃圾回收器;
- 如果是单核,没有停顿时间要求,选择Serial +Serial Old垃圾回收器;
- 如果是多核CPU,需要高吞吐量,并且停顿时间不长,选择Parallel+Parallel Old垃圾回收器;
- 如果是多核CPU,低停顿时间
- 如果是jdk14以前,则可以选用ParNew+CMS或者G1;jdk14后,选择G1;