垃圾回收
目录:
A:垃圾回收概述:
B:垃圾回收相关算法:
C:垃圾回收相关概念:
D:垃圾回收器章节概述
你知道哪几种回收器,各自的优缺点,重点讲一下cms和g1
一面:JVM GC算法有哪些,目前的JDK版本采用什么回收算法
一面:G1回收器讲下回收过程
GC是什么?为什么要有GC?
一面:GC 的两种评定方法?CMS 收集器与 G1 收集器的特点。
百度:
说一下GC算法,分代回收
垃圾回收策略和算法
天猫:
一面:jvm GC原理,JVM怎么回收内存
一面:CMS特点,垃圾回收算法有哪些?各自的优缺点,他们共同点的缺点是什么?
滴滴:
一面:java的垃圾回收器都有哪些,说下g1的应用场景,平时你是如何搭配使用功能垃圾回收器的
京东:
你知道哪几种垃圾回收器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点垃圾回收算法的实现原理。
阿里:
讲一讲垃圾回收算法。
什么情况下触发垃圾回收?
如何选择合适的垃圾收集算法?
JVM有那三种垃圾回收器?
字节跳动:
常见的垃圾回收器算法有哪些,各有什么优势?
system.gc()和runtime.gc()会做什么情况?
一面:Java GC机制?GC Roots有哪些?
二面:Java对象的回收方式,回收算法。
CMS和G1了解吗,CMS解决了什么问题,说一下回收过程。
CMS回收停顿了几次,为什么要停顿两次。
- 垃圾回收是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
- 如果不及时对内存中的垃圾进行垃圾回收,那么,这些垃圾对象所占的内存空间一直保留到应用程序结束,被保留的空间无法被其他对象使用。可能导致对象的内存溢出。
为什么需要垃圾回收?
- 内存迟早都会被消耗完
- JVM 将整理的内存分配给新对象
- 没有GC就不能保证应用程序的正常进行。
我们通过new关键字进行内存申请,并使用delete关键字进行内存释放。
这种方式可以灵活控制内存释放的时间,但频繁申请和释放申请内存的管理负担。一旦我们忘记了回收,将产生内存泄漏,永远无法清理,随着时间的累计,直到内存出现OOM。
垃圾回收后的代码:这种自动化的内存分配合垃圾回收的方式已经成为现代开发语言必备的标准。
- 优点:
自动内存管理,降低内存泄漏金额内存溢出的风险
自动内存管理机制,可以更专心的专注于业务开发
- 缺点:
弱化Java开发人员在程序出现内存溢出是定位问题和解决问题的能力。
所以我们需对这项技术实施必要的监控和调节。
- 垃圾回收工作区域:
垃圾回收器可以对年轻代回收,也可老年代回收,甚至是全栈和方法区的回收。
E 其次,Java堆是垃圾收集器的工作重心。
从次数上讲:频繁收集Young区,较少收集Old区,基本不动Perm区(或元空间)。
引用计数算法
- 垃圾标记阶段:需要区分内存中哪些存活对象, 哪些对象已经死亡。只有被标记为死亡的对象,GC才会收它,释放掉内存空间。
- JVM中如何标记一个死亡对象?当一个对象不在被任何的存活对象继续引用时,可以宣判为已经死亡。
- 判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
- 引用计数算法:对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。
- 场景:停车场进去一辆车,计数器则记录总停车数减少一个,走一辆车总停车数则增加一个,当计数器为零后,表示没有停车位了,不可在使用了
- 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
- 缺点:最为致命 -- 即无法处理循环引用,导致Java的垃圾回收器中没有使用这一类算法。
B:Java代码例子 -- Python的引用计数实施方案
package com.ylc.java07;
/**
证明:java使用的不是引用计数算法
* @Author ylc
* @Date 2022/3/28 14:58
* @Version 1.0
*/
public class RefCountGC {
//这个成员属性唯一作用就是占用内存
private byte[] bigSize = new byte[5 * 1024 * 1024];//5mb
Object reference = null;
public static void main(String[] args) {
RefCountGC obj1 = new RefCountGC();
RefCountGC obj2 = new RefCountGC();
obj1.reference = obj2;
obj2.reference = obj1;
obj1 = null;
obj2 = null;
//显示的执行垃圾回收行为
//这里发生GC,obj1和obj2能否被回收?
System.gc();
}
}
小结:
- 引用计数算法,是很多语言的资源回收选择,它更是同时支持引用计数和垃圾收集机制。
- Python如何解决循环引用?
手动解除:就是在合适的时机,解除引用关系。
使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环引用。
可达性分析算法(或根搜索算法,追踪性垃圾收集)
- 作用:有效的解决了在引用计数算法中循环引用的问题,防止内存泄漏的发生。
- 追踪性垃圾收集:这种可达性分析就是Java,C#选择的。
- 所谓“GC Roots”根集合就是一组必须活跃的引用。
基本思路:(理解场景:一串葡萄从根到最后,最后那些没有相连的坏葡萄就没用了)
可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上之下的方式搜索被根对象集合所连接的目标对象的是否可达。
使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接,搜索走过的路径为引用链。
如果目标对象没有任何引用链相连,则是不可达的,就意味该对象死亡,可以标记为垃圾对象
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
CG Roots
在Java 语言中,GC Root包含以下几类元素:
虚拟机栈中引用的对象(各个线程被调用的方法中使用到的参数,局部变量等)
本地方法栈内JNI(普通是的本地方法)引用对象
方法区中类静态属性引用的对象(如果:JAVA类的引用类型静态变量)
方法区中常量引用的对象(比如:字符串常量池(String Table)里的引用)
所有被同步synchronized持有的对象
Java虚拟机内部引用(比如:基本数据类型对应的Class对象,一些常驻的异常对象:空指针异常,内存溢出,系统类加载器)
分代收集和局部回收(大佬)
小技巧:
由于Root 采用栈的方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己有不存放在堆内存里面,那它就是Root。
注意:
如果使用可达性分析算法判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。
GC进行必须 “Stop The World“ 的一个重要原因。
即使号称(几乎)不会发生停顿的CMS 收集器中,枚举根节点是也是必须要停顿的。
对象的finalization机制
- 对象被销毁之前的自定义处理逻辑。
- 垃圾回收此对象之前,总会先调用这个对象的finalize()的方法。
- 用于在对象被回收时进行资源释放。
永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。
- 在finalize()时可能会导致对象复合。
- finzlize()方法的执行时间是没有保障的,完成由GC线程决定,若不发生GC,永远也没有执行机会。
- 一个糟糕的finalize()方法将没有执行机会。
由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。
- 可触及的:从根节点开始,可以到达这个对象。
- 可复活的:对象的所有引用都被释放,但是对象有可能在finzlize()中复活
- 不可触及的:对象finalize()被调用,并且没有复活,进入不可触及状态。不可触及的对象不可能被复活。因为finzlize()只会被调用一次。
具体过程:
判断一个对象objA释是否可回收,至少要经历两次标记过程:
1,如果对象objA到 GC Roots没有引用链,则进行第一次标记。
2,进行筛选,判断此对象是否有必要执行finalize()方法
a,如果对象objA没有重写finzlize()方法,或者finzlize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及
b,如果对象objA重写finzlize()方法,且还未执行过,那么objA会插入到F-Queue队列中,由一个虚拟机自动创建,低优先级的Finalizer线程
触发finzlize()方法执行。
c,finzlize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列队列中的对象进行第二次标记,如果objA在finzlize()方法中与引用链
上任何一个对象建立了联系,那么在第二次标记时,objA会被移动出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个
情况下,finzlize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
package com.ylc.java07; import com.sun.corba.se.spi.servicecontext.CodeSetServiceContext; /** * * 验证finalize方法,可以复活对象和只调用一次 * @Author ylc * @Date 2022/3/29 11:04 * @Version 1.0 */ public class CanReliveObj { public static CanReliveObj obj;//类变量,属于 GC Root //此方法调用一次 @Override protected void finalize() throws Throwable{ super.finalize(); System.out.println("调用当前类重写的finalize()方法"); obj = this;//当前待回收的对象finalize()方法中与引用链上的一个对象obj建立一个联系 } public static void main(String[] args) { try { obj = new CanReliveObj(); //对象第一次成功拯救自己 obj = null; System.gc();//调用垃圾回收器 System.out.println("第1次 gc"); //因为Finalize线程优先级很低,暂停2秒,以等待它 Thread.sleep(2000); if (obj == null) { System.out.println("dead"); }else { System.out.println("still alive"); } System.out.println("第2次 gc"); //代码一样,这次自己却失败了 obj=null; System.gc(); //因为Finalize线程优先级很低,暂停2秒,以等待它 Thread.sleep(2000); if (obj == null) { System.out.println("dead"); }else { System.out.println("still alive"); } }catch (InterruptedException e){ e.printStackTrace(); } } }
清除阶段:标记-清理算法
垃圾清理阶段:当成功区分出存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间。
目前JVM三种垃圾收集算法标记-清理算法(Mark-Sweep),复制算法(Copying),标记-压缩算法(Mark-Compact)
背景:标记-清理算法(Mark-Sweep)是一种非常基础和常见的垃圾收集算法
执行过程:
当堆中的有效内存空间被耗尽的时候,就会停止整个程序(俗称:stop the world),然后进行两项工作:标记,清理
标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象
清理:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
缺点:
- 效率不算高
- 在进行GC的时候,需要停止整个应用程序,导致用户体验差
- 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表。
注意:何为清理?
这里所谓的清理并不是真的置空,而是把需要清理的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。
个人理解场景:去餐厅吃饭,当客人用完餐后,用过的餐巾纸工作人员会清理掉。
垃圾清理阶段算法之复制算法
背景:
为了解决标记-清除算法在垃圾收集效率方面的缺陷“使用双存储区的Lisp语言垃圾收集器”,引入到Lisp语言的一个实现版本中。
核心思想:
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收是将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
没有标记和清除过程,实现简单,运行高效
复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:
此算法的缺点也是很明显的,就是需要两倍的内存空间。
对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
特别的:
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。
应用场景:
在新生代,对常规应用垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。
所以现在的商业虚拟机都是用这种收集算法回收新生代。
个人理解场景:一个场景分为东区和西区,当东区使用过餐巾纸被清理后,那些未使用的就有可能到西区去。
垃圾清理阶段算法之标记-压缩(整理)算法
背景:
复制算法的高效性是建立在存活对象少,垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是
存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此:基于老年代垃圾回收的特性,需要使用其他的算法。
标记-清理算法的确可以应用在老年代,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所有 JVM 的设计者需要在此基础之上进行改进。标记 - 压缩(Mark - Compact)算法由此诞生。
执行过程:
- 第一阶段和标记-清理算法一样,从根节点开始标记所有被引用对象
- 第二阶段将所有的存活对象压缩到内存一端,按顺序排放。
- 之后,清理边界外所有的空间。
标记-清理-压缩(Mark-Sweep-Compact)算法:标记-压缩算法的最终效果等同于标记-清理算法执行完成后,在进行一次内存碎片整理。
二者的本质差异在于标记-清理算法是一种非移动式的回收算法,标记-压缩是移动式的,是否回收后的存活对象是一项优缺点并存的风险决策。
指针碰撞:(Bump the Pointer)
如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系这一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式叫做指针碰撞。
个人理解场景:一个餐厅餐巾纸还需要使用的则画个圆圈,不用了的就不标记,等待工作人员清理,这些还需要使用去往另一端。
小结:
效率上来看,复制算法是当之无愧的老大,但是却浪费太多内存。
而为了尽量兼顾上面提到的的三个指标,标记0整理算法相对来说更平滑,但是效率不好,
它比复制算法多了一个标记的阶段,比标记-清理多了整理内存的阶段
分代收集算法
分代收集算法:不同的对象的生命周期不一样,不同生命周期的对象可以采取不同的收集方式,以便于提高回收效率。例:Java堆中分为新生代和老年代,根据不同的特点使用不同的回收算法,提高垃圾回收效率。
目前集合所有的GC都采用分代收集算法执行垃圾回收。
- Mark阶段的开销与存活对象的数量成正比。
- Sweep阶段的开销与所管理阶段区域的大小成正比相关。
- Compact阶段的开销与存活对象的数据成正比。
以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。而对于碎片问题
CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施:当内存回收不佳,将采用Serial Old执行Full
GC 以达到对老年代内存的整理。
增量收集算法
基本思想:
如果一次性所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。垃圾收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
好处:增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记,清理或复制工作。
缺点:
间断的执行应用程序代码,所以能减少系统的停顿时间。因为线程切换和上下文转换的消耗,
会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
个人理解场景:比如洗衣服衣服累计的越多,要清洗的时间就更长,只有穿一天洗一天,就很好。
分区算法
- 分代算法将按照对象的生命周期长短划分两个部分,分区算法将整个堆空间划分为成连续的不同小区间。
- 每个小区间都独立使用,独立回收。这种算法的好处就是可以控制一次回收多个小区间。
最后:
只是基本算法思路,实际GC实现过程要复杂的多,目前还在发展中前沿 GC 都是符合算法,并且并行并发兼备。
System.gc()的理解
- 默认情况下:System.gc()或者Runtime.getRuntime.gc()的调用,会显示触发Full GC
- 然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用。
- 一般情况下,垃圾回收应该是自动进行的,无需手动触发,否则太过麻烦。
package com.ylc.java08; /** * * 验证:System.gc()方法是不是提醒但不会马上执行 * @Author ylc * @Date 2022/3/30 10:57 * @Version 1.0 */ public class SystemGCTest { public static void main(String[] args) { new SystemGCTest(); System.gc();//提醒jvm的垃圾回收器执行gc,但是不确定是否马上执行gc //与Runtime.getRuntime().gc();的作用一样 System.runFinalization();//强制调用使用引用的对象的finalize()方法 } @Override protected void finalize() throws Throwable{ super.finalize(); System.out.println("SystemGCTest 重写了finalize()"); } }
B:手动gc理解不可达对象的回收过程
package com.ylc.java08; /** * @Author ylc * @Date 2022/3/30 15:46 * @Version 1.0 */ public class LocalVarGC { public void localvarGC1(){ byte[] buffer = new byte[10 * 1024 * 1024];//10MB System.gc();//不可 } public void localvarGC2(){ byte[] buffer = new byte[10 * 1024 * 1024]; buffer = null; System.gc();//可 } public void localvarGC3(){ { byte[] buffer = new byte[10 * 1024 * 1024];//10MB } System.gc();//不可 } public void localvarGC4(){ { byte[] buffer = new byte[10 * 1024 * 1024];//10MB } int value = 10; System.gc();//可 } public void localvarGC5(){ localvarGC1(); System.gc();//可 } public static void main(String[] args) { LocalVarGC lv = new LocalVarGC(); lv.localvarGC5(); } }
内存溢出与内存泄漏
javadoc中对OutOfMemoryError的理解是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
Java虚拟机的堆内存不够原因有:
Java虚拟机的堆内存设置不够
代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
严格来说:只有对象不会在被程序用到了,但是GC有不能回收他们的情况,才叫内存泄漏。
宽泛意义上“内存泄漏”:实际情况很多时候一些不太好的实践(或疏忽)导致对象的生命周期变得很长导致OOM
注意:这里的存储空间并不是物理内存,而是指虚拟机内存大小,这个虚拟内存大小取决于磁盘
交换区设定的大小
举例:
1,单例模式
单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收,这导致内存泄漏。
2,一些提供close的资源未关闭导致内存泄漏
数据库连接,网络连接,io连接必须手动close,否则是不能被回收。
个人理解场景:
内存溢出:我有个房子,房子的客厅里放了很多东西,当你想放一些其他东西时,但是实在不知道从哪下手,实在没有空闲空间了,导致内存溢出
内存泄漏:你有个房子,买了但是就是不去住,我也不能去住,一直空闲着,就是气人,我也回收不了。
Stop The World
STW:指的是GC事件发送过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程就会被暂停,没有任何响应,有点卡死的感觉。
STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
开发中不要用System.gc();会导致Stop-the-world的发生。
个人理解场景:当你妈妈帮你房间打扫卫生,你需要停止你的所有活动,让你妈妈打扫卫生。
垃圾回收的并行与并发
并发:
在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是同一个处理器上运行。
并发不是真正意义的的“同时进行”,由于CPU处理的速度非常快,只要时间间隔处理得当,用户可以感觉多个应用程序同时进行。
个人理解场景:饭店吃饭,只有一个厨师,熟练到让每一个桌子都有菜,让人感觉到都在吃,这就是并发
并行:
当系统有一个以上的CPU时,当一个CPU执行一个进程,另一个CPU可以执行另一个进程两个进程互不抢占CPU
决定并行因素不是CPU数量,而是CPU核心数量,比如一个CPU多个核也能并行
个人理解场景:十个人打球,当每个人都碰到球,就可以看似并行
二者对比:
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的。
只有在多CPU或者一个CPU多核的情况下,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
垃圾回收的并发和并行
并行:指多条垃圾收集线程并行工作,但是用户线程仍处于等待状态。
串行:
相较于并行的概念,单线程执行
如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收,回收完,在启动程序的线程。
并发:指用户线程与垃圾收集线程同时执行,垃圾回收线程在执行时不会停顿用户线程的运行。
用户程序继续运行,而垃圾收集程序线程运行与另一个CPU上
如:CMS,G1
安全点与安全区域
1,程序执行时并非在所有地方都能停顿下来开始 GC ,只有特定的位置才能停顿下
来开始 GC ,这些位置成为“安全点”。
2, 如果太小可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。
3,“算法具有让程序长时间执行的验证”,方法调用,循环跳转和异常跳转
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
a,抢先式中断
b,主动式中断
安全区域是指一段代码片段中,对象的引用关系不会发生关系,在这个区域中的任何位置开始GC都是安全的。
个人理解:北京到上海,没有休息站的地方不能休息,只能到休息站进行休息。
再谈引用:
既偏门又非常高频的面试题:强引用,软引用,弱引用,虚引用有什么区别?具体使用场景是什么?
强引用(StrongReference):最传统的“引用”的定义,是指在程序代码中普遍存在的引用赋值,
类似“Object obj = new Object()”
无论什么情况下,只要强引用关系还存在,垃圾收集器就永远不会回收被引用的对象。
软引用(SofeReference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。
如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,
无论内存空间是否足够,都会回收掉被弱引用关联的对象。
虚引用(PhantomReference):一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用
获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
强引用(Strong Reference)---不回收
1,常见的引用类型是强引用(普通系统99%以上都是强引用),也就是我们常见的普通对象引用,
也是默认的引用类型。
2,强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的的对象。
3,如果没有其他的引用关系,只有超过了引用的作用域或者显示地将相应(强)引用赋值为null,
就是可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策略。
4,相对的,软引用,弱引用和虚引用的对象是软可触及,弱可触及和虚可触及的,这一定条件下,
都是可以回收的,
强引用是造成Java内存泄漏主要原因
强引用列子:
局部变量str指向StringBuffer实例所在堆空间,通过str可以操作该实例,那么str就是StringBuffer实例的强引用。
对应内存结构:
强引用局备以下特点:
- 强引用可以直接访问目标对象
- 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常也不会回收强引用所指 向的对象。
- 强引用可能导致内存泄漏。
package com.ylc.java08; import java.util.ArrayList; import java.util.List; /** * * 验证:强引用 * @Author ylc * @Date 2022/3/30 17:33 * @Version 1.0 */ public class StrongReferenceTest { public static void main(String[] args) { StringBuilder str = new StringBuilder("HELLO"); StringBuilder str1 = str; str = null; System.gc(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(str1); } }
再谈引用:软引用--内存不足即回收
描述:只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,
没有足够的内存,则OOM。
应用:软引用实现内存敏感的缓存。:高速缓存
软引用:当内存足够,不会回收软引用的可达对象。当内存不够时,会回收软引用的可达对象。
package com.ylc.java08; import java.lang.ref.SoftReference; /** * 验证:软引用:创建软引用,获得对象,当内存足够时不会显示OOM,当创建另外对象导致内存不足,则会清理掉软引用,并报OOM * @Author ylc * @Date 2022/3/31 8:57 * @Version 1.0 */ public class SofeReferenceTest { public static class User{ public User(int id,String name){ this.id = id; this.name = name; } public int id; public String name; @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + '}'; } } public static void main(String[] args) { //创建对象,建立软引用 // SoftReference<User> userSoftReference = new SoftReference<User>(new User(1,"ylc")); //等价于 User u1 = new User(1,"ylc"); SoftReference<User> userSoftReference = new SoftReference<User>(u1); u1 = null;//注销强引用 //从软引用中重新获得强引用对象 System.out.println(userSoftReference.get()); System.gc(); System.out.println("After GC"); //垃圾回收之后获得软引用的对象 System.out.println(userSoftReference.get());//对于堆空间内存足够,所有不会回收软引用的可达对象 try { //让系统认为内存资源紧张,不够 byte[] b = new byte[1024 * 1024 * 7]; }catch (Throwable e){ e.printStackTrace(); }finally { //再次从软引用中获取数据 System.out.println(userSoftReference.get());//在报OOM之前,垃圾回收器会回收软引用的可达对象 } } }
再谈引用:弱引用--发现即回收
描述:只被弱引用关联的对象只能生存到下一个垃圾收集发生为,gc只要发现弱引用,不管什么都会被收集掉。
但是:垃圾回收器线程优先级很低,不会能很快发现弱引用对象,所以弱引用对象可以存在较长时间。
作用:弱引用,软引用都非常适合保存那些可有可无的缓存数据。
不同:弱引用对象与软引用对象的最大不同就在于,当GC在进行回收后,需要通过算法那检查是否回收
软引用对 象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易,更快被GC回收。
package com.ylc.java08; import java.lang.ref.WeakReference; /** * @Author ylc * @Date 2022/3/31 13:01 * @Version 1.0 */ public class WeakReferenceTest { public static class User{ public User(int id,String name){ this.id = id; this.name = name; } public int id; public String name; @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + '}'; } } public static void main(String[] args) { //构造弱引用 WeakReference<User> userWeakReference = new WeakReference<>(new User(1,"ylc")); //从弱引用中重新获取对象 System.out.println(userWeakReference.get()); System.gc(); //不管当前内存空间足够与否,都会回收它的内存 System.out.println("After GC"); //重新尝试从弱引用中获取对象 System.out.println(userWeakReference.get()); } }
再谈引用:虚引用--对象回收跟踪
由于虚引用可以跟踪对象的回收时间,因此,也可以将将一些资源放操作放置在虚拟机中执行和跟踪。
PhantomReference类来实现虚引用。
终结器引用(Final reference)
-
它用以实现对象的finalize方法,也可以成为终结器引用。
-
无需手动编码,其内部配合引用队列使用。
-
在GC时,终结器引用入队。由Finalizer线程通过终结引用找到被引用对象并调用它的finzlize方法,第二次gc进行回收对象。
垃圾回收器分类
按照线程数分,可以分为串行垃圾回收器和并发垃圾回收器。
按照工作模式分,可以并发式垃圾回收器和独占式垃圾回收器。
按照碎片处理方式分,可以分为压缩式垃圾回收器和非压缩式回收器。
按照工作的内存空间,分为年轻代垃圾回收器和老年代垃圾回收器。
Java不同版本的新特性:
1,语法层面:Lambda表达式,switch,自动装箱,自动拆箱,enum,<>,。。。
2,API层面:StreamAPI,新的日期时间,Optional,String,集合框架
3,底层优化:JVM的优化,GC的变化,元空间,静态域,字符串常量池等,
评估GC的性能指标
1,吞吐量:运行用户代码的时间占总运行时间的比例。(总运行时间:程序的运行时间+内存回收运行时间)a/a+b
2,垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
3,暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
4,收集频率:相对于应用程序的执行,收集操作发生的频率。
5,内存占用:Java 堆区所占的内存大小。
6,快速:一个对象从诞生到被回收所经历的时间。
不同的垃圾回收器概述
A: Java常见的垃圾收集器有哪些?
串行回收器:Serial,Serial Old
并行回收器:ParNew,Parallel Scavenge,Parallel Old
并发回收器:CMS,G1
B:7中经典收集器与垃圾分代之间的关系
新生代收集器:Serial,ParNew,Parallel Scavenge;
老年代收集器:Serial Old,Parallel Old(后备),CMS
整堆收集器:G1;
jdk8:默认Parallel Scavenge ----- Parallel Old
C:如何查看默认的垃圾收集器
-XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID
jdk8即以前使用-XX:+UseParallelGC,jdk8之后使用-XX:+UseG1GC;
Serial回收器:串行回收
Serial 收集器采用复制算法,串行回收和“STW”的机制的方式执行内存回收。
Serial Old收集器同样采用串行回收和“STW”机制,只不过内存回收算法使用的是标记-压缩算法。
Serial Old是运行在Client模式下默认的老年代的垃圾回收器
Serial Old在Server模式下主要两个用途:1,与新生代的Parallel Scavenge配合使用 2,作为老年代CMS收集器的后备垃圾收集方案
这个收集器是一个单线程的收集器,但她的“单线程”的意义并不仅仅说明它只会使用一个CPU 或一条收集线程去完成垃圾收集工作,更重要的是在他进行垃圾收集时,必须暂停其他所有的工作线程,直到它结束(STW)。
如何设置使用垃圾回收器?
-XX:UseSerialGC
总结:
目前已经不用串行的了,而且在限定单核CPU才可以用,现在都不是单核。
对应交互较强的应用而言,这种垃圾收集器是不能接受的。一般在Java Web应用程序中是不会采用串行垃圾收集器的。
ParNew回收器--串行
- Serial GC是年轻代中的单线程垃圾回收器,那么ParNew收集器则是Serial收集器的多线程
- ParNew 收集器采用并行回收,其他方面没有区别,同时也是采用复制算法,“STW”机制。
- ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器。
对于新生代,回收次数频繁,使用并行方式高效。
对于老年代,回收次数少,使用串行方式介绍资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)
问题:ParNew收集器任何条件下都比Serial收集器快吗
1,ParNew 收集器运行在多CPU环境下,由于可以充分利用多CPU,多核心等物理硬件资源优势,可以更快速完成垃圾收集提升吞吐量。
2,在单个CPU的环境下,ParNew收集器不比Serial 收集器更高效,由于频繁切换任务,因此可以有效避免多线程交互过程中产生的一些额外开销
“-XX:UseParNewGC”手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
-XX:ParallelGCThreads 限制线程数量,默认开启和CPU数据相同的线程。
Parallel Scavenge回收器:吞吐量优先
1,Parallel Scavenge收集器同样也采用复制算法,并行回收和“STW”.
2,那么Parallel收集器和ParNew一样是不是就多此一举了?
1,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量,也称为吞吐量优先的垃圾收集器。
2,自适应调节也是Parallel Scavenge与ParNew一个重要区别。
3,高吞吐量可以高效率利用 CPU时间,尽快完成程序运行任务,主要适合后台运算而不需要太多
交互的任 务。经常在服务器环境中使用。(批量处理,订单处理,工资支付,科学计算)。
5,Parallel Old收集器采用标记-压缩算法,但同样也是基于并行回收和“STW” 。
6,Parallel Old收集器和Parallel收集器组合使用,Parallel Old默认开启(当Parallel开启时),Server模式下内存回收性能很不错。
7,Java8中,默认此垃圾收集器。
Parallel Scavenge垃圾回收器一些参数配置
-XX:+UseParallel GC 手动指定年轻代使用Parallel并行收集器执行内存回收内存。
-XX:+UseParallelOld GC 手动指定老年代都是使用并行回收收集器。
1,分别适应新生代和老年代。默认jdk8是开启的
2,这两个参数互相激活。
-XX:ParallelGCThreads 设置年轻代并行的线程数。
1,当CPU 数量小于 8 个,ParallelGCThreads 的值等于 CPU 数量
2,当CPU数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU_Count]/8]。
-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。
1,尽可能将时间控制在MaxGCPauseMills以内,收集器会调整Java堆大小
-XX:GCTimeRatio 垃圾收集时间占总时间的比例(= 1 / (N + 1))用于衡量吞吐量的大小。
-XX:+UseAdaptiveSizePolicy 设置Parallel Scaveng收集器具有自适应调节策略
1,年轻代大小,Eden和Survivor的比例等参数自动调整,以达到平衡点
CMS回收器:低延迟
强交互应用中几乎可认为有划时代意义:CMS真正意义上的并发收集器,它第一次实现让垃圾收集线程与用户线程同时工作。
CMS可缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越合适与用户交互的程序。
目前很大一部分的Java引用集中在互联网或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短。
CMS的垃圾收集算法采用标记-清理算法,并且也会“STW”。
CMS整个过程分为4个主要阶段,初始标记阶段,并发标记阶段,重新标记阶段和并发清理阶段。
1,初始标记阶段:主要任务仅仅只是标记出GC Roots能直接关联到对象。由于直接关联对象比较小,速度非常快。
2,并发标记阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个多次耗时较长但是不需要停顿用户线程。
3,重新标记阶段:修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始化阶段稍长,并发阶段短
4,并发清理阶段:清理删除掉标记阶段判断的已经死亡的对象,释放内存空间,由不需要移动存活对象,可以与用户线程同时并发。
CMS特点:
初始化标记和再次标记这两个阶段中仍然需要执行“STW”机制不过时间并不会太长,,所以说目前所有的垃圾回收器做不到“STW”,只是尽可能缩短暂停时间。
由于最耗费时间的并发标记与并发清理阶段都不需要暂停工作,所以整体的回收是第停顿的
CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。当堆内存使用率达到某一阈值时,便开始进行回收。
CMS收集器的垃圾收集算法采用标记-清理算法,所以被执行内存回收的无用对象所占用空间极有可能是不连续的内存块,不可避免将会产生一些内存碎片。
问题:有个会决定既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact呢?
因为当并发清理的时候,用Compact整理内存,原来的用户线程使用的内存还怎样用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响。Mark Compact更适合“STW”
CMS的优点: 并发收集 低延迟
CMS的弊端:
1,会产生内存碎片
2,CMS收集器堆CPU资源非常敏感
3,CMS收集器无法处理浮动垃圾
CMS收集器可以设置的参数
-XX:+UseConcMarkSweepGC:手动指定使用CMS 收集器执行内存回收任务。(它一开启年轻代的UseParNewGC就打开了)
-XX:CMSLnitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦到达该阈值,便开始进行回收。
-XX:+UseCMSCompactAtFullCollection 用于指定执行完Full GC后对内存进行压缩整理。避免内存碎片的产生。
-XX:CMSFullGCsBeforeCompaction 设置在执行多少次Full GC后对内存空间进行压缩整理。
-XX:ParallelCMSThreads 设置CMS线程数量。
小结:
如果你要最小化地使用内存和并行开销,请选Serial GC;
如果想要最大化应用程序的吞吐量,请选Parallel GC;
如果想要最小化GC的中断或停顿时间,请选CMS GC;
JDK后续版本中CMS的变化
JDK9:CMS被标记Deprecate(JEP291),表示未来将放弃,表示使用+UseConMarkSweepGC开启收集器。
JDK14:删除CMS垃圾回收器(JEP363)。
G1回收器:区域化分代式
既然我们已经有了前面几个强大的GC,为什么还要发布Garbage First GC?
原因:面对业务越来越庞大,复杂,用户越来越多,经常造成STW的GC又跟不上实际需求。为了适应不断扩大的内存和不断的处理器数量,
进一步降低暂停时间,同时兼顾良好的吞吐量。
官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才当前“全功能收集器”的重任与期望。
为什么名字叫做Garbage First(G1)呢?
因为G1是一个并行回收器,它把堆分成不同区域(物理上不连续)。使得Region表示Eden,幸存者0区,幸存者1区,老年代等
每次根据允许收集时间,优化回收价值最大的Region。
因为侧重点在于回收垃圾最大量的区间,所以G1一个名字:垃圾优先。
介绍:
G1是一款面向服务端应用的垃圾收集器,主要针对配置多核CPU及大容量内存的机器,兼高吞吐量的性能特征。
是jdk9 以后默认的垃圾回收器取代CMS,Parallel + Parallel Old组合。称为“全功能的垃圾收集器”。
G1回收器特点(优势)
并行与并发
并发性:有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
并发性:应用程序交替执行1的能力,部分工作可以和应用程序同时执行
分代收集:
G1依然属于分代型垃圾回收器,将堆空间分为若干个区域,这些区域包含了逻辑上的年轻代和老年代,同时兼顾年轻代和老年代。
空间整合:
CMS:“标记-清理”算法,内存碎片,若干次GC后进行一次碎片整理
G1将内存划分为一个个region。而且以region作为基本单位,Region之间是复制算法,
整体上课看做标记-压缩算法,两种都是避免内存碎片
可预测的停顿时间模型:
每次根据允许的收集时间,优先回收价值最大的Region,保证 G1 收集器在有限的时间内可以获取尽可能高的收集效率。
G1回收器的缺点:
相较CMS,G1还不具备全方位,压倒性优势。比如在用户线程运行过程中G1无论为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS要高。
在小内存应用上CMS的表现大概率会优先与G1,而G1在大内存应用上则发挥其优势,平衡点在6-8GB之间。
G1回收器的参数设置
-XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务
-XX:G1HeapRegionSize 设置每个Region的大小。
-XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标。默认值200ms
-XX:ParallelGCThread 设置STW工作线程数值。最多设置为8
-XX:ConGCThreads 设置并发标记的线程数。
-XX:InitiatingHeapOccupancyPercent 设置触发并GC周期的Java堆占用率阈值。超过此值触发GC,默认值45.
实现G1三步:
第一步:开启G1垃圾收集器
第二步:设置堆的最大内存
第三步:设置最大的停顿时间
G1提供了三种垃圾回收模式:YoungGC,Mixed GC和Full GC,在不同的条件下触发。
G1回收器的适用场景
面对服务端应用,针对具有大内存,多处理器的机器。(在普通大小的堆里表现并不惊喜)。
主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;
用来替换掉JDK5中的CMS收集器;
在下面的情况时,使用G1可能比CMS好;
1,超过50%的Java堆被活动数据占用
2,对象分配频率或年代提升频率变化很大
3,GC停顿时间过长
分区Region:化整为零(一个烟花还没放时是一个整个的,点燃开放后成为了美丽的烟花)
所有的Region大小相同,且JVM生命周期内不会被改变。通过Region的动态分配方式实现逻辑上是连续。
G1 垃圾收集器还增加了一种新的内存区域,叫Humongous 内存区域,主要用于存储大对象,如果超过1.5个region,就放到H。
设置H的原因:
对于堆中的大对象,默认直接会分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。
如果一个H去装不下一个大对象,那么G1会导致寻找连续的H区老存储。
G1回收器垃圾回收过程
G1 GC的垃圾回收过程主要包含如下三个环节:
年轻代GC(Young GC)
老年代并发标记过程(Cocurrent Marking)
混合回收(Mixed GC)
如果需要,单线程,独占式,高强度的Full GC还是继续存在的。它是针对GC的评估失败提供了
一种失败保护机制,即强力回收。
当年轻代的Eden区用尽时开始年轻代回收过程;G1年轻代收集阶段是一个并行的独占式收集器。然后从年轻代区间移动存活对象到
Survivor区间或者老年区间,也有可能是两个区间都会涉及。
当堆内存使用达到一定值(默认值45%)时,开始老年代并发标记过程。
标记完成后马上开始回收过程,对于一个混合回收器,G1的老年代回收器不需要整个老年代被回收,
一次只需要扫描/回收一个小部分老年代的Region就可以。同时这个老年代Region是和年轻代一起被回收。
G1回收器垃圾回收过程:Remembered Set
一个对象被不同区域引用的问题!!!
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描;
每个Region都有一个对应的Remembered Set;
G1回收过程一:年轻代GC
第一阶段:扫描根 根引用连用RSet记录的外部引用作为扫描存活对象的入口
第二阶段:更新RSet RSet可以准确的反应老年代堆所在的内存分段中对象的引用。
第三阶段:处理RSet 识别老年代对象指向的Eden中对象,这些被指向的对象被认为是存活对象
第四阶段:复制对象 Eden去存活对象被复制到Survivor区中空的内存分段,未达到阈值加一,达到就复制到Old区中空的内存分段。
第五阶段:处理引用 最终Eden空间的数据为空,GC停止工作,而目标内存中对象是连续的,没有碎片,所以复制过程可以达到内存整理。
G1回收过程二:并发标记过程
第一阶段:初始标记阶段 标记根节点直接可达的对象
第二阶段:根区域扫描 扫描Survivor区直接可达的老年代区域对象,并标记被引用对象。
第三阶段:并发标记 整个堆中进行并发标记。此过程被YGC中断,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。
第四阶段:再次标记 修正上一次标记结果
第五阶段:独占清理 计算比例,识别可以混合回收的区域
第六阶段:并发清理阶段 识别并清理完全空闲区域
G1回收过程三:混合回收
当越来越多的对象晋升为老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾回收器,即Mixed GC,该算法并不是一个
Old GC,除了回收整个Young Region,还会回收一部分整个 Old Region。一部分老年代,而不是全部老年代。可以选择那些Old Region
进行收集,从而可以对垃圾回收耗时时间进行控制。也要注意Mixed GC并不是Full GC。
G1回收过程:补充
G1只是回收一部分Region,停顿时间是用户可控制的,所以并不迫切实现,而选择把这个特性放到了G1之后出现的低延迟垃圾收集器。
G1不仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。
G1回收器建议:
年轻代大小
避免使用-Xmn或-XX:NewRatio等相关选项显示设置年轻代大小
固定年轻代的大小会覆盖暂停时间目标。
暂停时间目标不要太过严苛
G1 GC的吞吐量目标是90%D1应用程序时间和10%的垃圾回收时间
评估G1 GC的吞吐量时,暂停时间目标不要太苛刻,最后直接影响到吞吐量。
垃圾回收器总结
怎么选择垃圾收集器?
1,优先调整堆的大小让JVM自适应完成。
2,如果内存小于100M,使用串行收集器
3,如果单核,单机程序,并且没有停顿时间的要求,串行收集器
4,如果是多CPU,需要高吞吐量,允许停顿时间的超过1秒,选择并行或者JVM自己选择
5,如过多CPU,追求低停顿时间,需快速响应(比如延迟不能超过1秒),使用并发收集器(官方G1,基本使用)
面试:
1,垃圾收集算法有哪些?如何判断一个对象是否可以回收?
2,垃圾收集器工作的基本流程。
GC日志分析
内存分配与垃圾回收的参数列表
-XX:+PrintGC 输出GC日志。类似:-verbose:gc
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC时间戳(以日期的形式,2018-06-08T21:54:43.344+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径
GC数据中垃圾回收数据分析:
Mrion GC
Full GC
堆空间数据怎么读?
JDK7 当eden区没有空闲空间时,from也没空闲时,将Eden的三个2转换到老年代,进行一次GC之后,将这个四MB直接转到Eden。
JDK8,当eden区没有空闲空间时,from也没空闲时,这个4MB并不用转到Eden,直接转为老年代。
GC日志分析工具
-Xms60m -Xmx60m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -Xloggc:./logs/gc.log
在项目中加入上面的参数,并在项目中加入一个logs的目录,就会生成一个 gc.log文件,在使用 easygc分析
垃圾回收器的新发展
新时代Shenandoah: 主打特点:低停顿时间
Shenandoah,无疑是众多GC中最孤独的一个。因为并不是Oracle开发的。不可避免会受到排挤。
旨在针对JVM上的内存回收实现低停顿的需求
Shenandoah垃圾回收器的暂停时间与堆大小无关,这意味着无论将堆设置200 MB还是200GB,99.9%的目标都可以把垃圾收集的停顿时间限制在十毫秒以内。
总结:
Shenandoah GC强项:高运行负担下的吞吐量下降。
Shenandoah GC弱项:低延迟时间
ZGC
在尽可能堆吞吐量影响不大的前提下,实现任意内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟
实现可并发标记-压缩算法的,以低延迟为首要目标
ZGC:并发标记-并发预备重分配-并发重分配-并发重映射等。
未来将服务端,大内存,低延迟应用的首选垃圾收集器
posted on 2022-03-31 20:38 只想做加法(ylc) 阅读(416) 评论(0) 编辑 收藏 举报