JVM垃圾回收,介绍JVM的GC垃圾回收机制,包括对象可达性分析、GC算法、GC垃圾回收器等。
JVM内存分配与回收
Java堆是垃圾收集器管理的主要区域,因此也被称为GC堆,从垃圾回收的角度来说,现在收集器基本都采用 分代垃圾收集算法 ,所有Java堆还可以细分为: 新生代和 老年代 ,新生代还可以细分为:Ed en区、SurvivorFrom区、SurvivorTo区。进一步划分的目的是为了更好的回收内存,或者更好的分配内存。
年轻代MinorGC过程
年轻代MinorGC过程:复制–>清空–>互换
Eden、SurvivorFrom复制到SurvivorTo,年龄+1
大部分情况,对象都会首先在Eden区域分配,当Eden区满的时候,会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候,会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次的回收还存活的对象,则直接复制到To区(如果有对象的年龄到带了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1
清空Eden、SurvivorFrom
然后清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是To
SurvivorTo和SurvivorFrom互换
最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC的SurvivorFrom区,部分对象会在From和To区域复制来复制去,如此交换15次(由jvm参数MaxTenuringTreshold决定),最终如果还是存活,就进入老年代
对象在Eden区分配
大多数情况下,对象在新生代中Eden区分配,当Eden区没有足够空间分配时,虚拟机会发起一次Minor GC。
Minor GC和Full GC不同:
- Minor GC:指发生新生代的垃圾收集动作,Minor GC非常频繁,回收速度也一般比较快。
- Major GC/Full GC:指发生在老年代的GC,出现Full GC经常会伴随至少一次的Minor GC,Full GC的速度一般会比Minor GC的慢10倍以上。
大对象直接进入老年代
大对象需要大量连续的内存空间,如字符串数组等,为了避免为大对象分配内存时带来复制降低效率,所以直接将大对象分配至老年代。
长期存活的对象进入老年代
虚拟机采用分代收集的思想管理内存,虚拟机给每个对象一个年龄计数器。
如果对象在Eden出生并经过第一次Minor GC后仍能存活,在复制交换后,并将对象年龄设为1,对象没熬过一次Minor GC,年龄就增加1岁,当年龄增加到一定程度(默认15,可-XX:MaxTenuringTreshold=15 来设置),就会被晋升到老年代。
对象死亡
堆中几乎存放着所有对象的实例,对堆垃圾回收前,第一步就要判断哪些对象已经死亡可以被回收。
引用计数法
给对象添加一个引用计数器,每当有一个地方引用它,计数器就加1,当引用失效,计数器就减1;当计数器为0时,对象就被判定为死亡。
这个方法实现简单、高效,但是主流的虚拟机都没有选择这个算法,原因是它很难解决对象之间的循环引用问题。并且每次对对象操作,都要维护计数器,且计数器本身也有一定的消耗。
可达性分析算法
这个算法基本思想是通过一系列名为GC ROOTS的对象作为起始点,从这个对象开始向下搜索,如果一个对象到GC ROOTS没有任何引用链相连时,则说明此对象不可用,也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可达的)对象就被判定为存活;没有遍历到的判定为死亡。
那些对象可作为GC ROOTS:
- 虚拟机栈(栈帧的局部变量区,也叫局部变量表)中引用的对象
- 方法去中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中(Native方法)引用的对象
引用
无论通过引用计数法还是通过可达性分析来判定对象的引用链是否可达,判定对象的存活都与引用有关。
Java中引用的定义:如果reference类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用,引用分为:强引用、软引用、弱引用、虚引用四种。
强引用(StrongReference)
我们大部分引用都是强引用,垃圾回收器绝不会回收它,当内存空间不足,JVM宁愿抛出OutOfMemory错误,使进程异常终止,也不会回收强引用对象。
软引用(SoftReference)
如果JVM内存空间足够时,垃圾回收器不会回收它,如果内存空间不足了,就会回收这些对象的内存,只要垃圾回收器没有回收它,该对象就可与你被程序使用,软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列联合使用,如果软引用所引用的对象被回收,JVM就会把这个引用加入到与之关联的引用队列中。
弱引用(weakReference)
弱引用与软引用的区别在于:指具有弱引用的对象拥有更短的生命周期,在垃圾回收线程扫描它的内存区域时,一旦发现只具有弱引用的对象,不管内存够不够,都会回收它的内存。
虚引用(PhantomReference)
虚引用和没有引用一样,在任何时候都可能被垃圾回收,虚引用主要用于追踪对象被垃圾回收的活动。
虚引用与软引用的区别:虚引用必须和引用队列联合使用,当垃圾回收器准备回收一个对象时,如果发现有虚引用,就会在回收对象内存之前,把这个虚引用加入到与之关联的引用队列中,程序可以通过判定引用队列中是否已经加入了虚引用,来了解被引用的对象是否要被垃圾回收。
程序中很少使用软引用与虚引用,使用软引用的情况较多,软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出OutOfMemory等问题
不可达对象
可达性分析中的不可达对象,也并非非死不可,这时候它们暂时处于缓刑阶段,至少经历两次标记过程,不可达对象第一次标记并且进行一次筛选,筛选对象是否有必要执行finalize方法,当对象没有覆盖finalize方法,或者finalize方法已经被虚拟机调用过时,虚拟机将这两种情况被视为没有必要执行。
经过两次标记后,除非再次加入引用链,否则下次扫描时就会被回收。
废弃常量
字符串常量,如果当前没有任何String对象引用该字符串常量的话,说明该常量时废弃常量,在发生内存回收时,该常量就会被清理出常量池。
Jdk1.7之后的版本,JVM将运行时常量池从方法区中移了出来,在JAVA堆中开辟了一块区域存放运行时常量池。
类回收
方法区主要回收的使无用的类,回收的条件:
- 该类所有实例都被回收,也就是java堆不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
GC垃圾回收算法
标记清除算法
该算法分为“标记”、“清除”两个阶段:首先标记出所需要回收的对象,在标记完成后统一回收这些对象。它是最基础的收集算法,后续的算法都是对其不足的改进。
这种算法的不足有:
- 效率问题
- 空间问题,标记清除后会产生大量的不连续的碎片
标记整理算法
标记整理算法是在标记清除的基础上做的改进,即在标记清除后,不是直接回收对象,而是让所有存活对象向一端移动,然后直接清理掉除存活对象以外的内存。
标记整理的缺点是,移动对象需要成本。
复制算法
为了解决效率问题,“复制”收集算法出现了,它可以将内存分为大小相同的两块,每次使用其中的一块,当这一块内存使用完成后,就将还存活的对象复制到另一块区,然后再把使用的空间清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
新生代的Minor GC就是使用的复制算法。
分代收集算法
当前虚拟即的垃圾收集都是采用分代收集算法,这种算法是根据对象的存活周期的不同将内存分成几块,一般将java堆分成新生代、老年代。这样我们就可以根据各个年代的特点选择合适的来记收集算法。
在新生代,每次手机都会有大量的对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可一完成每次垃圾收集。而老年代的对象存活几率比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择标记清除或者标记整理算法进行垃圾收集。
GC垃圾回收器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。没有十分完美的收集器,都有其优点和缺点,我们要根据具体的场景选择适合自己的垃圾收集器。
Serial收集器
Serial串行收集器是最基本、历史最悠久的垃圾收集器了,它是一个单线程收集器,它只会使用一条垃圾收集线程去完成垃圾收集工作,它在进行垃圾收集工作的时候必须暂停所有的JVM工作线程,知道它收集完成。
回收算法:新生代采用复制算法,老年代采用标记整理算法。
它的缺点就是收集时暂停所有的线程,优点是简单高效,Serial收集器没有线程交互的开销,自然获得很高的单线程收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是个不错的选择。
ParNew收集器
ParNew并行收集器,就是Serial收集器的多线程版本,处了使用多线程进行收集外,其余行为,控制参数、收集算法、回收策略等和Serial收集器完全一样。
回收算法:新生代采用复制算法,老年代采用标记整理算法。
它是许多运行在Server模式下的虚拟机的首要选择,处了Serial收集器外,只有它能与CMS并发收集器配合工作。
- 并行:指多条垃圾收集线程并行工作,但此时用户的线程仍处于等待状态
- 并发:指用户线程与垃圾收集器线程同时执行,但不一定是并行,可能交替执行,用户程序在继续运行,而垃圾收集器运行在另一个CPU上。
Parallel Scavenge 收集器
Parallel Scavenge 收集器也是使用复制算法的多线程收集器。
Parallel Scavenge 收集器关注点是吞吐量(高效利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓的吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值,Parallel Scavenge 收集器提供了用户找到最合适的停顿时间或最大的吞吐量。
回收算法:新生代采用复制算法,老年代采用标记整理算法。
Serial Old收集器
Serial收集器的老年代版本,它同样是一个单线程收集器,主要用途:一是在JDK1.5之前与Parallel Scavenge收集器搭配使用,二是用作CMS收集器的后备方案。
Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,非常注重用户体验,是HotSpot虚拟机上第一款并发收集器,实现了垃圾收集器线程与用户线程基本上同时工作。
回收算法:新生代采用复制算法,老年代采用标记清除算法。
老年代的标记清除算法更加复杂,整个过程分为四个步骤:
- 初始标记:暂停所有的其他线程,标记一下GC Roots相连的对象速度很快。
- 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象,这个阶段接受,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能不断的更新引用,所以GC线程无法保证可达性分析的实时性,所以这个算法会跟踪记录这些发生引用更新的地方。
- 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记时间短。
- 并发清除:开启用户线程同时GC线程开始对标记的区域做清扫。
优点:并发收集、低停顿
缺点:CPU资源敏感;无法处理浮动垃圾;标记清除会导致大量空间碎片
G1收集器
G1收集器是一款面向服务器的垃圾收集器,抓哟针对配备多核处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
特点:
- 并行和并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU来缩短暂停线程的时间,部分其他收集器原本需要停顿java线程执行的GC操作,G1收集器仍然可以通过并发的方式让java程序继续执行。
- 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
- 空间整合:与CMS的标记整理算法不同,G1从整理来看是基于标记整理算法实现的收集器,从局部来看是基于复制的算法实现的。
- 可预测的停顿:这是G1相对于CMS的另一优势,降低停顿时间是G1和CMS共同关注点,但G1除了追求停顿外,还能简历可预测的停顿时间模型,能让使用者明确停顿指定在一段时间内。
G1收集器运作的步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的区域,这种使用区域划分内存空间以及优先级区域的回收方式,保证了G1收集器在有限的时间内可以尽可能高的收集效率。