目录
一、垃圾收集(Garbage Collection,GC)
(一)对象引用
JDK1.2以前,Java中引用的定义为:如果referencce类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。
- 强引用:是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用:用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
- 弱引用:是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
- 虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。
(二)对象存活判定
对象存活判定与是否有“引用”有关,目前判定对象是否存活的主流算法有两种:
- 引用计数法:给对象添加一个引用计数器,每当有一个地方引用它时,计数值就加1;当引用失效时,计数值就减一;任何时刻计数器为0的对象就不可能再被使用的。FlashPlayer、Python、Squirrel等使用了引用计数法进行内存管理。
- 可达性分析:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下探索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
Java、C#使用可达性分析进行进行内存管理,在Java语言中,GC Roots的对象有以下几种:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。
(三)对象回收
对象的被回收至少要经历两次标记过程:如果对象在进行可达性分析无引用,那它将会被第一次标记并且进行一次是否有必要执行finalize()方法的判定。当对象没有覆盖finalize()方法或者finalize()已被调用过,虚拟机将这两种情况视为“没有必要执行”。如果这个对象被判定为需要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动创建的、低优先级的Finalizer线程去执行它。虚拟机不承诺会等待它运行结束,如果其中的某一个对象的finalize()方法执行缓慢甚至进入死循环,那么F-Queue队列将进入永久等待。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己,只要将对象重新与引用链上的任何一个对象建立关联即可,如把this赋值给某个类变量或者对象的成员变量。finalize()方法能做的所有工作通过使用try-finally或者其他方式都可以做的更好、更及时,所以对finalize()的建议为不使用。
(四)垃圾收集算法
1.标记-清除算法(Mark-Sweep)
首先标记出所有需要回收的对象,在标记完成后统一清除所有被标记的对象。缺点:标记和清除两个过程效率不高、清除后会产生大量不连续的内存碎片。
2.复制算法(Copying)
将内存分成容量大小相等的两块,当一块用完时,将还存活的对象全部复制到另一块内存上。缺点:内存缩小为原来的一半。IBM研究表明,新生代的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和一块Survivor空间。当回收时将Eden和Survivor中还存活的对象一次复制到另外一块未使用的Survivor空间上,最后清理掉Eden和原来用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,空间利用率为90%(80(Eden) + 10(Survivor *1) = 90)。这只是理想状态,如果Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。
3.标记-整理算法(Mark-Compact)
标记过程仍然与”标记-清除算法“一样,后续是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4.分代收集算法(Generational Collection)
根据对象的存活周期的不同将内存划分为几块。Java一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。新生代:每次收集时都会有大量对象死去,那么就选用复制算法,只需要付出少量对象的复制成本就可以完成收集。老年代:由于对象存活率高、没有额外空间对它进行分配担保,就必须使用”标记-清理“或者”标记-整理“算法来进行回收。
(五)HotSpot的算法实现
1.枚举根节点
GC Roots的节点主要在全局性的引用(如方法区的常量和静态属性)与执行上下文(虚拟机栈的栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,逐个检查需要消耗大量时间。另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个确保一致性的快照中进行,这导致DC进行时必须停顿所有Java执行线程(Stop The World)。分析过程分为两种:
保守式GC:把不能识别指针还是非指针的对象当做指针来保守处理,也就是当成活动对象保留下来。
准确式GC:能准确识别出内存中某位置数据的类型什么(如内存中有个32位整数123456,它到底是指针类型指向123456的内存地址还是一个32位的整数123456,虚拟机能够准确分辨出来)。
目前主流Java虚拟机使用的都是准确式GC,HotSpot的实现是使用一组称为OopMap(用来保存对象类型的映射表,区分是否指针)的数据结构来达到这个目的的,在类加载完成的时候,HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。
2.安全点(SafePoint)
安全Stop The World的时间点,选定标准是”是否具有让程序长时间执行特征“。”长时间执行“最明显的特征就是指令序列复用,例如方法调用、循环跳转、异常跳转,具有这些功能的指令才会产生SafePoint。如何在GC发生时都”跑“到最近的安全点上在停顿下来。有两种方案:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension),其中抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它”跑“到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。主动中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。
3.安全区域(SafeRegion)
由于程序会有”不执行“的时候,这时就无法响应安全点的中断,如线程处于Sleep状态或者Block状态,这时需要安全区域来解决。安全区域是指在一段代码片段中,引用关系不会发生变化,在这个区域任意地方GC都是安全的。当线程执行到SafeRegion中的代码时,首先标识自己进入了SafeRegion,在线程要离开SafeRegion时,需要检查系统是否已完成了枚举根节点,如果未完成线程需要等待直到收到可以安全离开SafeRegion的信号为止。
(六)Java垃圾收集器总结
引用地址:https://blog.csdn.net/CrankZ/article/details/86009279
收集器 串行、并行or并发 新生代/老年代 算法 目标 适用场景 Serial 串行 新生代 复制 响应速度优先 单CPU环境下的Client模式 Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后备预案 ParNew 并行 新生代 复制 响应速度优先 多CPU环境时在Server模式下与CMS配合 Parallel Scavenge
并行 新生代 复制 吞吐量优先 在后台运算而不需要太多交互的任务 Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务 CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网站或B/S系统服务端上的Java应用 G1 并发 Java堆 标记-整理+复制算法 响应速度优先 面向服务端应用,将来替换CMS
1.串行、并行or并发
- 串行:指单条垃圾收集线程工作,此时用户线程处于等待状态。
- 并行:指多条垃圾收集线程工作,此时用户线程仍处于等待状态。
- 并发:指用户线程与垃圾收集线程同时执行(但不一定并行,可能是交替执行)。
2.Minor GC、Major GC和Full GC
- Minor GC:指发生在新生代的垃圾回收动作,因为Java大多数对象都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- Major GC:指发生在老年代的GC,一般比Minor GC慢10倍以上。
- Full GC:新生代和老年代GC。
3.响应速度与吞吐量
- 响应速度:垃圾回收的停顿时间越短,响应速度就越快。
- 吞吐量:吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
4.Parallel Scavenge
Parallel Scavenge收集器它提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数(大于0的毫秒数)以及直接控制吞吐量大小的-XX:GCTimeRatio(垃圾收集时间占总时间的比率,吞吐量倒数(大于0小于100的整数)。例:默认99,即1/(1+99) = 1%,允许最大1%的垃圾收集时间)参数。
Parallel Scavenge收集器带有GC自适应调节策略,设置-XX:+UseAdaptiveSizePolizy为true,只需把基本的内存数据配置好,然后指定-XX:MaxGCPauseMillis或者-XX:GCTimeRatio给虚拟机设立一个优化目标,具体的细节参数的调节工作就由虚拟机完成了。
5.CMS
CMS收集器的运作过程分为4个步骤:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标识期间因用户程序继续运作而导致标记产生变动的那一部分对象的标识记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。由于整个过程中耗时最长的并发标记和并发清除线程可与用户线程一起工作,所以整体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器的并发清除线程与用户线程并发执行,伴随着程序运行会产生新的垃圾,这部分垃圾未进行标记,CMS无法在当次收集中处理掉它们,只要留待下一次GC时在清理掉,这部分垃圾就称为“浮动垃圾”。由于并发清除阶段会产生浮动垃圾,CMS收集器不能像其他收集器那样等到老年代几乎完全填满了在进行收集,需要预留一部分空间提供给并发清除时的程序运作使用。
-XX:CMSInitiatingOccupancyFration参数(CMS收集器的启动阈值),设置太高容易导致大量“Concurrent Mode Failure”失败,性能下降。基本满足公式:
CMS是基于“标记-清除”算法实现的,会产生空间碎片,一旦空间碎片过多,分配大对象时可能会导致Full GC。
- -XX:+UseCMScompactAtFullCollection开关(默认开启),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理。
- -XX:CMSFullGCsBeforeCompaction参数,这个参数是用于设置执行多少次不压缩的Full GC后跟着来一次压缩的(默认是0,每次都压缩)。
6.G1
G1垃圾收集器主要是为那些拥有大内存的多核处理器而设计的,在jdk11中,G1已经取代了CMS,是默认的垃圾收集器。它在以很高的概率满足垃圾收集的停顿时间的要求同时还可以达到很高的吞吐量,同时几乎不需要做什么配置。G1的目标是为应用提供停顿时间和吞吐量的最佳平衡,它的主要特性包含:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短停顿时间。
- 分代收集:分代概念保留。
- 空间整合:基于“标记整理”算法,不存在大量的空间碎片。
- 可预测的停顿:建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
后续...
(七)内存分配
1.Java堆内存模型
Java堆被划分成两个不同的区域:新生代和老年代。默认的,新生代与老年代的比例的值为 1:2(-XX:NewRatio)。
- 新生代:新生代在每次垃圾收集时都发现有大批对象死去,只有少量存活,所以选用复制算法进行回收,只需要付出少量存活对象的复制成本。新生代被划分为三块:Eden、Survivor From、Survivor To,默认Eden : From : To= 8 : 1 : 1(-XX:SurvivorRatio)。
- 老年代:老年代对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”来进行回收。
2.对象优先在Eden分配
对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden上,如果启动了本地线程分配缓冲,将线程优先在TLAB上分配。少数情况也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的哪一种垃圾收集器组合,还有虚拟机中与内存相关参数的设置。 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
/**
* VM options:-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
*/
public class TestAllocation {
private static final int ONE_MB = 1024 * 1024;
private static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * ONE_MB];
allocation2 = new byte[2 * ONE_MB];
allocation3 = new byte[2 * ONE_MB];
allocation4 = new byte[4 * ONE_MB]; //出现一次Minor GC
}
public static void main(String[] args) {
testAllocation();
}
}
Java8收集器使用的是Parallel Scavenge和Parallel Old。Java8移除了持久代PermGen,替换为元空间Metaspace。
3.长期存活的对象将进入老年代
当对象在 Eden(包括一个Survivor区域,这里假设是From区域)出生后,在经过一次Minor GC后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳 ,则使用复制算法将这些仍然还存活的对象复制到另外一块Survivor区域中,然后清理所使用过的Eden以及 Survivor区域,并且将这些对象的年龄设置为1,以后对象在Survivor区每熬过一次 Minor GC,就将对象的年龄+1,当对象的年龄达到某个值时(默认是 15 岁,可以通过参数-XX:MaxTenuringThreshold来设定),这些对象就会成为老年代。但这也不是一定的,对于一些较大的对象(如数组,即需要分配一块较大的连续内存空间)则是直接进入到老年代。
4.动态年龄判断
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold所规定的年龄。
5.空间分配担保
在发生Minor GC之前,虚拟机会去确认老年代剩余连续空间是否大于新生代所有对象的总空间,如果条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFaiure设置是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFaiure设置不允许冒险,这时也要改为进行一次Full GC。
Minor GC的风险就是老生代最大可用连续空间大于历次晋升老年代对象的平均大小,但是本次新生代存活数量大于老生代最大可用连续空间,那么还是要去进行一次Full GC。