前瞻
引言
垃圾的定义
垃圾是指在运行程序中没有任何指针指向的对象。
如果不及时对这些垃圾对象进行垃圾回收,那么这些垃圾对象会一直占用一定的内存空间直到应用程序结束,这些空间无法被其他对象使用,甚至可能造成内存溢出。
为什么需要GC
GC(Garbage Collection,垃圾回收器)
如果只分配内存,而不进行回收,那么内存迟早会被消耗完。
GC除了可以回收内存,还可以进行碎片化整理,否则一些过小的内存在面对较大的对象时无法被分配,将使用的堆内存移到堆的一端,以便JVM将整理出来的内存分配给新的对象。
JAVA中的垃圾回收
Java中的自动内存管理机制,无需开发人员手动的进行内存分配与回收,使开发人员专注于业务开发,同时也可以降低内存溢出和内存泄露的风险。
Java堆是垃圾回收器工作的主要区域。
垃圾回收的频率:频繁回收Younger区,较少回收Old区,基本不动Perm区(元空间)。
垃圾判断算法
堆中几乎存放着所有Java实例,GC在进行垃圾回收前,我们要判断出哪些对象是存活的,哪些对象是死亡的,只有被标记的死亡对象,GC才会在垃圾回收阶段释放其所占用的内存空间,所以这个阶段也称为垃圾标记阶段。
判断一个对象是否存活的本质就是判断这个对象是否被其他存活的对象所引用。垃圾判断算法也分为两种:1,引用计数算法,2,可达性分析算法
引用计数法
引用计数法(Reference Counting)实现思想比较简单,对每个对象保存一个整型的引用计数属性,用来记录这个对象的被引用情况。
优点:实现简单,垃圾对象便于辨识;判定效率高,垃圾回收没有延迟。
缺点:
- 需要给对象新增一个属性,增加了存储空间开销。
- 该对象每次被引用或被取消引用时都会对引用计数属性进行操作,所以会增加时间开销。
- 无法处理循环引用问题(致命缺陷),这也是引用计数法没有被采用的主要原因。
循环引用:即两个或多个对象互相引用,虽然其引用计数属性不为0,但其实际上已经是垃圾对象了。
可达性分析算法
相较于引用计数算法,可达性分析算法不仅同样有实现简单,执行高效的优点,还解决了对象循环引用的问题,解决了内存泄漏的问题。这个算法也被称为追踪性垃圾收集。
基本思路:
- 可达性分析是以根对象集合(GC roots)为起始点,按从上往下的方式搜索被根对象集合连接的目标对象是否可达。
- 使用可达性分析算法后,内存中存活的对象都被根对象集合(GC roots)直接或间接的连接着,搜索所经过的路径被称为引用链(Reference Chain)。
- 如果目标对象没有与任何引用链相连,则认为该对象是不可达的,就将其标记为垃圾对象。
- 在可达性分析算法中,只有和根对象集合直接或间接相连的对象,才认为是存活的。
GC Roots包括哪些对象?
- 虚拟机栈中引用的对象。如:Java线程中,当前所有被调用的方法的引用类型参数,局部变量等。
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象 。如:字符串常量池里的引用
- 所有被同步锁synchronized持有的对象
- java虚拟机内部引用的对象
注意:
如果使用可达性分析算法判断对象是否为垃圾对象,分析需要在一个能保证一致性的快照中进行,否则,分析的准确性无法保证。这也是GC进行时需要“Stop The World”的一个重要原因。即使是号称不会停顿的CMS垃圾回收器,枚举根节点也是需要停顿的。
垃圾清除算法
当区分出内存中的存活对象与死亡对象后,GC就要进行垃圾回收,释放掉已经死亡的对象所占用的内存空间,以便有足够的空间为新的对象分配内存。
JVM中常见的三种垃圾清除算法:
- 标记-清除算法
- 复制算法
- 标记-复制算法
标记-清除算法
标记-清除算法是最基础的收集算法,分为“标记”和“清除”两个阶段
标记:从GC Roots开始遍历,标记所有可达的对象。一般是在对象头中记录是否是可达。
清除:对堆内存从头到尾遍历,如果发现某个对象没有被标记为可达的对象,则将其回收。
优点:不需要进行的对象的移动,仅对垃圾对象进行处理,在存活对象较多的情况下效率极高。
缺点:
- 标记和清除的效率都不算高。
- 需要使用一个空闲列表来记录所有空闲的内存空间及其大小,对空闲列表的管理会增加分配对象时的工作量。
- 会产生大量的不连续的内存碎片。
标记-整理算法
标记-整理算法分为“标记”和“整理”两个阶段
标记:和标记-清楚算法的标记阶段一样,都是从GC Roots开始,标记可达的对象。
整理:将所有存活的对象整理到内存的一端,然后清除端边界外所有的垃圾对象。
标记-整理算法相当于标记-清除算法执行之后又对内存进行了一次碎片整理,所以也被称为标记-清除-压缩算法。在对存活对象进行内存整理时,将对象按内存地址依次排放。在给新对象分配内存时,JVM只需持有一个内存起始地址即可,这比维护一个空闲列表节省了很多开销。
优点:解决了内存区域分散的问题。
缺点:
- 移动对象的时候,还需要修改引用该对象的引用地址。
- 移动过程中,需要全程暂停用户应用程序。
复制算法
核心思想:将内存分为相等的两部分,每次只使用一部分,在进行垃圾回收的时候将正在使用的内存中存活的对象复制到未使用的内存,然后清理正在使用的内存空间,交换两块内存的角色。
对于这种算法,适用于存活对象较少的场景。在年轻代中,就是使用的这种算法。
优点:复制之后能够保证空间的连续性,不会出现碎片化问题。
缺点:
- 有一半的内存空间不能被使用,空间利用率较低。
- 如果存活对象较多,需要进行的复制操作也比较多,效率相对来说较低。
比较三种算法:
标记-清除算法 | 标记-整理算法 | 复制算法 | |
速度 | 中等 | 最慢 | 最快 |
空间利用率 | 高(但有碎片化问题) | 高(无碎片化问题) | 只能使用一般的空间 |
移动对象 | 否 | 是 | 是 |
分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块,一般将JVM堆分为新生代和老年代,根据各个区域的特点选择合适的垃圾回收算法。
新生代(复制算法):几乎所有新的对象都会被放在新生代(极大的对象会被分配到老年代),新生代区按照8:1:1的比例分为Eden(伊甸园区)和两个Survivor(幸存区),两个幸存区分为from区和to区(谁空谁是to区)。当为新对象分配空间,Eden区空间不足时,会触发Minor GC,第一次MinorGC会将Eden区所有存活的对象复制到From区,然后清空Eden区。之后触发Minor GC时,会将Eden区和From区中存活的对象复制到To区,然后清空Eden区和From区,此时From区是空的,From区和To区名字交换(谁空谁是To区)。当To区不足以存放Eden区和From区中存活的对象时,就把存活的对象复制到老年区。对象在Survivor区中每经历一次Minor GC,年龄就会加1(对象被放到Survivor中时,年龄设为1),默认情况下,对象年龄达到15时就会晋升到老年代。若是老年代也满了,则会触发FullGC,即对新生代和老年代都进行垃圾回收。
老年代(标记-清除算法或标记-整理算法):在新生代中经历了N次Minor GC后存活的对象会被放到老年代,所以老年代中存放的是一些生命周期比较长的对象,内存也比新生代大的多(2:1)。当老年代空间不足以再次分配对象时会触发FUll GC,即对新生代和老年代都进行垃圾回收。但Full GC一般发生的频率较低,老年代中的对象存活的时间较长,存活率高。一般来说,大对象会直接分配到老年代中,所谓大对象是指需要大量连续存储空间的对象,最常见的就是大的数组。
垃圾回收相关概念
System.gc()
- 在默认情况下,通过调用System.gc()或Runtime.getRuntime.gc(),会触发显示Full GC,对新生代和老年代进行垃圾回收,回收垃圾对象占用的内存。
- 调用System.gc()会附带一个免责声明,无法保证垃圾回收器一定调用。
STW
STW即Stop The World,指的是在进行垃圾回收的时候,会产生应用程序的停顿。停顿产生时会暂停整个应用程序线程,这个停顿称为“STW”,可达性分析算法中从枚举GC Roots会导致Java执行线程停顿。
GC时为什么会有停顿?
内存溢出
内存泄漏
一些对象程序不会用到,但是也无法被GC回收,这种情况称为内存泄漏。
内存泄漏举例:
- 一些提供close()方法的资源但未调用close()。如:数据库连接,网络连接(socket),io连接都必须手动调用close,否则不会被回收。
- 匿名内部类创建静态实例。
四种引用类型
无论是通过引用计数法判断对象的被引用次数,还是通过可达性分析算法判断对象的引用链是否可达,判断对象的存活都与“引用”有关。
除了我们平时常用的强引用(StrongReference)外,还有软引用(SoftReference),弱引用(WeakReference),虚引用(PhantomReference),引用强度依次降低。
强引用
我们平时使用的基本上都是强引用,这是最普遍的引用,类似于“Object object=New Object()”。一个对象具有强引用,就相当于不可缺少的生活用品,垃圾回收器绝对不会回收它。当内存空间不足时,JVM宁可抛出OutOfMemoryError错误,是程序异常终止,也不会回收强引用的对象。
软引用
如果一个对象具有软引用,相当于可有可无的生活用品,如果内存空间充足时,不会回收这类对象,如果内存空间不足时,该对象的内存都会被垃圾回收器回收。软引用可以用来实现内存敏感的高速缓存。
弱引用
弱引用与软引用类似,如果一个对象具有弱引用,那么该对象相当可有可无的生活用品。但其区别在于:如果一个对象只具有弱引用,那么它具有更短的生命周期。当垃圾回收器工作时,无论当前内存空间是否充足,都会将该对象回收。不过,弱引用是一个优先级很低的线程,因此不一定很快会发现那些只具有弱引用的对象。
虚引用
虚引用顾名思义,就是形同虚设。和其他几种引用不同,虚引用不会决定对象的生命周期。如果一个对象只有虚引用,那么它和没有任何引用一样,在任何时候都会被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用必须和引用队列(ReferenceQueue)联合使用,当垃圾回收器回收一个对象时,如果发现这个对象还有虚引用,再回收这个对象的内存之前,就将这个虚引用加入到与之相关的引用队列中。程序可以通过判断引用队列中是否有这个虚引用,来了解被引用的对象是否将要被垃圾回收器回收,程序如果发现引用队列中有某个弱引用,就会在被引用的对象被回收前采取必要的行动。
在程序设计中,很少使用弱引用和虚引用,经常使用软引用。软引用可以加速JVM对垃圾对象的回收速度,可以维护系统的运行安全,防止内存溢出等问题的发生。
垃圾回收器
新生代垃圾回收器:Serial,ParNew,Parallel Scavenge
老年代垃圾回收器:Serial Old,CMS,Parallel Old
整堆垃圾回收器:G1
几个相关概念:
- 并行(Parallel):指多条垃圾收集线程并行工作,此时用户线程处于等待状态;
- 并发(Concurrent):指垃圾收集线程和用户线程同时进行(但不一定是并行,可能是交替执行),用户程序继续执行,而垃圾收集器运行在另一个CPU上。
- 吞吐量:运行用户程序时间/(运行用户程序时间+垃圾收集时间)
Serial收集器
Serial(串行)收集器是最古老,最基本的垃圾收集器,顾名思义,它是一个单线程的垃圾收集器。它的“单线程”不仅仅代表它是用一个垃圾收集线程完成垃圾收集,更重要的是它在进行垃圾回收工作的时候会暂停其他所有工作线程(Stop The World)。
Serial与其他垃圾收集器相比,有着简单高效的特点(与其他垃圾收集器的单线程相比),Serial收集器没有线程交互的开销,所有可以有很高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是个不错的选择。
ParNew收集器
ParNew收集器就是Serial收集器的多线程版本,除了使用多线程进行垃圾回收外,其他行为(控制参数,收集算法,回收策略)和Serial收集器一样。
它是许多运行在Server模式下的虚拟机的首要选择。除了Serial收集器外,只有ParNew收集器才能与CMS收集器(真正意义上的并发收集器)配合工作。
Parallel Scavenge收集器
Parallel Scavenge收集器也是使用标记-复制算法的多线程收集器。Parallel Scavenge收集器的关注点是吞吐量。CMS等垃圾回收器关注的更多的是用户线程的停顿时间。Parallel Scavenge收集器提供了许多参数供用户找到合适的停顿时间或最大吞吐量。如果用户对垃圾收集器运作不太了解,手工优化存在困难,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成是一个不错的选择。
Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,同样是单线程收集器,但是采用的是标记-整理算法。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,是多线程收集器,标记-整理算法。在注重吞吐量和CPU资源的场景,都可以使用Parallel Scavenge收集器和Parallel Old收集器。
CMS收集器
CMS收集器是一种以获取最短停顿时间为目标的垃圾收集器。是真正意义上的并发收集器,他“基本上”实现了垃圾回收线程和用户线程同时工作。基于标记-清除算法实现。
整个过程分为四个阶段:
- 初始标记:暂停用户线程,标记与GC roots直接相连的对象,速度很快。
- 并发标记:用户程序和垃圾回收线程同时开启,通过第一个阶段标记出来的对象,继续遍历,标记出与其直接或间接相连的对象。因为用户线程和垃圾回收线程同时进行,所以可能会产生新的对象或对象关系发生变化。
- 重新标记:重新标记阶段是为了修正并发标记阶段因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。这一阶段的时间会比初始阶段长,但远远比并发阶段短。
- 并发清除:开启用户线程,清除已死亡的对象。
优点:并发收集,低停顿。
缺点:对CPU资源敏感,无法处理浮动垃圾,使用的标记-清除算法会产生碎片化内存。
G1收集器
从JDK1.7诞生,JDK1.9之后成为默认垃圾收集器。
G1(Garbage-First)收集器是一款面向服务器的垃圾收集器,主要针对具有多颗处理器和大量内存的机器,以及高概率满足GC停顿时间的同时,还具备高吞吐量的特征。
G1将内存划分为一个个大小相等区域(Region),一般情况下是2048个。依然保有伊甸区,幸存区,老年代的概念,但是物理上他们不是连续的空间(这个区域被新生代使用,那么它就属于新生代,当它被回收时,又被老年代使用,那么他就属于老年代,所以无论是新生代还是老年代,空间大小都不是固定的)。
Region的类型:
- 三种常见:伊甸区,幸存区,老年代
- 巨无霸区:保存比标准区域大50%以上的对象,存储在一组连续的区域,转移会影响GC效率。标记阶段发现对象不是存活的直接进行回收。
- 未使用区:未被使用的区域。
收集整体是“标记-整理算法”。
Region之间基于复制算法。
G1的运行过程和CMS大致相同:
- 初始标记:标记GC Roots能直接到达的对象,这个阶段需要暂停用户线程,但时间很短,而且是在进行Minor GC同步进行的,所以实际上G1没有产生额外的停顿。
- 并发标记:从第一个阶段找出来的对象开始进行可达性分析,找出要回收的对象。这个阶段用户线程继续执行。
- 最终标记:暂停用户线程,处理并发标记阶段漏标的对象。
- 筛选回收:对各个Region的回收价值和成本进行排序。根据用户所期望的停顿时间指定回收计划,可有自由选择任意多个Region构成回收集,然后把决定回收的那一部分区域中的存活对象复制到空的Region,再清理掉整个旧Region的空间。暂停用户进程,多个线程进行垃圾收集。