概述
什么是垃圾
- 什么是垃圾:
- 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾
- 如果不及时进行对内存中的垃圾进行清理,那么这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象所引用,甚至可能导致内存溢出
为什么需要 GC
- 对高级语言来说,一个基本的认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断的分配内存空间而不进行回收,就好像不停的生产生活垃圾而从不打扫一样
- 除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,一边jvm将整理出的内存分配给新对象
- 随着应用程序所应付的业务越来越大、复杂,用户越来越多,没有gc就不能保证应用程序的正常进行,而经常造成STW的GC又跟不上实际的需求,所以才会不断的尝试对GC进行优化
早期的垃圾回收
Java垃圾回收机制
相关算法
垃圾标记阶段:对象存活判断
- 在堆里存放着几乎所有的java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中那些是存活对象,那些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段
- 那么在jvm中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何存活的对象所继续引用时,就可以宣判为已经死亡,
- 判断对象的存活一般有两种方式:引用计数算法和可达性算法分析
引用计数算法
引用计数算法比较简单,对每个对象保存一个整形的引用计数器属性,用记录对象被引用的情况
-
对于一个对象A,只要有任何一个对象引用了对象A,则A的引用计数器就加一,当引用失效时,引用计数器就减一,只要对象A的引用计数器的值为零,即表示对象A不可能再被使用,可进行回收
-
优点:实现简单,垃圾对象易于辨识,判定效率高,回收没有延迟性
-
缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销
- 引用计数器有一个严重的问题就是:无法处理**循环引用(比如循环链表)**的情况,这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法
- 小结
- 引用计数算法是很多语言的资源回收选择,例如Python同时支持引用计数和垃圾收集机制
- 具体那种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试
- Java并没有选择引用计数,是因为其存在一个基本难题,也就是很难处理循环引用关系
- Python怎么解决循环引用?
- 手动解除:在合适的时机,解除引用关系
- 使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环引用
可达性分析算法(根搜索算法、追踪性垃圾收集)
-
相对于引用计数算法而言,可达性分析算法 不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效的解决在引用计数算法中循环引用的问题,防止内存泄漏
-
可达性分析算法就是Java、C#选择的,这种垃圾收集通常也叫做追踪性垃圾收集
-
GC Roots
- 所谓的GC Roots根集合就是一组必须活跃的引用
- 在Java语言中 GC Roots包括以下几类元素:
- 虚拟机栈中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等
- 本地方法栈内JNI(本地方法)引用的对象
- 方法区中类静态属性引用的对象:Java类的引用类型静态变量
- 方法区中常量引用的对象:字符串常量池里的引用
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用:基本数据类型对应的Class对象,一些 常驻的异常对象(NullPointerException等),类系统加载器
- 反映Java虚拟机内部的JMXBean,JVMTI中注册的回调、本地代码缓存等
- 除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”的加入,共同构成完整的GC Roots集合:分带收集和局部回收(Partial GC)
- 如果只针对Java堆中某一块内存区域进行垃圾回收(比如只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性
-
基本思路:
- 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被跟对象集合所链接的目标对象是否可达,
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径被称为引用链
- 如果目标对象没有被任何引用链相连 ,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象,
- 在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象
- 判断是不是root的小技巧:
- 由于root采用栈方式存放变量和指针,所以如果一个指针,他保存了堆内存里面的对象 ,但是自己又不存放在堆内存里面,那他就是一个root
- 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行,这点不满足的话分析结果的准确性就无法保证
- 这点也是导致GC进行是必须“Stop TheWorld”的一个重要原因,即使是号称(几乎)不会发生停顿的CMS收集器中 ,枚举根节点时也是必须要停顿的
对象的finalization机制
- Java语言提供了对象终止(finallzation)机制来允许开发人员提供对象被销毁之前的自定义处理
- 当垃圾回收器发现没有引用指向一个对象即垃圾回收此对象之前,总会先调用这个对象的finalize()方法
- finalize()方法允许在子类中重写,用于在对象被回收时进行资源释放,通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等
- 永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用,
- 在finalize()时可能导致对象复活
- finalize()方法的执行时间是没有保障的,它完全有 GC决定,极端情况下,若没有发生GC,finalize方法 将没有执行机会
- 一个糟糕的finalize会严重影响GC性能
- 从功能上来说,finalize方法与C++中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理,所以finalize方法在本质上不同与C++中的析构函数
- 由于finalize方法的存在,虚拟机中对象一般处于三种状态
- 如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说,此对象需要被回收,但事实上,也并非是“非死不可”,这时候的它们暂时处于“缓刑”阶段,一个无法触及的对象有可能在某一条件下复活自己,如果这样那么对她的回收就是不合理的,为此定义虚拟机中的对象可能的三种状态:
- 可触及的:从根节点开始可达
- 可复活的:对象的所有引用都被释放,但是对象可能在finalize中复活
- 不可触及的:对象的finalize被调用,并且没有复活,那么就会进入不可触及状态,不可触及的的对象不可能被复活,因为finalize只会被调用一次
- 以上三种状态中是由于finalize方法的存在,进行的区分,只有在对象不可触及时才会被回收
MAT与Jprofile的GC Roots溯源
-
MAT介绍
- MAT是MemoryAnalyzer的简称,是一款功能强大的Java堆内存分析器。用于查找内存泄漏以及查看内存消耗情况
-
步骤:
-
使用MAT需要生成dump文件
-
方式一:使用jmap命令,生成离线dump文件
jmap -dump:format=n,live,file=test1.bin 进程号
-
方式二:使用JVisualVM导出
-
捕获的heap dump是一个临时文件,关闭JVisualVM后自动删除,
-
在左侧Application子窗口中右击相应的程序,选择heap dump或者在monitor子标签页中点击heap dump按钮
-
本地应用程序的heap dump 作为应用程序标签页的一个字标签页打开,同时,heap dump在左侧的application栏中对应一个含有时间戳的节点,右击这个节点选择save as 即可将heap dump保存到本地
-
-
-
Jprofiler OOM查看
-
-XX:+HeapDumpOnOutOfMemoryError :当程序出现OOM时生成dump文件
-
-
垃圾清除阶段
当成功的区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占的内存空间,以便有足够的可用内存空间为新对象分配内存
目前JVM中 比较常见的三种垃圾收集算法是标记-清除算法、复制算法、标记-压缩算法
标记-清除(Mark-Sweep)算法
-
背景:标记-清除算法是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并应用于Lisp语言
-
执行过程:当堆中的有效内存空间被耗尽的时候,就会停止整个程序(STW),然后进行两项工作:
- 标记:Collector从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的header中记录为可达的对象
- 清除:Collector对堆内存从头到尾进行线性遍历,如果发现某个对象在header中没有标记为可达对象,则将其回收
-
缺点:
- 效率不算高
- 在进行GC的时候需要停止整个程序,用户体验差
- 清理出来的空闲内存是不连续的,产生内存碎片,需要维护一个空闲列表
-
何为清除:
- 这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否够
复制算法
-
背景:为了解决标记 -清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表论文“使用双存储区的Lisp语言垃圾收集器”,其在论文中描述的算法被人们称为复制算法,
-
核心思想:将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收
-
优点:
- 没有标记和清楚过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现碎片问题
-
缺点:
- 此算法的缺点也是很明显的,就是需要两倍的内存空间
- 对于G1这种拆分成大量region的GC,复制而不是移动,意味着GC需要维护region直接对象引用关系,不管是内存占用或者是时间开销也不小
-
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,或者说非常低
标记-压缩算法
-
背景:复制算法的高效性是建立在存活对象少,垃圾对象多的前提下的,这种情况在新生代经常发生,但是 在老年代,更常见的是大部分对象都是存活对象,如果依然使用复制算法,由于存活对象多,复制的成本也将很高,因此基于老年代垃圾回收的特性,需要使用其他算法。 标记-清除算法的确可以 应用在老年代,但是该算法不仅效率低下,而且在执行完回收内存后还会产生内存碎片,所以JVM的设计值在此基础上进行改进,标记-压缩算法由此诞生
-
执行过程:第一阶段和标记-清除算法相同,第二阶段将所有存活对象压缩到内存的一端按顺序排放之后清除边界外所有的空间
-
标记-压缩算法的最终效果和标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-整理算法
-
二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的,是否移动回收后的存活对象是一项优缺点并存的风险决策
-
优点:
- 消除了标记-清除算法汇总内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个 内存的起始地址即可
- 消除了复制算法中,内存减半的高额代价
-
缺点:
- 从效率上来说,标记-压缩算法低于复制算法
- 移动对象的同时,如果对象被其他对象引用,还需要调整引用的地址
- 移动过程中,需要STW
三种算法对比
分代收集算法
不同的对象的生命周期是不同的,因此,不同生命周期的对象可以采用不同的收集方式,一遍提高效率,一般是吧java堆分为新生代和老年代,这要就可以根据各个年代的特点使用不同的回收算法,提高回收效率
增量收集算法
分区算法
相关概念
System.gc()
- 默认情况下通过System.gc()或者Runtime.getRuntime.gc()的调用,会显示触发Full GC 同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存
- 然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用
- JVM实现者可以通过System.gc()调用来决定JVM的GC行为,而一般情况下,垃圾回收应该是自动进行的,无需手动触发,在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()
- 使用System.gc()提醒jvm垃圾回收器执行gc,但是不保证能否马上执行,当调用System.runFinalization()方法时,强制 调用引用对象的finalize方法
内存溢出与内存泄漏
-
内存溢出
- 由于GC一直在发展,所以一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不容易出现OOM
- 大多数情况下GC会进行各种年龄段的垃圾回收,实在不行就进行一次Full GC操作,这时候会回收大量的 内存,供应用程序继续使用
- javadoc中对OOM的解释是,没有空闲内存,并且垃圾回收器也无法提供更多内存
- 通常情况下,当内存满时,总是要先进行一次GC,如果空间还是不足才会报OOM,但当我们分配一个超大对象,JVM可以 判断出垃圾收集不能解决问题时,会直接报OOM
-
内存泄漏
-
严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏
-
但实际情况很多时候一些不太好的实践会导致对象的生命周期变得很长甚至导致OOM也可以叫做宽泛意义上的内存泄漏
-
尽快内存泄漏不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的内存就会被逐步蚕食,直至耗尽内存引发OOM
-
这里 的存储空间并不是指物理内存,而是指虚拟内存的大小,这个虚拟内存大小取决于磁盘交换区设定的大小
-
-
举例:
- 单例模式,单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的
- 一些提供close的资源未关闭而导致内存泄漏,数据库连接,网络连接,和IO连接必须手动关闭,否则是不能被回收的
-
STW
- 指GC事件发生过程中,会产生应用程序的停顿,停顿时整个应用程序线程都会被暂停,会产生应用程序停顿,停顿时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿被称为STW
- 可达性分析算法中枚举根节点会导致所有java执行线程停顿,
- 分析工作必须在一个确保一致性的快照中进行,
- 一致性指整个分析期间整个执行系统看起来就像被冻结在某个时间点上,
- 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
- 可达性分析算法中枚举根节点会导致所有java执行线程停顿,
- 被STW中断后的子程序会在GC完成之后恢复 ,频繁的终断会让用户感觉像是网速不快造成电影卡啊样,所以我们要减少STW的发生
- STW与采用哪种GC无关,所有的GC都有此事件
- STW是JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部听到
- 开发中不要用System.gc()。会导致STW的发生
垃圾回收的并行与并发
- 并行:指多条垃圾收集线程并行工作,但 此时用户线程仍处于等待状态,
- 串行:相较于并行的概念,单线程执行;如果内存不够则程序暂停,启动jvm垃圾回收器进行垃圾回收,回收完再启动程序线程
- 并发:指用户线程与垃圾收集线程同时执行,不一定是并行的,但是不会停顿用户程序的执行
- 用户程序在继续运行,而垃圾收集程序线程运行在另一个CPU上
- 如:G1,CMS
安全点与安全区域
- 安全点
- 程序执行时并非在所有地方都能停顿下来开始 GC,只有在特定的位置才能停顿下来进行GC,这些位置称为安全点
- 安全点的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题,大部分指令的执行时间都非常短暂,通常会根据 是否具有让程序长时间执行的特征 为标准。比如 选择一些执行时间较长的指令作为安全点,如方法调用,循环跳转,异常跳转等
- 如何在GC发生时,检查所有的线程都跑到最近的安全点停顿下来呢?
- 抢先试中断(目前没有虚拟机采用了)
- 首先中断所有线程,如果还有线程不在安全点,就恢复线程,让线程跑到安全点
- 主动式中断
- 设置一个中断标志,各个线程 运行到安全点时主动轮询这个标志,如果中断标志位真,则将自己进行中断挂起
- 抢先试中断(目前没有虚拟机采用了)
- 安全区域:
- 安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的安全点,但是程序“不执行”的时候呢,例如线程处于sleep状态,或blocked状态,这时候线程 无法响应jvm中断 请求,去运行到安全点进行中断,jvm也不会等待线程被唤醒,这时候就需要安全区域来解决问题
- 安全区域指一段代码片段中,对象的引用关系不会发生 变化,在这个区域中的任何位置开始GC都是安全的,
- 实际执行时,
- 当线程运行到安全区域是,首先标识已经进入 了安全区域,如果这段时间发生了GC,jvm会忽略标识为安全区域状态的线程
- 当线程即将离开安全区域时 ,会检查是否已经执行完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开安全区域的信号为止
引用
Reference子类汇总只有终结器 引用是包内可见的,其他三种引用类型均为public可以在应用程序中直接使用
- 强引用、软引用、弱引用、虚引用有什么区别,具体使用场景是什么
强引用-不回收
-
最传统的引用的 定义,是指在程序代码中普遍存在的引用赋值,即类似
Object o=new Object();
这种引用关系,无论任何情况下,只要强引用关系还存在,垃圾回收器就永远不会回收掉被引用的对象 -
在Java 系统中,最常见的引用类型是强引用,也就是我们最常见的普通对象的引用,也是默认的引用类型
-
当在Java语言中使用new 操作符创建一个新的对象,并将其 赋值给一个变量的时候,这个变量就称为指向该对象的一个强引用
-
强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象
-
对于一个普通的 对象,如果没有其他的引用关系,只要超过了引用的作用域或者显示的讲引用赋值为null就可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策略
-
相对的,软引用、弱引用、虚引用的对象是软可触及、弱可触及、虚可触及的,在一定条件下,都是可以被回收的,所以强引用造成java内存泄漏的主要原因之一
-
强引用特点:
- 强引用可以直接访问目标对象
- 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向的对象
- 强运营可能导致内存泄漏
软引用-不够才回收
-
在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收,如果这次回收后还没有足够的内存,才会抛出OOM
-
软引用使用来描述一些还有用,但非必要的对象,只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象例会回收范围之中进行的第二次回收,如果这次还没有你足够的内存,才会抛出内存溢出异常
-
软引用通常用来实现内存敏感的缓存,比如:高速缓存就有用到 软引用,如果还有空闲内存,就可以暂时保留内存,当内存不足时清理掉,这样就保证了使用缓存的同时,还不会耗尽内存
-
垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选的吧引用存放到一个引用队列
-
类似弱引用只不过java虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理
弱引用-发现即回收
- 被弱引用关联的对象只能生存到下一次垃圾收集之前,当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象
- 由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快的发现持有弱引用的对象,这种情况下,弱引用对象可以存在较长时间
- 弱引用和软引用一样,在构建弱引用时,可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况
- 弱引用和软引用都非常适合保存那些可有可无的缓存数据,这么做当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出,而当内存资源充足时,缓存数据又可以存在相当长的时间,从而起到加速系统的作用
虚引用-对象回收跟踪
- 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
- 是所有引用里面最弱的一个
- 不能单独使用,也无法通过虚引用来获取被引用的对象,当试图通过虚引用的get来获取对象时,总是返回null
- 为一个对象设置虚引用关联的唯一目的是在于跟踪垃圾回收过程
- 虚引用必须和运用队列一起使用,虚引用在创建时必须提供一个引用队列作为参数,当垃圾回收器准备回收一个对象时,如果发现 他还有虚引用,就会在回收对象后,将虚引用加入引用队列,以通知程序对象的回收情况
- 由于虚引用可以跟踪对象的回收时间,因此也可以将一些资源释放操作放置在虚引用中执行和记录
终结器引用
- 用以实现对象的finalize方法,
- 无需手动编码,其内部配合引用队列使用
- 在GC时终结器引用入队,有finalizer线程通过终结器引用找到被引用对象并调用finalize方法,第二次GC时才能回收被引用对象