前言
菜鸟程序员的学习笔记,各位大佬请指教
概述
C需要程序员手动的去回收
问题?
关于垃圾收集有三个经典问题:
哪些内存需要回收?
什么时候回收?
如何回收?
面试题
讲讲JVM的gc (携程)
GC是什么?为什么要有GC? (蚂蚁金服)
垃圾回收的优点和原理。 (蚂蚁金服)
垃圾回收机制等 (支付宝)
GC回收的是哪部分的垃圾?(vivo)
垃圾回收的优点和原理?基本原理是什么?(瓜子)
GC是什么?为什么要有GC? (美团)
简述Java垃圾回收机制 (美团)
垃圾回收的优点和原理。(美团)
什么是垃圾?
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
为什么需要GC?
对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象。
另一方面
对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重的就会弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。
此时,了解JVM的自动内存分配和内存回收原理就显得非常重要,只有在真正了解JVM是如何管理内存后,我们才能够在遇见OutOfMemoryError时,快速地根据错误异常日志定位问题和解决问题。
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。
Java中垃圾回收的重点区域
垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收。
其中,Java堆是垃圾收集器的工作重点。
从次数上讲:
**频繁收集Young区
较少收集Old区
基本不动Perm区(或元空间)**永久代
早期的GC
C/C++写代码回收
垃圾回收算法*
面试题
GC算法都有哪些?他们之间的区别是什么?(菜鸟)
JVM的常用的GC算法(高得地图)
GC垃圾回收机制算法(数信互融科技发展有限公司)
GC的算法,复制算法和标记清除的优缺点?(迪原创新)
常用的GC算法,如何确定哪些是要被清除的哪些是不能被清除(网易邮箱、美团)
垃圾回收机制的几种回收算法(亚信)
GC算法都有哪些?他们之间的区别是什么?各自的适用场景?(B站)
GC 的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?(腾讯)
如和判断一个对象是否存活?(唯品会)
Java 中垃圾收集的方法有哪些?(苏宁)
你是用什么方法判断对象是否死亡?(滴滴)
如何判断一个对象是否存活? (蚂蚁金服)
垃圾收集策略和算法 (百度)
常见的垃圾回收器算法有哪些,各有什么优劣?(网易)
JVM有哪些回收算法,对应的收集器有哪些? (蚂蚁金服)
JVM GC算法有哪些,目前的JDK版本采用什么回收算法 (蚂蚁金服)
垃圾回收算法的实现原理。 (京东)
讲一下JVM中如何判断对象的生死? (京东)
如何选择合适的垃圾收集算法? (阿里)
讲一讲垃圾回收算法。 (阿里)
JVM有哪些回收算法,对应的收集器有哪些? (拼多多)
讲讲你知道的垃圾回收算法 (字节跳动)
Java对象的回收方式,回收算法。 (字节跳动)
JVM垃圾收集算法与收集器有哪些? (京东)
常见的垃圾回收器算法有哪些,各有什么优劣?(阿里-天猫、UC)
有哪些垃圾回收方法,jdk8的垃圾收集器是什么?(搜狐、万达集团)
如何判断一个对象是否存活?(或者GC对象的判定方法) (美团)
常见的垃圾回收器算法有哪些,各有什么优劣? (字节跳动)
垃圾判别阶段的算法
那么在JVM中究竟是如何标记一个死亡对象呢?
简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
引用计数算法
专门拿一个变量来记录你被引用几次,这个数为零就是不引用了,回收了。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:
缺点1:它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
缺点2:每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
缺点3:引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java 的垃圾回收器中没有使用这类算法。
public class RefCountGC {
//这个成员属性唯一的作用就是占用一点内存
private byte[] bigSize = new byte[4 * 1024 * 1024];
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();
}
}
这个算法jdk没用,用的下面的算法
可达性分析算法
可达性分析 (或根搜索算法、追踪性垃圾收集)
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)。
567这里就得回收了
优点:
实现简单,执行高效 ,有效的解决循环引用的问题,防止内存泄漏。
可达性分析算法也会有内存泄漏:
有部分通过可达性算法搜索到,从根搜索到,但是本身不用了,这就会产生内存泄漏
GC Roots
面试题:
Java GC机制?GC Roots有哪些? (拼多多)
JVM怎样判断一个对象是否可回收,怎样的对象才能作为GC root (腾讯)
Java GC机制?GC Roots有哪些? (字节跳动)
哪些部分可以作为GC Root? (字节跳动)
Java GC机制?GC Roots有哪些? (抖音)
Java GC机制?GC Roots有哪些? (京东)
GC root如何确定,哪些对象可以作为GC Root? (美团)
在Java 语言中, GC Roots 包括以下几类元素:
虚拟机栈中引用的对象
比如:各个线程被调用的方法中使用到的参数、局部变量等。
本地方法栈内JNI(通常说的本地方法)引用的对象
类静态属性引用的对象静态的放到堆里面了
比如:Java类的引用类型静态变量
方法区中常量引用的对象
比如:字符串常量池(String Table)里的引用
所有被同步锁synchronized持有的对象
Java虚拟机内部的引用。
基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
小技巧:
由于Root 采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root 。
注意:
分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
这点也是导致GC进行时必须“Stop The World”的一个重要原因。
垃圾清除阶段的算法
标记-清除算法*
标记不是垃圾的,清除黑色的垃圾
缺点:内存碎片化。有大对象的话空间太割裂。
缺点:
1、效率比较低:递归与全堆对象遍历两次
2、在进行GC的时候,需要停止整个应用程序,导致用户体验差
3、这种方式清理出来的空闲内存是不连续的,产生内存碎片。
注意:何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。
复制算法*(新生代用)
先标记,然后把标记的按照顺序排复制,原有的全部清除,原有的关联关系还保留。
优点:
没有标记和清除过程,实现简单,运行高效
复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:
此算法的缺点也是很明显的,就是需要两倍的内存空间。
对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
特别的:
如果系统中的存活对象很多,复制算法不会很理想。因为复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。
应用场景:
一般不用,但CMS会用
应用场景:
在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
比如:IBM 公司的专门研究表明,新生代中 80% 的对象都是“朝生夕死”的。
标记-压缩算法*(老年代用)
标记玩之后,连续的就不动了,判断有用的往上面移动,占用挨着空闲的空间,尽量不碎片化。解决的碎片化和两倍的空间(上面两种),实现了指针碰撞的效果。
指针碰撞:
如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞(Bump the Pointer)。
优点:(此算法消除了“标记-清除”和“复制”两个算法的弊端。)
消除了标记/清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
消除了复制算法当中,内存减半的高额代价。
缺点:
从效率上来说,标记-压缩算法要低于复制算法。
效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
对于老年代每次都有大量对象存活的区域来说,极为负重。
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
移动过程中,需要全程暂停用户应用程序。即:STW
分代收集算法
三种算法比较:
标记清除算法,标记压缩算法,复制算法
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。
而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。(新生代用复制(朝生夕死),老年代用标记压缩(GC少))
面试题:
JVM的垃圾回收为什么采用分代GC。跟语言有关系吗?(阿里-钉钉)
分代的意义说一下 (阿里-钉钉)
GC分代算法(花旗银行)
说一下gc算法,分代回收说下 (百度)
Java怎么进行垃圾回收的?什么对象会进老年代? 垃圾回收算法有哪些?为什么新生代使用复制算法? (京东)
分代垃圾回收过程? (美团)
GC如何分代的?各代用什么算法回收? (美团)
分析:
目前几乎所有的GC都是采用分代收集(Generational Collecting)算法执行垃圾回收的。
在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
年轻代(Young Gen)
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
标记Mark阶段的开销与存活对象的数量成正比。
清除Sweep阶段的开销与所管理区域的大小成正相关。
整理Compact阶段的开销与存活对象的数据成正比。
以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。
分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。
增量收集算法
上述现有的算法,在垃圾回收过程中,应用软件将处于一种Stop the World 的状态。在Stop the World 状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting)算法的诞生。
基本思想
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
缺点:
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
分区算法—G1 GC使用的算法
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
相关概念
System.gc()
在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用。
JVM实现者可以通过System.gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。
没事的时候不调用,调用的时候会Full GC,线程会停一下。性能基准测试,比较两个代码谁快,测试之前掉一下这个方法。
面试题
System.gc()和Runtime.getRunTime().gc()会做什么事情? (字节跳动)
会Full GC
finalize()方法详解
在判断这个对象是垃圾之前,有当前对象会触发这个。
但是回收前会有遗言“finalize()”,就是这个方法,回收后没死?
package com.atguigu.other;
/**
* 测试Object类中finalize()方法,即对象的finalization机制。
*
* @author shkstart
* @create 2020 下午 2:57
*/
public class FinalizeObj {
public static FinalizeObj 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 FinalizeObj();
// 对象第一次成功拯救自己
obj = null;
System.gc();//调用垃圾回收器
System.out.println("第1次 gc");
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("第2次 gc");
// 下面这段代码与上面的完全相同,但是这次自救却失败了
obj = null;
System.gc();
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在第一次睡眠的时候,给时间去触发finalize(),把当前对象在赋给他,此时对象就还存活。
但是当一个对象首次考虑要被回收时,会调用其finalize(),第二次就不会了。
finalize(方法作用:上面代码那种情况一般不会做。
(1)finalize()与C++中的析构函数不是对应的。C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java中的finalize的调用具有不确定性
(2)不建议用finalize方法完成“非内存资源”的清理工作,但建议用于:① 清理本地对象(通过JNI创建的对象);② 作为确保某些非内存资源(如Socket、文件等)释放的一个补充:在finalize方法中显式调用其他资源释放方法。
内存泄漏与内存溢出
面试题.解释?(两部分)
什么是内存泄漏和什么是内存溢出 (陌陌)
Java存在内存泄漏吗,内存泄漏的场景有哪些,如何避免(百度)
Java 中会存在内存泄漏吗,简述一下?(猎聘)
内存泄漏是怎么造成的?(拼多多、字节跳动)
内存泄漏与内存溢出的区别 (字节跳动)
Java存在内存溢出的现象吗 (字节跳动)
Java中会存在内存泄漏吗,请简单描述。 (美团)
内存溢出:
应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况。没有空闲内存,并且垃圾收集器也无法提供更多内存。
内存不够的原因:
1.内存够,但是太过于碎片化,创建大对象的话,碎片化内存用不了
OOM前一定会有GC吗?
正常的话有,但是:
比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutOfMemoryError。
内存泄漏:(八种情况)
内存泄漏(本来就是垃圾(本身生命周期没那么长,后面不用,严重超出生命周期长度)没有被回收)
1:静态集合类
静态变量生命周期和类一样,类一般没有GC,局部变量出方法就会被销毁,但是放在静态变量中,回收不掉,就泄漏
public class MemoryLeak {
static List list = new ArrayList();
public void oomTests() {
Object obj = new Object();//局部变量
list.add(obj);
}
}
2:单例模式
单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。
3:内部类持有外部类
内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。
这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
4:各种连接(mysql连接,网络连接,IO连接)
各种连接,如数据库连接、网络连接和IO连接等。
在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。
否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏
public static void main(String[] args) {
try {
Connection conn = null;
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("url", "", "");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("....");
} catch (Exception e) { //异常日志
} finally {
//1.关闭结果集 Statement
// 2.关闭声明的对象 ResultSet
// 3.关闭连接 Connection
}
}
5:变量不合理的作用域
msg不置空的话,本来生命周期局限于方法内,msg就没有被销毁,不是null就不被回收
public class UsingRandom {
private String msg;
public void receiveMsg(){
//private String msg;
readFromNet();// 从网络中接受数据保存到msg中
saveDB();// 把msg保存到数据库中
//msg = null;
}
}
6:改变哈希值
public class ChangeHashCode {
public static void main(String[] args) {
HashSet set = new HashSet();
Person p1 = new Person(1001, "AA");
Person p2 = new Person(1002, "BB");
set.add(p1);
set.add(p2);
p1.name = "CC";
set.remove(p1);
System.out.println(set);//2个对象!
// set.add(new Person(1001, "CC"));
// System.out.println(set);
// set.add(new Person(1001, "AA"));
// System.out.println(set);
}
}
p1.name = “CC”;不改变可以移除,改变就无法移除。
(不重写hashCode和equals方法,能remove;重写了,不能remove)
7:缓存泄漏
内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。
对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。(弱引用)
面试题:
用过WeakHashMap吗?什么时候用?
做缓存的时候用,缓存内存中的数据,应为会被垃圾回收期回收掉,即使有引用也会被回收掉
WeakHashMap弱引用,特点:发现即回收(不管内存够不够)
HashMap强引用。特点()
(频繁使用的缓存不建议使用WeakHashMap,建议会用软引用)
循环引用使用WeakHashMap也能解决
package com.atguigu.other;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
/**
* 演示内存泄漏
*
* @author shkstart
* @create 14:53
*/
public class MemoryLeak1 {
static Map wMap = new WeakHashMap();
static Map map = new HashMap();
public static void main(String[] args) {
init();
testWeakHashMap();
/**
结果:
String引用ref1,ref2,ref3,ref4 消失
WeakHashMap GC之前
weakMap2=cacheObject2
weakMap1=cacheObject1
WeakHashMap GC之后
**/
testHashMap();
/**
String引用ref1,ref2,ref3,ref4 消失
HashMap GC之前
map3=cacheObject3
map4=cacheObject4
HashMap GC之后
map3=cacheObject3
map4=cacheObject4
**/
}
public static void init() {
String ref1 = new String("weakMap1");
String ref2 = new String("weakMap2");
String ref3 = new String("map3");
String ref4 = new String("map4");
wMap.put(ref1, "cacheObject1");
wMap.put(ref2, "cacheObject2");
map.put(ref3, "cacheObject3");
map.put(ref4, "cacheObject4");
System.out.println("String引用ref1,ref2,ref3,ref4 消失");
}
public static void testWeakHashMap() {
System.out.println("WeakHashMap GC之前");
for (Object o : wMap.entrySet()) {
System.out.println(o);
}
try {
System.gc();
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("WeakHashMap GC之后");
for (Object o : wMap.entrySet()) {
System.out.println(o);
}
}
public static void testHashMap() {
System.out.println("HashMap GC之前");
for (Object o : map.entrySet()) {
System.out.println(o);
}
try {
System.gc();
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("HashMap GC之后");
for (Object o : map.entrySet()) {
System.out.println(o);
}
}
}
/**
* 结果
* String引用ref1,ref2,ref3,ref4 消失
* WeakHashMap GC之前
* obejct2=cacheObject2
* obejct1=cacheObject1
* WeakHashMap GC之后
* HashMap GC之前
* obejct4=cacheObject4
* obejct3=cacheObject3
* Disconnected from the target VM, address: '127.0.0.1:51628', transport: 'socket'
* HashMap GC之后
* obejct4=cacheObject4
* obejct3=cacheObject3
**/
8:监听器和回调
内存泄漏另一个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显式的取消,那么就会积聚。
需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为WeakHashMap中的键。
STW
达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。
GC Roots时保证关联对象的一致性停止用户线程,就是Stop-the-World—STW(不是手动发起的)
不是手动发起的:
Sysytem.gc()会触发Full GC中会出现STW。不是直接触发的,是Full GC触发的。
开发中不要用System.gc();会导致Stop-the-world的发生。
垃圾回收的并发与并行
并发(独占式):
在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行。
并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。
用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
如:CMS、G1
并行(串行):
当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,我们称之为并行(Parallel)。
其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。
适合科学计算,后台处理等弱交互场景
在垃圾回收的概念下:
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
如ParNew、Parallel Scavenge、Parallel Old;
串行(Serial)
相较于并行的概念,单线程执行。
如果内存不够,则程序暂停,启动jvm垃圾回收器进行垃圾回收。回收完,再启动程序的线程
对比:
二者对比:
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
如ParNew、Parallel Scavenge、Parallel Old;
串行(Serial)
相较于并行的概念,单线程执行。
如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完,再启动程序的线程。
好处:多个线程同时进行垃圾回收,效率非常好,吞吐量大
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。
用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
如:CMS、G1
好处:就在这:用户线程跟垃圾线程同时出发,低延迟
安全点与安全区域
安全点?
做GC的时候只有在安全点的时候才能做。
选择安全点标准:
通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。
安全区域?
到这个区域就可以GC,把 Safe Region 看做是被扩展了的 Safepoint。如线程处于 Sleep 状态或 Blocked 状态
5中引用
周阳JUC中讲到???????
面试题
强引用、软引用、弱引用、虚引用的区别?(字节跳动)
你开发中使用过WeakHashMap吗?(京东)
强引用*:不回收
强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。
强引用是造成Java内存泄漏的主要原因之一。
例子:
StringBuffer str = new StringBuffer (“Hello,谷”);
局部变量str指向StringBuffer实例所在堆空间,通过str可以操作该实例, 那么str就是StringBuffer实例的强引用
对应内存结构:
此时,如果再运行一个赋值语句:
StringBuffer str1 = str;
对应内存结构:
本例中的两个引用,都是强引用,强引用具备以下特点:
强引用可以直接访问目标对象。
强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象。
强引用可能导致内存泄漏。
软引用*:内存不足即回收
软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
软引用通常用来实现内存敏感的缓存
比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue)。
类似弱引用,只不过Java虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。
在JDK 1.2版之后提供了java.lang.ref.SoftReference类来实现软引用。
Object obj = new Object(); //声明强引用
SoftReference sf = new SoftReference(obj);
obj = null; //销毁强引用
但是内存是够的就不会被回收
public class SoftReferenceTest {
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 "[id=" + id + ", name=" + name + "] ";
}
}
public static void main(String[] args) {
//创建对象,建立软引用
// SoftReference<User> userSoftRef = new SoftReference<User>(new User(1, "songhk"));
//上面的一行代码,等价于如下的三行代码
User u1 = new User(1,"songhk");
SoftReference<User> userSoftRef = new SoftReference<User>(u1);
u1 = null;//取消强引用
//从软引用中重新获得强引用对象
System.out.println(userSoftRef.get());
System.gc();//触发full gc,但是内存是够的就不会被回收
System.out.println("After GC:");
// //垃圾回收之后获得软引用中的对象
System.out.println(userSoftRef.get());//由于堆空间内存足够,所有不会回收软引用的可达对象。
内存不够就会GC,会报OOM,对象就为null了,被回收了
try {
//让系统认为内存资源紧张、不够
byte[] b = new byte[7168 * 1024];
} catch (Throwable e) {
e.printStackTrace();
} finally {
//再次从软引用中获取数据
System.out.println(userSoftRef.get());//在报OOM之前,垃圾回收器会回收软引用的可达对象。
}
弱引用*:发现即回收(比软还软)
弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。
弱引用非常适合来保存那些可有可无的缓存数据。
在JDK 1.2版之后提供了java.lang.ref.WeakReference类来实现弱引用。
Object obj = new Object(); //声明强引用
WeakReference wr = new WeakReference(obj);
obj = null; //销毁强引用
弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收。
GC后就没有了,发现就回收了
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 "[id=" + id + ", name=" + name + "] ";
}
}
public static void main(String[] args) {
//构造了弱引用
WeakReference<User> userWeakRef = new WeakReference<User>(new User(1, "songhk"));
//从弱引用中重新获取对象
System.out.println(userWeakRef.get());
System.gc();
// 不管当前内存空间足够与否,都会回收它的内存
System.out.println("After GC:");
//重新尝试从弱引用中获取对象
System.out.println(userWeakRef.get());
}
}
虚引用:对象回收跟踪
为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。
终结器引用
它用以实现对象的finalize()方法,也可以称为终结器引用。
无需手动编码,其内部配合引用队列使用。
在GC时,终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()方法,第二次GC时才能回收被引用对象。
垃圾回收器*
GC分类
串行vs并行
按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。
只有一个垃圾回收器就叫串行,好几个垃圾回收器就是并行
更多的影响的是吞吐量的问题
并发式vs独占式
按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
同时执行的称为并发,只有垃圾回收线程执行的叫独占式
更多的是影响暂停时间,响应时间
压缩式vs非压缩式
按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器。
压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
再分配对象空间使用:指针碰撞
非压缩式的垃圾回收器不进行这步操作。
再分配对象空间使用:空闲列表
年轻代vs老年代
按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。
GC评估指标
面试题
请问吞吐量的优化和响应优先的垃圾收集器是如何选择的呢?(滴滴)
吞吐量优先选择什么垃圾回收器?响应时间优先呢? (阿里)
吞吐量公式:
用户线程执行时间÷用户线程+垃圾回收线程=吞吐量
吞吐量:程序的运行时间(程序的运行时间+内存回收的时间)。
暂停时间(响应时间):执行垃圾收集时,程序的工作线程被暂停的时间。每个GC一定都会有暂停时间,有些暂停时间非常短,称为低延迟
收集频率:相对于应用程序的执行,收集操作发生的频率。
内存占用: Java 堆区所占的内存大小。
快速: 一个对象从诞生到被回收所经历的时间。
吞吐量如果要大,增加堆的大小,吞吐量越大暂停时间越长(一次暂停长)
简单来说,主要抓住两点:
吞吐量 —>吞吐量越大越好!
暂停时间(或响应时间) --> 追求低延迟
公式应用,吞吐量优先(跟暂定时间比)
上面越大就越大
评估指标:
直觉上,吞吐量越高程序运行越快。吞吐量越多用户执行操作越多,运行就越快。
低暂停时间(低延迟)较好因为从最终用户的角度来看不管是GC还是其他原因导致一个应用被挂起始终是不好的。这取决于应用程序的类型,有时候甚至短暂的200毫秒暂停都可能打断终端用户体验。因此,具有低的较大暂停时间是非常重要的,特别是对于一个交互式应用程序。(与用户打交道的场景下最求低延迟)
现在JVM调优标准:在最大吞吐量优先的情况下,降低停顿时间。
垃圾回收器有哪些?
面试题:
GC 收集器有哪些?(滴滴)
几种垃圾回收器(亚信)
垃圾回收器有哪些?都有哪些算法来实现?项目中用的垃圾回收器是什么?(平安)
垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?(平安)
你知道那些垃圾回收器(高德地图)
有哪些垃圾方法,垃圾收集器是什么。(新浪)
JVM有哪三种垃圾回收器? (阿里)
7款经典收集器与垃圾分代之间的关系
新生代:C儿GC,拍瑞郎GC,拍扭GC
老年代:C儿oldGC,派瑞oldGC,CMSGC
新老:G佛斯特,横跨两者
历史:
1999年JDK1.3,Serial GC ,串行
Parallel GC在JDK6和8之后成为HotSpot默认GC。
2012年,在JDK1.7u4版本中,G1可用。-XX:+UseG1GC不是默认的垃圾回收,显示输入
2017年,JDK9中G1变成默认的垃圾收集器,以替代CMS。
2018年9月,JDK11发布。引入Epsilon 垃圾回收器,又被称为"No-Op(无操作)"回收器。同时,引入ZGC(未来的方向):可伸缩的低延迟垃圾回收器(Experimental)。
2020年3月,JDK14发布。删除CMS垃圾回收器。扩展ZGC在macOS和Windows上的应用
7种GC组合关系
jdk8中PSGC+P OGC
为什么这么多GC?
为什么要有很多收集器,一个不够吗?因为Java的使用场景很多,移动端,服务器等。所以就需要针对不同的场景,提供不同的垃圾收集器,提高垃圾收集的性能。
虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来。没有一种放之四海皆准、任何场景下都适用的完美收集器存在,更加没有万能的收集器。所以我们选择的只是对具体应用最合适的收集器。
如何查看默认GC:
-XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
使用命令行指令:jinfo –flag 相关垃圾回收器参数 进程ID
Serial GC:串行回收
概述:
回收新生代采用复制算法
Serial Old用的标记清除算法
优势:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
小结:
在单核CPU场景下或跟用户交互场景下配置不高的时候选择这个回收器
ParNew GC:并行回收
概述:
Serial GC的一个多线程版
Par是Parallel的缩写,New:只能处理的是新生代
多核硬件配置不错的场景下比单核的好。
对于新生代,回收次数频繁,使用并行方式高效。
ParNew更好?
但是在单个CPU的环境下,ParNew收集器不比Serial 收集器更高效。切换线程的时候耗时,效率就不高了
参数设置:
-XX:ParallelGCThreads 限制线程数量,默认开启和CPU数据相同的线程数。
Parallel GC:吞吐量优先
jdk8用的就是这个,并行关注吞吐量
在Java8中,默认是此垃圾收集器。
概述:
和ParNew收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和”Stop-the-World”机制。
自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。
伊甸园区跟S1,S2是8:1:8,但实际分配的是6:1:1就是这里用到的自适应调节,自适应启用就是为了达到一个可控制的吞吐量
参数设置:
老年代指定CMS,新生代就会切换到ParNew GC,就没有开启这个自适应调节了,伊甸园区的实际比例就是8:1:1
CMS :低延迟*
只有老年代用的CMS时候,新生代才用ParNew GC
面试题:
CMS特点,垃圾回收算法有哪些?各自的优缺点,他们共同的缺点是什么? (天猫)
讲一下CMS垃圾收集器垃圾回收的流程,以及CMS的缺点 (抖音)
说几个垃圾回收器,cms回收器有哪几个过程,停顿几次,会不会产生内存碎片。老年代产生内存碎片会有什么问题。 (小米)
g1和cms区
别,吞吐量优先和响应优先的垃圾收集器选择 (携程)
概述:
JDK 1.5 时期,HotSpot 推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器
这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
CMS的垃圾收集算法采用标记-清除算法,并且也会”Stop-the-world”
CMS 作为老年代的收集器,新生代只能选择ParNew或者Serial收集器中的一个。
初始标记(STW):暂时时间非常短,标记与GC Roots直接关联的对象。
并发标记(最耗时):从GC Roots开始遍历整个对象图的过程。不会停顿用户线程
重新标记:(STW):修复并发标记环节,因为用户线程的执行,导致数据的不一致性问题
并发清理(最耗时)
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。
初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(比如:由不可达变为可达对象的数据),这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
为什么不用标记整理算法:(标记整理算法)不用的就删除,有用的排列到一起。因为并发回收,用户还在执行,内存还在占着呢?这时候移动走了那就玩了。
CMS的优点:
并发收集
低延迟
CMS的弊端:
1)会产生内存碎片(标记清除导致碎片化),导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
2)**CMS收集器对CPU资源非常敏感。**在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
3)CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure”失败而导致另一次 Full GC 的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
CMS产生碎片,长时间不处理,一个大对象过来被动触发Full GC。
用户线程垃圾线程同时执行,即使空间够,新对象撑不下,就down掉了,用备用方案,是个单线程的直接寄u很慢。
设置参数,完成指定多少次GC就开始压缩整理:
-XX:CMSFullGCsBeforeCompaction设置在执行多少次Full GC后对内存空间进行压缩整理。(一次)
-XX:ParallelCMSThreads 设置CMS的线程数量。(多次)
jdk14后被干掉了
G1 GC:区域划分式 *
面试题:
G1原理。(亚信)
CMS和G1了解么,CMS解决什么问题,说一下回收的过程。(字节跳动)
CMS回收停顿了几次,为什么要停顿两次。(字节跳动)
CMS过程是怎样的?内部使用什么算法做垃圾回收? (美团)
CMS 收集器与 G1 收集器的特点。(滴滴)
CMS 收集器与 G1 收集器的特点。 (蚂蚁金服)
G1回收器讲下回收过程 (蚂蚁金服)
你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。(拼多多)
你知道哪几种垃圾回收器,各自的优缺点,重点讲一下cms和g1,包括原理,流程,优缺点 (蚂蚁金服)
Java的垃圾回收器都有哪些,说下g1的应用场景,平时你是如何搭配使用垃圾回收器的 (滴滴)
为什么需要G1?
原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求》
最大吞吐量情况下最低延迟的需求。
官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。
概述:全部打成小区域
在JDK1.7版本正式启用,是JDK 9以后的默认GC选项,取代了CMS 回收器。
特点:与其他 GC 收集器相比,G1使用了全新的分区算法,其特点如下所示:
并行与并发
并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
分代收集
从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;
整体上来看是个标记压缩算法,整体没有碎片,Region之间是复制算法
操作步骤:
G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:
第一步:开启G1垃圾收集器
第二步:设置堆的最大内存
第三步:设置最大的停顿时间
G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和Full GC,在不同的条件下被触发。
使用场景:
面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)
例子:
web应用,java进程最大堆4G,每分钟1500个请求,45s年轻代的垃圾回收。
31小时使用率达到了45%,则开发并发标记,进行混合回收。
垃圾回收过程:
1:年轻代GC
2:并发标记过程
3:混合回收
4:FullGC
GC新发展
革命性的ZGC
分析GC日志
GC日志参数
GC日志格式
案例演示
GC日志分析工具
GCeasy在线的,把gc.log导入进去