JVM(二)垃圾回收

文章目录

本系列文章:
  JVM(一)Java运行时区域、对象的使用
  JVM(二)垃圾回收
  JVM(三)类文件结构、类加载机制
  JVM(四)JVM调试命令、JVM参数
  JVM(五)JVM调优

前言 内存分配与回收

  Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java自动内存管理最核心的功能是堆内存中对象的分配与回收。
  Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代。再细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
  堆空间的基本结构:

  上图所示的eden区、s0(“From”)区、s1(“To”)区都属于新生代,tentired区属于老年代。大部分情况,对象都会首先在Eden区域分配。在一次新生代垃圾回收后,如果对象还存活,则会进入s1(“To”),并且对象的年龄还会加1(Eden 区->Survivor 区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。
  对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。经过一次Minor GC后,Eden区和"From"区已经被清空。这个时候,“From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。

  对象的内存分配通常是在Java堆上分配,对象主要分配在新生代的Eden区,如果启动了本地线程缓冲,将按照线程优先在TLAB上分配。少数情况下也会直接在老年代上分配。

  总的来说分配规则不是百分百固定的,其细节取决于哪一种垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵循以下几种"通用"规则:

  • 1、对象优先在Eden区分配
      多数情况,对象都在新生代Eden区分配。当Eden区分配没有足够的空间进行分配时,虚拟机将会发起一次Minor GC。如果本次GC后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。

  Minor GC:指发生在新生代的GC,因为Java对象大多都是朝生夕死,所有Minor GC非常频繁,一般回收速度也非常快;
  Major GC/Full GC:是指发生在老年代的GC,出现了Major GC通常会伴随至少一次Minor GC。Major GC的速度通常会比Minor GC慢10倍以上。

  • 2、大对象直接进入老年代
      所谓大对象是指需要大量连续内存空间的对象(比如:字符串、数组),频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发GC以获取足够的连续空间来安置新对象。
      前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致Eden区和两个Survivor区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
  • 3、长期存活对象将进入老年代
      虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器。
      如果对象在Eden区出生,并且能够被 Survivor容纳,将被移动到 Survivor 空间中,这时设置对象年龄为1。对象在Survivor区中每熬过一次Minor GC年龄就加1,当年龄达到一定程度(默认15) 就会被晋升到老年代。使用-XXMaxTenuringThreshold设置新生代的最大年龄,只要超过该参数的新生代对象都会被转移到老年代中去。
  • 4、动态对象年龄判定
      大部分情况,对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入s0或者s1,并且对象的年龄还会加1(Eden区 -> Survivor区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。

  修正:“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。

  默认晋升年龄并不都是15,这个是要区分垃圾收集器的,比如CMS就是6。

  • 5、空间分配担保
      在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那此时也要改为进行一次Full GC。
      冒险是指当出现大量对象在Minor GC后仍然存活的情况,就需要老年代进行分配担保,把Survivor区无法容纳的对象直接进入老年代。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共会有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之间每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
      取平均值进行比较其实仍然是一种动态概率的手段,依然存在担保失败的情况。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。

一、如何判断对象是否已死

  堆内存是由存活和死亡的对象组成的。垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些对象是"存活"的,哪些对象已经"死掉"的。
  存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。

1.1 两种判断对象已死的方法

1.1.1 引用计数法*

  Java堆中每个具体对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1。当引用失效时,即一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。

  • 引用计数法的优点
      引用计数收集器执行简单,判定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。
  • 引用计数法的缺点
      难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以主流的虚拟机并没有选择这种算法进行垃圾回收。
      所谓对象之间的相互引用问题,如下面代码所示:除了对象objA和objB相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知GC回收器回收他们。
public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
    }
}
1.1.2 可达性分析算法*

  可达性分析算法又叫根搜索算法,该算法的基本思想就是通过一系列称为GC Roots的对象作为起始点,从这些起始点开始往下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots对象之间没有任何引用链的时候(不可达),证明该对象是不可用的,于是就会被判定为可回收对象
  在下图中, Object5、Object6、Object7虽然互有关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象。

  在Java中可作为GC Roots的对象包含以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  2. 方法区中类静态变量引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中JNI(Native 方法)引用的对象。

1.2 两次标记

  在可达性分析算法中不可达的对象,也并非是“非死不可”的。这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程(即一个对象是否应该在垃圾回收器在GC时回收,至少要经历两次标记过程)。

  • 第一次标记
      如果对象在进行可达性分析后被判定为不可达对象,那么它将被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法。对象没有覆盖finalize()方法或者该对象的finalize()方法曾经被虚拟机调用过,则判定为没必要执行。
  • finalize()第二次标记
      如果被判定为有必要执行finalize()方法,那么这个对象会被放置到一个F-Queue队列中,并在稍后由虚拟机自动创建的、低优先级的Finalizer线程去执行该对象的finalize()方法。但是虚拟机并不承诺会等待该方法结束,这样做是因为,如果一个对象的finalize()方法比较耗时或者发生了死循环,就可能导致F-Queue队列中的其他对象永远处于等待状态,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,如果对象要在finalize()中挽救自己,只要重新与GC Roots引用链关联上就可以了。这样在第二次标记时它将被移除“即将回收”的集合,如果对象在这个时候还没有逃脱,那么它基本上就真的被回收了。
      示例:
/**
*1.对象可以在被GC时自我拯救。
*2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
*/
public class FinalizeEscapeGC{
	public static FinalizeEscapeGC SAVE_HOOK=null;
	public void isAlive(){
		System.out.println("yes,i am still alive");
	}
	@Override
	protected void finalize()throws Throwable{
		super.finalize();
		System.out.println("finalize mehtod executed!");
		FinalizeEscapeGC.SAVE_HOOK=this;
	}
	public static void main(String[]args)throws Throwable{
		SAVE_HOOK=new FinalizeEscapeGC();
		//对象第一次成功拯救自己
		SAVE_HOOK=null;
		System.gc();
		//因为finalize方法优先级很低,所以暂停0.5秒以等待它
		Thread.sleep(500);
		if(SAVE_HOOK != null){
			SAVE_HOOK.isAlive();
		}else{
			System.out.println("no,i am dead 1");
		}
		//下面这段代码与上面的完全相同,但是这次自救却失败了
		SAVE_HOOK=null;
		System.gc();
		//因为finalize方法优先级很低,所以暂停0.5秒以等待它
		Thread.sleep(500);
		if(SAVE_HOOK != null){
			SAVE_HOOK.isAlive();
		}else{
			System.out.println("no,i am dead 2");
		}
	}
}

  测试结果:

finalize mehtod executed!
yes,i am still alive
no,i am dead 2

1.3 回收方法区

  前面介绍过,方法区在HotSpot虚拟机中被划分为永久代。在Java虚拟机规范中没有要求方法区实现垃圾收集,而且方法区垃圾收集的性价比也很低。方法区(永久代)的垃圾收集主要回收两部分内容:废弃常量和无用的类。

  • 回收废弃常量
      运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?假如在常量池中存在字符串"abc",如果当前没有任何String对象引用该字符串常量的话,就说明常量"abc"就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc"就会被系统清理出常量池。
  • 回收无用的类
      方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是 “无用的类” :
  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

  虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。

二、垃圾收集算法

  • Card Table
      由于YGC时,需要扫描整个OLD区(因为需要找从Old区指向Eden区的对象),效率非常低,所以JVM设计了CardTable, 如果一个OLD区CardTable中有对象指向Y区,就将它设为Dirty,下次扫描时,只需要扫描Dirty Card。
      在结构上,Card Table用BitMap来实现。

  由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。垃圾回收有两种类型:Minor GC和Full GC。

  • 1、Minor GC
      新生代垃圾收集。对新生代进行回收,不会影响到年老代。因为新生代的Java对象大多死亡频繁,所以Minor GC非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。
      Minor GC产生的原因是新生代空间不够用。
  • 2、Full GC
      也叫Major GC,对整个堆进行回收,包括新生代和老年代(JDK8 取消永久代)。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。它的收集频率较低,耗时较长。

2.1 标记-清除算法*

  标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段:

  1. 标记阶段:标记出可以回收的对象。
  2. 清除阶段:回收被标记的对象所占用的空间。

  标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。

  • 标记-清除算法的优点
  1. 实现简单,不需要对象进行移动;
  2. 存活对象较多的时候效率比较高。
  • 标记-清除算法的缺点
  1. 标记、清除过程效率低;
  2. 产生大量不连续的内存碎片

  标记-清除算法的执行的过程示例:

2.2 复制算法*

  为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收

  • 复制算法的优点
  1. 按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片;
  2. 适合于对象较少的情况。
  • 复制算法的缺点
      可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

  复制算法的执行过程示例:

2.3 标记-整理算法*

  在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-清除算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。
  标记-整理算法的优点:解决了标记-清理算法存在的内存碎片问题。
  标记-整理算法的缺点:仍需要进行局部对象移动,一定程度上降低了效率。
  标记-整理算法的执行过程示例:

2.4 分代收集算法*

  当前商业虚拟机都采用分代收集的垃圾收集算法。除Epsilon、ZGC、Shenandoah之外的GC都是使用逻辑分代模型。
  分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块,示例:

  新生代和老年代的比例是1:2,Eden、S1和S2的比例是8:1:1,这种关系都是默认(JDK1.8)的,可以改变。该比例对应的JVM参数是:-XX:SurvivorRatio,它定义了新生代中Eden区域和Survivor区域(From幸存区或To幸存区)的比例,默认为8,也就是说Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10。
  在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

  • 1、新生代(Young generation)
      绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为 minor GC
      新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。
      新生代中存在一个Eden区和两个Survivor区。新对象会首先分配在Eden中(如果新对象过大,会直接分配在老年代中)。在YGC(minor GC)中,Eden中的对象会被移动到Survivor中,正常情况下,会持续在S1和S2区域之间进行移动,直至对象满足一定的年纪(定义为熬过GC的次数),会被移动到老年代。
      Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

ServivorFrom:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
ServivorTo:保留了一次 MinorGC 过程中的幸存者。

  可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数 -XX:NewRatio 设置老年代与新生代的比例。例如 -XX:NewRatio=8指定老年代/新生代为8/1。老年代占堆大小的7/8 ,新生代占堆大小的1/8(默认即是1/8)。例如:

	-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8
  • 2、老年代(Old generation)
      对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。因此,可以认为年老代中存放的都是一些生命周期较长的对象。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代要少得多。对象从老年代中消失的过程,可以称之为major GC(或者full GC)。
  • 3、永久代(permanent generation)
      像一些类的层级信息,方法数据 和方法信息(如字节码,栈 和 变量大小),运行时常量池(JDK7之后移出永久代),已确定的符号引用和虚方法表等等。它们几乎都是静态的并且很少被卸载和回收,在JDK8之前的HotSpot虚拟机中,类的这些永久的 数据存放在一个叫做永久代的区域。
      永久代是一段连续的内存空间,我们在JVM启动之前可以通过设置-XX:MaxPermSize的值来控制永久代的大小。但是JDK8之后取消了永久代,这些元数据被移到了一个与堆不相连的称为元空间 (Metaspace) 的本地内存区域

-XX:MetaspaceSize:分配给类元数据空间(以字节计)的初始大小。
-XX:MaxMetaspaceSize:分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。

  当执行一次Minor GC时,Eden空间的存活对象会被复制到To Survivor空间,并且之前经过一次Minor GC并在From Survivor空间存活的仍年轻的对象也会复制到To Survivor空间
  有两种情况Eden空间和From Survivor空间存活的对象不会复制到To Survivor空间,而是晋升到老年代:
  1)一种是存活的对象的分代年龄超过-XX:MaxTenuringThreshold(用于控制对象经历多少次Minor GC才晋升到老年代)所指定的阈值。该参数表示的是对象在S1和S2经过多少次调整后进入老年代。在不同的GC有不同的默认值:

  2)动态年龄。

-XX:TargetSurvivorRatio 目标存活率,默认为50%

  目标存活率的使用过程:

  1. 通过这个比率来计算一个期望值,desired_survivor_size 。
  2. 然后用一个total计数器,累加每个年龄段对象大小的总和。
  3. 当total大于desired_survivor_size 停止。
  4. 然后用当前age和MaxTenuringThreshold 对比找出最小值作为结果

  总体特征就是,年龄从小到大进行累加,当加入某个年龄段后,累加和超过survivor区域*TargetSurvivorRatio的时候,就从这个年龄段网上的年龄的对象进行晋升。例如:年龄1的占用了33%,年龄2的占用了33%,累加和超过默认的TargetSurvivorRatio(50%),年龄2和年龄3的对象都要晋升。

  当所有存活的对象被复制到To Survivor空间,或者晋升到老年代,也就意味着Eden空间和From Survivor空间剩下的都是可回收对象,示例:

  这时GC执行Minor GC,Eden空间和From Survivor空间都会被清空,而存活的对象都存放在To Survivor空间。
  接下来将From Survivor空间和To Survivor空间互换位置,也就是此前的From Survivor空间成为了现在的To Survivor空间,每次Survivor空间互换都要保证To Survivor空间是空的,这就是复制算法在新生代中的应用。在老年代则采用了标记-压缩算法。
  JDK8堆内存一般是划分为年轻代和老年代,不同年代 根据自身特性采用不同的垃圾收集算法。
  对于新生代,每次GC时都有大量的对象死亡,只有少量对象存活。考虑到复制成本低,适合采用复制算法。因此有了From Survivor和To Survivor区域。
  对于老年代,因为对象存活率高,没有额外的内存空间对它进行担保。因而适合采用标记-清理算法和标记-整理算法进行回收
【MinorGC过程(采用复制算法)】

  • 1)eden、servicorFrom复制到ServicorTo,年龄+1
      首先,把Eden和 ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区);
  • 2) 清空eden、servicorFrom
      然后,清空 Eden 和 ServicorFrom 中的对象;
  • 3) 清空ServicorTo和ServicorFrom互换
      最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。

【MajorGC过程(标记清除算法)】
  首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的
时候,就会抛出OOM异常。

2.5 复制算法、标记-整理算法和标记-清除算法的简单比较*

  效率:复制算法>标记-整理算法>标记-清除算法;
  内存整齐度:复制算法=标记-整理算法>标记-清除算法;
  内存利用率:标记-整理算法=标记-清除算法>复制算法。

三、垃圾收集器

3.1 垃圾回收器

  JDK1.8默认用的是Parallel Scavenge(新生代)+ Parallel Old(老年代)
  如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。在JVM中,具体实现有Serial、ParNew、Parallel Scavenge、CMS、Serial Old(MSC)、Parallel Old、G1等。

Epsilon调试使用,不用关注;ZGC、Shenandoah不再进行分代;前6个垃圾回收器逻辑和物理都分代;G1逻辑分代,物理不分代。

  在下图中,不同垃圾回收器适合于不同的内存区域,如果两个垃圾回收器之间存在连线,那么表示两者可以配合使用。

  如果当垃圾回收器 进行垃圾清理时,必须暂停其他所有的工作线程,直到它完全收集结束。我们称这种需要暂停工作线程才能进行清理的策略为Stop-the-World(STW)。Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old均采用的是STW的策略

  图中的前7种垃圾回收器,它们分别用于不同分代的垃圾回收:

  1. 新生代回收器:Serial、ParNew、Parallel Scavenge
  2. 老年代回收器:Serial Old、Parallel Old、CMS
  3. 整堆回收器:G1

  三种常用的新生代垃圾回收器(Serial、ParNew、Parallel Scavenge),都用的是复制算法。
  三种常用的老年代垃圾回收器和G1整堆回收器,除了CMS用的是标记-清除算法外,别的用的都是标记整理算法。
  两个垃圾回收器 之间有连线表示它们可以搭配使用,可选的搭配方案:

新生代老年代
SerialSerial Old
SerialCMS
ParNewSerial Old
ParNewCMS
Parallel ScavengeSerial Old
Parallel ScavengeParallel Old
G1G1

  Serial使用较少,因为随着内存越来越大,执行效率太慢。
  常用的组合为:Serial和SerialOld、ParNew和CMS、Parallel Scavenge和Parallel Old

  简单总结几种GC回收器的特点:

  • 1、Serial回收器
      复制算法,单线程,在GC时,STW(停止一切用户线程),作用于新生代,简单高效。
  • 2、Serial Old回收器
      标记-整理算法,单线程,作用于老年代,Serial收集器的老年代版本。
  • 3、ParNew回收器
      复制算法,Serial回收器的多线程版本,根据CPU核数,开启 不同的线程数(默认和CPU核数一致),作用于新生代。
  • 4、Parallel Scavenge回收器
      复制算法,吞吐量优先,作用于新生代。Parallel Scavenge收集器的老年代版本。

  吞吐量 = 用户线程时间/(用户线程时间+GC线程时间)。
  高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景。

  • 5、Parallel Old回收器
      标记-整理算法,吞吐量优先,作用于老年代。
  • 6、CMS回收器
      标记-清除,最短回收停顿时间为目标。
  • 7、G1回收器
      在大多数情况下可以实现指定的GC暂停时间,同时还能保证较高的吞吐量。
收集器串行、并行or并发新生代/老年代算法目标适用场景
Serial串行新生代复制算法响应速度优先单CPU环境下的Client模式
ParNew并行新生代复制算法响应速度优先多CPU环境时在Server模式下与CMS配合
Parallel Scavenge并行新生代复制算法吞吐量优先在后台运算而不需要太多交互的任务
Serial Old串行老年代标记-整理响应速度优先单CPU环境下的Client模式、CMS的后备预案
Parallel Old并行老年代标记-整理吞吐量优先在后台运算而不需要太多交互的任务
CMS并发老年代标记-清除响应速度优先集中在互联网站或B/S系统服务端上的Java应用
G1并发both标记-整理+复制算法响应速度优先面向服务端应用,将来替换CMS

3.2 垃圾回收器的分类

3.2.1 单线程垃圾回收器*
  • 1、Serial(-XX:+UseSerialGC)
      Serial(串行)回收器是最基本的新生代垃圾回收器,是单线程的垃圾回收器。由于垃圾清理时,Serial回收器不存在线程间的切换,因此,特别是在单CPU的环境下,垃圾清除效率比较高
      Serial是一类用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial进行垃圾收集时,不仅只用一条单线程执行垃圾收集工作,它还在收集的同时,所用的用户必须暂停(STW)。

  Serial回收器只开启一条GC线程进行垃圾回收,并且在垃圾收集过程中停止一切用户线程(Stop The World)。一般客户端应用所需内存较小,不会创建太多对象,而且堆内存不大,因此垃圾收集器回收时间短,即使在这段时间停止一切用户线程,也不会感觉明显卡顿。因此Serial垃圾收集器适合客户端使用。由于Serial收集器只使用一条GC线程,避免了线程切换的开销,从而简单高效。

  Serial回收器的优势:简单高效,由于采用的是单线程的方法,因此与其他类型的收集器相比,对单个CPU来说没有了上下文之间的的切换,效率比较高。
  Serial回收器的缺点:会在用户不知道的情况下停止所有工作线程,用户体验感极差,令人难以接受。
  Serial回收器的适用场景:单核服务器。对应到内存的话,是jvm管理内存不大的情况(十兆到百兆)。Serial收集器对于运行在Client模式下的虚拟机来说是个不错的选择。

  • 2、Serial Old(-XX:+UseSerialGC)
      Serial Old回收器是Serial回收器的 老生代版本,属于单线程回收器,它使用标记-整理算法。对于Server模式下的虚拟机,在JDK1.5以前,它常与Parallel Scavenge回收器配合使用,达到较好的吞吐量,另外它也是CMS回收器在Concurrent Mode Failure时的后备方案

  Concurrent Mode Failure,是CMS垃圾收集器特有的错误。该问题是在执行CMS GC的过程中同时业务线程将对象放入老年代,而此时老年代空间不足,或者在做Minor GC的时候,新生代Survivor空间放不下,需要放入老年代,而老年代也放不下而产生的。

  Serial回收器和Serial Old回收器的使用示例:

  适用场景:Client模式;单核服务器;与Parallel Scavenge收集器搭配;作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用

3.2.2 多线程垃圾回收器*
  • 1、ParNew(-XX:+UseParNewGC)
      ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
      ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。
      由多条GC线程并行地进行垃圾清理。但清理过程依然需要STW。ParNew追求“低停顿时间”,与Serial唯一区别就是使用了多线程进行垃圾收集,在多CPU环境下性能比Serial会有一定程度的提升。

      ParNew一般和CMS配合使用,是Parallel Scavenge的演进版本。除了Serial收集器外,只有ParNew能与CMS收集器(真正意义上的并发收集器)配合工作。
      ParNew是Serial的多线程版本。由多条GC线程并行地进行垃圾清理。但清理过程依然需要STW。ParNew追求“低停顿时间”,与Serial唯一区别就是使用了多线程进行垃圾收集,在多CPU环境下性能比Serial会有一定程度的提升;但线程切换需要额外的开销,因此在单CPU环境中表现不如Serial。
  • 2、Parallel Scavenge(-XX:+UseParallelGC)
      和ParNew回收一样,Parallel Scavenge回收器也是运行在新生代区域,属于多线程的回收器。也是采用复制算法
-XX:+UseParallelGC
 使用 Parallel 收集器+ 老年代串行
-XX:+UseParallelOldGC
 使用 Parallel 收集器+ 老年代并行

  Parallel Scavenge回收器更关心的是程序运行的吞吐量(高效率的利用 CPU)。即一段时间内,用户代码运行时间占总运行时间的百分比。

  吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)

  Parallel Scavenge和ParNew一样,都是多线程、新生代垃圾收集器。但是两者有巨大的不同点:Parallel Scavenge:追求CPU吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算。ParNew:追求降低用户停顿时间,适合交互式应用。

  Parallel Scavenge追求高吞吐量,可以通过减少GC执行实际工作的时间,然而,仅仅偶尔运行GC意味着每当GC运行时将有许多工作要做,因为在此期间积累在堆中的对象数量很高。单个GC需要花更多的时间来完成,从而导致更高的暂停时间。而考虑到低暂停时间,最好频繁运行GC以便更快速完成,反过来又导致吞吐量下降。
  Parallel Scavenge相关参数:

 -XX:GCTimeRadio
 设置垃圾回收时间占总CPU时间的百分比。
 -XX:MaxGCPauseMillis
 设置垃圾处理过程最久停顿时间。
 -XX:+UseAdaptiveSizePolicy
 开启自适应策略。我们只要设置好堆的大小和MaxGCPauseMillis或GCTimeRadio,收集器会自动
 调整新生代的大小、Eden和Survivor的比例、对象进入老年代的年龄,以最大程度上接近设置的
 MaxGCPauseMillis或GCTimeRadio。
  • 3、Parallel Old(-XX:+UseParallelOldGC)
      Parallel Old回收器是Parallel Scavenge回收器的老生代版本,属于多线程回收器,采用标记-整理算法。Parallel Old回收器和Parallel Scavenge回收器同样考虑了吞吐量优先这一指标,非常适合那些注重吞吐量和CPU资源敏感的场合。

3.3 特殊的垃圾回收器

  目前被广泛使用的垃圾回收器是 G1,通过很少的参数配置,内存即可高效回收。CMS 垃圾回收器已经在 Java 14 中被移除,由于它的 GC 时间不可控,有条件应该尽量避免使用。

3.3.1 CMS*

  Serial回收器诞生后,为了提高效率,诞生了PS;为了配合CMS,诞生了PN。CMS是1.4版本后期引入,CMS是里程碑式的GC,它开启了并发回收(并发垃圾回收是因为无法忍受STW)的过程,但是CMS毛病较多,因此目前没有任何一个JDK版本默认是CMS。

  CMS是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
  CMS(Concurrent Mark Sweep)是老年代GC回收器。是在最短回收停顿时间为前提的回收器,属于多线程回收器,采用标记-清除算法。
  CMS适用场景:GC过程短暂停,适合对时延要求较高的服务,用户线程不不允许长时间的停顿。

  CMS的四个阶段:

  • 1、初始标记(CMS initial mark)
      暂停所有的其他线程,并记录下直接与GC Roots相连的对象,速度很快。
  • 2、并发标记(CMS concurrent mark)
      从GC Roots开始对堆进行可达性分析,找出存活对象。

此回收器的大部分GC时间都浪费在这个阶段,但不产生STW,因为是并发的。

  • 3、重新标记(CMS remark)
      重新标记阶段为了修正并发期间由于用户进行运作导致的标记变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,也需要STW。简单来说,就是标记上个阶段进行时,新产生的垃圾。
  • 4、并发清除(CMS concurrent sweep)
       开启用户线程,同时 GC 线程开始对未标记的区域做清扫。但因为是并发的,所以这个阶段仍然会产生新的垃圾,称为“浮动垃圾”。

  初始标记(CMS initial mark)和重新标记(CMS remark)会导致用户线程卡顿,STW现象发生

  在整个过程中,CMS回收器的内存回收基本上和用户线程并发执行。由于CMS回收器并发收集、停顿低,因此有些地方称为并发低停顿回收器。

  CMS 回收器的缺点:

  • 1、CMS回收器对CPU资源非常依赖(相对后两个缺点不重要)
      CMS回收器过分依赖于多线程环境,默认情况下,开启的线程数为(CPU的数量+3)/4,当CPU数量少于4个时,CMS对用户查询的影响将会很大,因为他们要分出一半的运算能力去执行回收器线程;
  • 2、CMS回收器无法清除浮动垃圾
      由于CMS回收器清除已标记的垃圾(处于最后一个阶段)时,用户线程还在运行,因此会有新的垃圾产生。但是这部分垃圾未被标记,在下一次GC才能清除,因此被成为浮动垃圾。
      由于内存回收和用户线程是同时进行的,内存在被回收的同时,也在被分配。当老生代中的内存使用超过一定的比例时,系统将会进行垃圾回收;当剩余内存不能满足程序运行要求时,系统将会出现Concurrent Mode Failure,临时采用Serial Old算法进行清除,此时的性能将会降低
      concurrent mode failure:CMS垃圾收集器特有的错误,CMS的垃圾清理和引用线程是并行进行的,如果在并行清理的过程中老年代的空间不足以容纳应用产生的对象(也就是老年代正在清理,从年轻代晋升了新的对象,或者直接分配大对象年轻代放不下导致直接在老年代生成,这时候老年代也放不下),则会抛出“concurrent mode failure”。
      一些解决浮动垃圾的方法:

  降低触发CMS的阈值,即调节CMSInitiatingOccupancyFraction参数,该值在JDK1.8中默认是92%,可以尝试降低为68%。该参数的意义是:指定当老年代空间使用的阈值达到多少才进行一次CMS垃圾回收。

  • 3、垃圾收集结束后残余大量空间碎片
      CMS回收器采用的标记清除算法,本身存在垃圾收集结束后残余大量空间碎片 的缺点。CMS配合适当的内存整理策略,在一定程度上可以解决这个问题。

一旦空间碎片过多,无法再存储新对象,此时,就要采取一些措施了,比如使用Serial Old。

  对于产生碎片空间的问题,可以通过开启 -XX:+UseCMSCompactAtFullCollection,在每次Full GC完成后都会进行一次内存压缩整理,将零散在各处的对象整理到一块。设置参数-XX:CMSFullGCsBeforeCompaction告诉CMS,经过了N次Full GC之后再进行一次内存整理。

  • CMS实现机制
      根据GC的触发机制分为:
      1、周期性Old GC(被动)。2s执行一次。
      2、主动Old GC。触发条件:如YGC过程发生Promotion Failed,进而对老年年代进行回收。

“promotion failed”:YGC时,Survivor空间溢出,溢出部分对象进入老年代时,如果空间不足则抛出“promotion failed”错误。

3.3.2 G1*

  G1是一种服务端应用使用的垃圾收集器,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保证较高的吞吐量
  与其它GC回收相比,G1具备如下4个特点:

  • 1、并行与并发
      使用多个CPU来缩短STW的停顿时间,部分其他回收器需要停顿Java线程执行的GC动作,G1回收器仍然可以通过并发的方式让Java程序继续执行。
  • 2、分代回收
      与其他回收器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他回收器配合就能独立管理整个GC堆,但它能够采用 不同的策略去处理新创建的对象和 已经存活 一段时间、熬过多次GC的旧对象,以获取更好的回收效果。新生代和老年代不再是物理隔离,是多个大小相等的独立Region。
  • 3、空间整合
      与 CMS 的 标记—清理 算法不同,G1 从 整体 来看是基于 标记—整理 算法实现的回收器。从 局部(两个 Region 之间)上来看是基于 复制算法 实现的。
      但无论如何,这 两种算法 都意味着 G1 运作期间 不会产生内存空间碎片,回收后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象 时不会因为无法找到 连续内存空间 而提前触发 下一次 GC。
  • 4、可预测的停顿
      这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾回收上的时间不得超过N毫秒。(后台维护的优先列表,优先回收价值大的Region)。

  G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

  G1中有个CSet(Collection Set)的概念,是一组可被回收的分区的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间或老年代。CSet会占用不到整个堆空间的1%大小。
  G1中还有个RSet(Remembered Set)分区,如图:

  每个蓝色区域代表一个Region,黄色区域代表RSet,该区域记录了其他Region中对象到本Region的引用。RSet的价值在于:使得垃圾收集器不需要扫描整个堆栈,就能找到谁引用了当前分区的对象,只需要扫描RSet即可。
  G1首先将堆分为大小相等的Region,避免全区域的垃圾回收。然后追踪每个Region垃圾堆积的价值大小,在后台维护一个优先列表(CSet),根据允许的回收时间优先回收价值最大的Region。同时G1采用Remembered Set来存放Region之间的对象引用 ,其他回收器中的新生代与老年代之间的对象引用,从而避免全堆扫描。G1的分区示例如图所示:

Humongous是大对象区域。其标准有两个:超过单个Region的50%;跨多个Region。

  从上面的图中也能看出,G1的内存区域不是固定的E或O。
  G1新生年区域比例是动态的:5%-60%,一般不用也不必手工指定。因为这是G1预测停顿时间的基准。
  虽然G1的新老年代是动态的,但是也有YGC和FGC,其触发条件和普通的垃圾回收器也一样,即:Eden空间不足和Old空间不足。G1的FGC在JDK10之前是串行的,在JDK10之后是并行的

  G1的MixedGC(MixedGC是不分代的)相当于CMS。用InitiatingHeapOccupancyPercent 参数表示,默认是45%,当Old区比例超过该值时,启动MixedGC。MixedGC的过程和CMS相似,也是有四个阶段:

  和CMS有差异的是第四个阶段,具有个筛选的过程。筛选那些最需要回收的、垃圾占的最多的Region,然后把Region中的存货对象复制到别的Region,复制的过程中还有压缩,这样碎片就会减少。

  这种使用Region划分内存空间,以及有优先级的区域回收方式,保证G1回收器在有限的时间内可以获得尽可能高的回收效率。
  G1回收的4个步骤:

  • 1、 初始标记(CMS initial mark)
      初始标记仅仅是标记GC Roots内直接关联的对象。这个阶段速度很快,需要Stop the World。
  • 2、 并发标记(CMS concurrent mark)
      并发标记进行的是GC Tracing,从GC Roots开始对堆进行 可达性分析,找出存活对象。
  • 3、重新标记(CMS remark)
      重新标记阶段为了修正并发期间由于用户进行运作导致的标记变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,也需要Stop The World。
  • 4、筛选回收
      首先对各个Region的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间 来制定回收计划。这个阶段可以与用户程序一起 并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高回收效率。

      G1和CMS的调优目标之一就是尽量别有FGC。
3.3.3 ZGC

  Zgc是Jdk11中要发布的最新垃圾收集器。完全没有分代的概念,先说下它的优点吧,官方给出的是无碎片,时间可控,超大堆。 Z Garbage Collector,即ZGC,是一个可伸缩的、低延迟的垃圾收集器,主要为了满足如下目标进行设计:

  • 停顿时间不会超过10ms;
  • 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在10ms以下);
  • 可支持几百M,甚至几T的堆大小(最大支持4T)。

  ZGC为什么可以这么优秀,主要是因为以下几个特性:

  • 1、Concurrent
      ZGC只有短暂的STW,大部分的过程都是和应用线程并发执行,比如最耗时的并发标记和并发移动过程。
  • 2、Region-based
      ZGC中没有新生代和老年代的概念,只有一块一块的内存区域page,以page单位进行对象的分配和回收。
  • 3、Compacting
      每次进行GC时,都会对page进行压缩操作,所以完全避免了CMS算法中的碎片化问题。
  • 4、NUMA-aware
      现在多CPU插槽的服务器都是Numa架构,比如两颗CPU插槽(24核),64G内存的服务器,那其中一颗CPU上的12个核,访问从属于它的32G本地内存,要比访问另外32G远端内存要快得多。
    ZGC默认支持NUMA架构,在创建对象时,根据当前线程在哪个CPU执行,优先在靠近这个CPU的内存进行分配,这样可以显著的提高性能,在SPEC JBB 2005 基准测试里获得40%的提升。
  • 5、Using colored pointers
      和以往的标记算法比较不同,CMS和G1会在对象的对象头进行标记,而ZGC是标记对象的指针。

      其中低42位对象的地址,42-45位用来做指标标记。
  • 6、Using load barriers
      因为在标记和移动过程中,GC线程和应用线程是并发执行的,所以存在这种情况:对象A内部的引用所指的对象B在标记或者移动状态,为了保证应用线程拿到的B对象是对的,那么在读取B的指针时会经过一个 “load barriers” 读屏障,这个屏障可以保证在执行GC时,数据读取的正确性。
3.3.4 Epsilon

  Epsilon(A No-Op Garbage Collector)垃圾回收器控制内存分配,但是不执行任何垃圾回收工作。一旦java的堆被耗尽,jvm就直接关闭。设计的目的是提供一个完全消极的GC实现,分配有限的内存分配,最大限度降低消费内存占用量和内存吞吐时的延迟时间。一个好的实现是隔离代码变化,不影响其他GC,最小限度的改变其他的JVM代码。

3.4 并发标记算法

  G1和CMS的并发标记算法用的都是三色标记算法。该算法的难点在于:在标记的过程中,对象的引用关系在发生改变。该将对象分成三种类型:黑色、灰色和白色。

  • 1、黑色
      根对象,自身和成员变量均已被标记(成员变量表示引用的对象)。
  • 2、灰色
      自身被标记,成员变量未被标记。
  • 3、白色
      自身未被标记。
      三色标记会产生两种问题:错标和漏标
  • 1、错标

      在该情况中,D不是垃圾,但也会被清除掉。
      CMS的解决方案是Incremental Update,对应到上面的例子中,就是把A重新标记成灰色。
  • 2、漏标
      漏标指的是:本来是存活的对象,但是由于没被遍历到,做垃圾给回收了。看下面的例子:当进行并发标记的时候此时C是一个垃圾对象,是要被回收的,如果此时对象B指向对象C的引用没有了,但是A指向了C,那么此时这个对象C就会找不到了,原因是因为通过B已经找不到C了,但是此时A指向了C,C是有新引用的不能被回收(A是黑色的不会再被扫描,在重新标记阶段就不会找到C)


  只要能跟踪到A指向了C,或者跟踪到B指向C消失,就能解决漏标问题:

  1. incremental update--增量更新,关注引用的增加,当A指向C的时候就把这个A变成一个灰色对象,这样在重新扫描的时候就可以扫描的到
  2. SATB snapshot at the beginning --关注引用的消失,当时B指向C的引用消失了,就把这个引用推到GC的堆栈(都是灰色对象指向白色对象的引用),保证C还可以被GC扫描到(因为存在这个引用的一个记录在GC的堆栈中,所以扫描的时候还是可以找到C这个对象)

  CMS中使用,漏标采用增量更新;G1中使用,漏标采用SATB。
  G1中采用SATB的原因:

  1. 因为增量更新会在重新标记的时候将从黑色变成灰色的对象在扫描一遍,会更费时。
  2. 使用SATB就会大大减少扫描对象,原因是只多扫描在GC堆栈中发生改变的引用(和G1的一个RSet进行配合,效率高)

四、GC相关问题

4.1 JVM的永久代中会发生垃圾回收吗*

  垃圾回收不会发生在永久代,如果永久代满了或是超过了临界值,会触发Full GC。
  Java8中已经移除了永久代,新加了一个叫元数据区的native内存区。

4.2 对象的分配过程

  • 1、栈上分配
      当进行栈上分配时,需要具备以下条件:

      在进行JVM调优时,该模块一般不需要改变
      栈上分配指的是:将线程中的私有对象打散,让它在栈上分配,而不是在堆上分配。好处是对象跟着方法调用自行销毁,不需要进行垃圾回收,可以提高性能。

      比如方法中的user引用,就是方法的局部变量,new的User()实例在堆上,我们需要的就是把这个实例打散:比如我们的实例user中有两个字段,就把这个实例认作它内部的两个字段以局部变量的形式分配在栈上也就是打散,这个操作称为:标量替换。
      使用栈上分配策略除了需要开启标量替换,还需要开启逃逸分析。什么是逃逸分析?其实就是判断我们将这个user对象会不会return出去,出去了的话,这时候我们对于这个对象来说就不会受用栈上分配,因为后续的代码可能还需要使用这个对象实例,可以说只要是多个线程共享的对象就是逃逸对象。
      栈上分配有什么好处?不需要GC介入去回收这个对象,出栈即释放资源,可以提高性能,原理:由于我们GC每次回收对象的时候,都会触发STW,这时候所有线程都停止了,然后我们的GC去进行垃圾回收,如果对象频繁创建在我们的堆中,也就意味这我们也要频繁的暂停所有线程,这对于用户无非是非常影响体验的,栈上分配就是为了减少垃圾回收的次数。
      栈上分配需要的技术基础,逃逸分析。逃逸分析的目的是判断对象的作用域是否会逃逸出方法体。注意,任何可以在多个线程之间共享的对象,一定都属于逃逸对象。
      栈上分配示例:
	public void test(int x,inty ){
   		String x = "";
   		User u = ...;
	}

  同样的User的对象实例,分配100000000次,启用栈上分配,只需6ms,不启用,需要3S。

  • 2、TLAB线程本地分配
      TLAB线程本地分配缓存区是什么?工作原理分析,TLAB全称Thread Local Allocation Buffer,即线程本地分配缓存区,是一个线程专用的内存分配区域。在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用。
      TLAB是虚拟机在堆内存的eden划分出来的一块专用空间线程专属。在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如需要分配内存,在自己的空间上分配,在不存在竞争的情况大大提升分配效率。
      该种分配的特点是:

      在进行JVM调优时,该模块一般不需要改变

  • 3、完整的对象分配过程

  • 4、分配担保机制
      在JVM的内存分配时,也有这样的内存分配担保机制。就是当在新生代无法分配内存的时候,把新生代的对象转移到老年代,然后把新对象放入腾空的新生代

4.3 对象什么时候进入老年代*

  都跟年龄有关。

  • 1、age超过-XX:MaxTenuringThreshold指定次数(TGC)
      对象头,markword里面,GC age标识位占用4位,所以对象的年龄最大为15:

Parallel Scavenge 15
CMS 6
G1 15

  可以理解为:长期存活的对象进入老年代。

  • 2、动态年龄(不重要)
      假设有次的YGC是Eden&S1->S2,如果S2中的存活对象超过了S2空间的一半,就把S2中年龄最大的对象放入老年代。
  • 3、分配担保(不重要)
      YGC期间 survivor区空间不够了 空间担保直接进入老年代。
      可以理解为:大对象直接进入老年代。

4.4 简述分代垃圾回收器是怎么工作的*

  分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
  新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程:

  • 1、把Eden+From Survivor存活的对象放入To Survivor区。
  • 2、清空Eden和From Survivor分区。
  • 3、From Survivor和To Survivor分区交换,From Survivor变To Survivor,To Survivor变From Survivor。
  • 4、每次在From Survivor到To Survivor移动时都存活的对象,年龄就+1,当年龄到达15(默认配置是15)时,升级为老生代。大对象也会直接进入老生代。
  • 5、老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

4.5 什么时候触发Full GC*

  对于Minor GC,其触发条件比较简单,当Eden空间满时,就将触发一次 Minor GC。而Full GC触发条件相对复杂,有以下情况会发生full GC:

  • 1、调用System.gc()
      只是建议虚拟机执行Full GC,但是虚拟机不一定真正去执行。
  • 2、老年代空间不足
      老年代空间不足的常见场景为大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的Full GC,应当尽量不要创建过大的对象以及数组。
      除此之外,可以通过-Xmn参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过-XX:MaxTenuringThreshold调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
  • 3、方法区空间不足
      在JDK 1.7及以前,HotSpot虚拟机中的方法区是用永久代实现的,永久代中存放的为一些Class的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么虚拟机会抛出OutOfMemoryError。
      为避免永久代占满造成Full GC现象,可采用的方法为增大永久代空间或转为使用CMS。
  • 4、通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 5、由Eden区/S1区向S2区复制时,对象大小大于S2可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
  • 6、CMS GC时出现promotion failed和concurrent mode failure
      对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
      promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
      应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。

4.6 什么情况下会出现栈溢出*

  • 1、方法创建了一个很大的对象,如List、Array。
  • 2、是否产生了循环调用、死循环。
  • 3、是否引用了较大的全局变量。

4.7 常见的OOM原因

  • 1、内存加载的数据量太大,一次性从数据库取太多数据;
  • 2、集合类中有对对象的引用,使用后未清空,GC不能进行回收;
  • 3、代码中存在循环产生过多的重复对象;
  • 4、启动参数堆内存值小。

4.8 如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?

  不会,在下一个垃圾回收周期中,这个对象将是可被回收的。

4.9 对象是怎么从年轻代进入老年代的*

  在下面四种情况下,对象会从年轻代进入老年代:

  • 1、如果对象够老,会通过提升(Promotion)进入老年代,这一般是根据对象的年龄进行判断的。
  • 2、动态对象年龄判定。有的垃圾回收算法,比如G1,并不要求age必须达到15才能晋升到老年代,它会使用一些动态的计算方法。
  • 3、分配担保。当 Survivor 空间不够的时候,就需要依赖其他内存(指老年代)进行分配担保。这个时候,对象也会直接在老年代上分配。
  • 4、超出某个大小的对象将直接在老年代分配。不过这个值默认为0,意思是全部首选Eden区进行分配。

4.10 如何减少GC的次数?

  • 1、对象不用时最好显示置为NULL
      一般而言,为NULL的对象都会被作为垃圾处理,所以将不用的对象置为NULL,有利于GC收集器判定垃圾,从而提高了GC的效率。
  • 2、尽量少使用System.gc()
      此函数建议JVM进行主GC,会增加主GC的频率,增加了间接性停顿的次数。
  • 3、尽量少使用静态变量
      静态变量属于全局变量,不会被 GC 回收,他们会一直占用内存
  • 4、尽量使用StringBuffer,而不使用String来累加字符串
  • 5、分散对象创建或删除的时间
      集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在这种情况下只能进行主GC以回收内存,从而增加主GC的频率。
  • 6、尽量少用finalize函数
      它会加大GC的工作量。
  • 7、如果有需要使用经常用到的图片,可以使用软引用类型,将图片保存在内存中,
    而不引起OOM
  • 8、能用基本类型入INT,就不用对象Integer
  • 9、增大-Xmx的值

4.11 什么是分布式垃圾回收(DGC )?它是如何工作的?

  RMI子系统实现基于引用计数的“分布式垃圾回收”(DGC),以便为远程服务器对象提供自动内存管理设施。
  当客户机创建(序列化)远程引用时,会在服务器端DGC上调用dirty()。当客户机完成远程引用后,它会调用对应的clean()方法。
  针对远程对象的引用由持有该引用的客户机租用一段时间。租期从收到dirty()调用开始。在此类租约到期之前,客户机必须通过对远程引用额外调用 dirty() 来更新租约。如果客户机不在租约到期前进行续签,那么分布式垃圾收集器会假设客户机不再引用远程对象。

4.12 GC回收期的基本原理是什么?垃圾回收期可以马上回收内存吗?

  对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆中的所有对象。当GC确定一些对象为“不可达”时,GC就有责任回收这些内存空间。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。

4.13 finalize() 方法什么时候被调用?

  垃圾回收期决定回收某对象时,就会运行该对象的finalize方法。但如果内存是充足的,那么垃圾回收可能永远也不会进行,也就是说finalize方法不会执行,显然指望它做收尾工作是靠不住的。
  finalize方法主要的用途是回收特殊渠道申请的内存,比如JNI(Java Native Interface)调用时产生的内存。

4.14 简述Java内存分配与回收策略以及Minor GC和Major GC*

  1. 对象优先在堆的Eden区分配。
  2. 大对象直接进入老年代。
  3. 长期存活的对象将直接进入老年代。

  当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC。Minor GC通常会发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收速度比较快。Full GC/Major GC发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行Minor GC,这样可以加快老年代的回收速度。

4.15 Minor GC和Full GC的区别*

  Minor GC:回收新生代,因为新生代对象存活时间很短,因此Minor GC会频繁执行,执行的速度一般也会比较快。新生代内存不够用时触发。
  Full GC:回收老年代和新生代,老年代的对象存活时间长,因此Full GC很少执行,执行速度会比Minor GC慢很多。

4.16 新生代、老年代都存储哪些对象*

  • 新生代
      方法中new一个对象,就会先进入新生代。
  • 老年代
      1、新生代中经历了N次垃圾回收仍然存活的对象就会被放到老年代中。
      2、大对象一般直接放入老年代。
      3、当Survivor空间不足。需要老年代担保一些空间,也会将对象放入老年代。

4.17 OOM你遇到过哪些情况,SOF你遇到过哪些情况

  除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OOM异常的可能。

  • 虚拟机栈和本地方法栈溢出
      如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
      如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
      StackOverflowError的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。
      栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。

  栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、map数据过大。

  • 运行时常量池溢出
      异常信息:java.lang.OutOfMemoryError:PermGenspace。
      如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,可以通过-XX:PermSize-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
  • 方法区溢出
      异常信息:java.lang.OutOfMemoryError:PermGenspace。
      方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。

4.18 将堆内存为什么要分成新生代、老年代,新生代中要分为Eden、Survivor的好处*

  JVM将内存分为不同的代,是为了更有效地管理内存和优化垃圾回收。
  将内存分为新生代和老年代,根据对象的不同特性在不同代中进行分配,可以更快地回收新生代中的垃圾,减少了全局垃圾回收的频率,提高了内存的利用率和性能。

  • Eden:新创建对象的主要分配区域,大部分新对象都会被分配到Eden空间。
  • Survivor:主要用于存放Eden区中经过一次垃圾回收后仍然存活的对象,经过多次存活的对象会被移到老年代。

  将新生代分为Eden和Survivor的好处:

  1. 减少碎片化: 在Eden中对象连续分配,减少了内存碎片化,提高了内存的利用率。
  2. 实现对象的快速分配与回收: Eden空间的对象生命周期短暂,使得垃圾回收器能够更快速地识别和清除不再使用的对象,避免了将短期对象放入老年代造成的性能问题。

  这样的分代和分区设计,能够更好地适应不同对象的生命周期,提高了垃圾回收的效率,同时也优化了内存的利用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值