1.GC概述
1.1 什么是垃圾
(1)垃圾是指在运行程序中没有任何指针指向的对象
(2)如果不及时对内存中的垃圾进行清理,这些垃圾对象所占的内存空间就会一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至可能导致内存溢出
1.2 为什么需要GC?
(1)不进行垃圾回收,内存迟早会被消耗完
(2)垃圾回收也可以清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象
1.3 早期垃圾回收
在早期的C/C++时代,垃圾回收基本上是手工进行的,开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放。这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。如果程序员编码时忘记回收则会造成内存泄漏,严重会造成程序崩溃。
1.4 Java垃圾回收机制
(1)Java采用的是自动内存管理,无需开发人员参与内存的分配和回收,这样可以降低内存泄漏和内存溢出的风险。
(2)自动回收会弱化Java开发人员在程序出现内存溢出时的定位问题和解决问题的能力
(3)当出现内存泄漏、内存溢出问题时,我们需要实施必要的监控和调节
1.5 评估GC的性能指标
(1)吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间=程序的运行时间+内存回收的时间)
(2)暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
2.垃圾回收相关算法
2.1 垃圾标记阶段的算法
(1)在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
(2)当一个对象已经不再被任何的存活对象继续引用时,JVM中就会标记为死亡对象。
(3)判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
2.1.1 引用计数算法(Java没有使用)
(1)引用计数算法是对每一个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。
(2)对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收了。
(3)优点:实现简单,垃圾对象便于辨识;判定效率高,且回收没有延迟性。
(4)缺点:
①它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
②每次赋值都需要重新计数,伴随着加法和减法操作,增加了时间开销。
③引用计数器无法处理循环引用。这是致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
(4)python使用了引用计数算法,它是如何解决循环引用的?
①手动解除:在合适的时机,解除引用关系
②使用弱引用weakref,weakref是python提供的标准库,旨在解决循环引用。
2.1.2 可达性分析算法
(1)可达性分析算法也叫,根搜索算法、追踪性垃圾收集。不仅实现简单、执行高效,而且可以解决循环引用问题
(2)可达性分析的基本思路
①可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达(所谓GC Roots是一组必须活跃的引用)
②使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链
③如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象
④在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象
(4)在Java语言中,GC Roots包括以下几类元素:
①虚拟机栈在引用的对象:比如各个线程被调用的方法中使用到的参数、局部变量等。
②本地方法栈内(通常说的本地方法)引用的对象
③方法区中类静态属性引用的对象,比如:Java类的引用类型静态变量
④方法区中常量引用的对象:比如字符串常量池(String Table)里的引用
⑤所有被同步锁synchronized持有的对象
⑥Java虚拟机内部的引用:基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError)以及系统类加载器
(5)除了这些固定的GC Roots集合意外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”的加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(Partical GC)
2.2 垃圾清除阶段的算法
(1)当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存
(2)目前在JVM中比较常见的垃圾收集算法有:标记-清除算法、复制算法、标记-压缩算法
2.2.1 标记-清除(Mark-Sweep)算法
(1)执行过程:当堆中的有效内存空间被耗尽的时候,就会停止整个程序,然后进行两项工作,第一项是标记,第二项则是清除
①标记:Collector从根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
②清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象的Header中没有标记为可达对象,则将其回收。
(2)优点:简单容易理解
(3)缺点:
①效率不高(从根节点标记,递归遍历可达对象,复杂度O(n),清除阶段需要把堆空间的对象遍历一遍,O(n))
②在进行GC的时候,需要停止整个应用程序,导致用户体验差
③这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表
(3)何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就直接覆盖原有位置。
2.2.2 复制(Copying)算法
(1)为了解决标记-清除算法在垃圾收集效率方面的缺陷,引入了复制算法
(2)核心思想:将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
(3)优点:
①没有标记和清除过程,实现简单,运行高效
②复制过去以后保证空间的连续性,不会出现“碎片”问题
(4)缺点:
①需要两倍的内存空间
②对于G1垃圾收集器这种拆成大量r egion的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小
(5)复制算法需要复制的存活对象数量不太多的情况。在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
2.2.3 标记-压缩(标记-整理Mark-Compact)算法
(1)标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次碎片整理。因此,也可以把它称为标记-清除-压缩算法。
(2)执行过程
①第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
②第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间
(3)优点:
①清除了标记-清除算法中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
②消除了复制算法当中,内存减半的高额代价
(4)缺点:
①从效率上说,标记-整理算法要低于复制算法
②移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
③移动过程中,需要全程暂停用户应用程序。
2.3 分代收集算法
(1)分代收集算法是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
(2)目前几乎所有的GC都是采用分代收集算法执行垃圾回收的。在HotSpot( SUN的JDK版本从1.3.1开始运用HotSpot虚拟机)中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点:
①年轻代:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。这种情况复制算法是最快的。
②老年代:区域较大,对象生命周期长、存活率高。一般是标记-清除或者是标记-清除与标记-整理的混合实现。
3.垃圾回收相关概念
3.1 System.gc()的理解
(1)在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显示触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。System.gc()的底层就是Runtime.getRuntime().gc()
(2)System.gc()只是尝试调用垃圾收集器,但是不能保证对垃圾收集器一定调用
(3)JVM实现者可以通过System.gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无需手动触发。但在一些特殊情况下,如我们正在编写一个性能基准,我们就可以在运行之间调用System.gc()
3.2 内存溢出(OOM)
(1)javadoc中对OutOfMemoryError的解释是:没有空闲内存,并且垃圾收集器也无法提供更多内存。
(2)没有空闲内存,说明Java虚拟机的堆内存不够,原因有二:
①Java虚拟机的堆内存设置不够。比如说:可能存在内存泄漏问题;也有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小。我们可以通过-Xms、-Xmx来调整
②代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
(3)在抛出OOM之前,通常垃圾收集器会被触发,尽其所能去清理出空间。例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等。当然,也不是任何情况下垃圾收集器都会被触发,比如:我们去分配一个超大数组超过堆的最大值,JVM会判断出垃圾收集并不能解决这个问题,会直接抛出OOM
3.3 内存泄漏
(1)也叫存储泄漏,指对象不会再被程序用到,但是GC又不能回收他们的情况。一旦发生内存泄漏,程序中的可用内存最终会被耗尽,出现OOM。
(2)举例
①单例模式:单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生
②一些提供close的资源未关闭导致内存泄漏。数据库连接,网络连接(socket)以及IO连接必须手动close,否则是不能被回收的。
3.4 Stop The Word(STW)
(1)Stop The Word(简称STW),指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
(2)SWT是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。被STW中断的应用程序线程会在完成GC之后恢复,频繁中断降低用户体验感,我们要减少STW的发生。在开发中尽量不要用System.gc(),会导致SWT的发生。
(3)可达性分析算法中枚举根节点(GC Roots)会导致所以Java执行线程停顿
(4)STW事件和采用哪款GC无关,所有的GC都会有这个事件。哪怕是G1也不能完全避免Stop The Word情况发生,只能说垃圾回收器越来越优秀,回收率越高,尽可能的缩短了暂停时间。
3.5 垃圾回收的并发与并行
(1)并行:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
(2)串行:单线程执行。如果内存不够,则程序暂停,启动JVM垃圾收集器进行垃圾回收,回收完,再启动程序的线程。
(3)并发:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。用户线程在继续执行,而垃圾收集程序线程运行于另一个CPU上。
4.强引用、软引用、弱引用和虚引用
(1)强引用:是指在程序代码中普遍存在的引用赋值,即类似“Object obj = new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
(2)软引用:在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
(3)弱引用:被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被软引用关联的对象。
(4)虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。
5.垃圾回收器
(1)垃圾收集器与垃圾分代之间的关系
①新生代收集器:Serial、ParNew、Parallel Scavenge
②老年代收集器:Serial Old、Parallel Old、CMS
③整堆收集器:G1
5.1 Serial和Serial Old垃圾收集器:串行回收
5.1.1 Serial垃圾收集器
(1)Serial垃圾收集器是最基本、历史最悠久的垃圾收集器。JDK1.3之前回收新生代唯一的选择
(2)Serial垃圾收集器是Client模式下的默认新生代垃圾收集器
(3)Serial垃圾收集器采用复制算法、串行回收和“Stop-the-Word”的机制执行回收
5.1.2 Serial Old垃圾收集器
(1)Serial Old垃圾收集器同样也采用了串行回收和“Stop-the-Word”机制,只不过内存回收算法使用的是标记-压缩算法。
(2)Serial Old是Client模式下默认的老年代垃圾回收器
(3)Serial Old在Server模式下主要有两个用途:
①与新生代的Parallel Scavenge配合使用
②作为老年代CMS收集器的后备垃圾收集方案
5.2 ParNew回收器:并行回收
(1)ParNew收集器是Serial收集器的多线程版本。ParNew也是采用复制算法、“Stop-the-Word”机制。
(2)ParNew收集器运行在多CPU的环境下,可以充分利用多CPU、多核心等物理硬件资源的优势,更快的完成垃圾收集,提升程序的吞吐量。但是在单个CPU的环境下,ParNew收集器没有Serial收集器效率高。虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁的做任务切换,因此可以有效的避免多线程交互过程中产生的一些额外开销。
(3)除了Serial外,目前只有ParNew能与CMS收集器配合工作。
5.3 Parallel Scavenge回收器、Parallel Old:吞吐量优先
5.3.1 Parallel Scavenge回收器
(1)Parallel Scavenge和ParNew一样是基于并行回收,采用复制算法、“Stop-the-Word”机制。Parallel Scavenge是吞吐量优先的垃圾收集器。
(2)自适应调节策略(在JVM运行过程中,根据当前的运行情况进行一个性能的监控,动态调整内存中的分配的情况,以达到最优策略,比如吞吐量最优)也是Parallel Scavenge和ParNew一个重要区别
5.3.2 Parallel Old回收器
Parallel Old收集器,是用来代替老年代的Serial Old收集器。Parallel Old和Serial Old一样采用标记-压缩算法和“Stop-the-Word”机制。但是是基于并行回收。
5.3.3 jdk8默认使用的是ParallelGC(并行垃圾回收器),也就是Parallel Scavenge + Serial Old的组合。
5.4 CMS回收器:低延迟
(1)CMS即Concurrent-Mark-Sweep收集器,实现了让垃圾收集线程与用户线程同时工作
(2)CMS缩短了垃圾收集时用户线程的停顿时间,即低延迟。
(3)CMS采用 标记-清除 算法,并且也会“Stop-the-Word”
5.5 G1回收器
(1)将Parallel Scavenge和CMS两种垃圾收集器的优点融合。
①支持巨大的堆空间回收,并有较高的吞吐量
②允许用户设置最大暂停时间
(2)G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、Survior、Old区。