黑马JVM课程——垃圾回收(二)

如何判断对象可以回收

引用计数分析算法

在对象头中使用一个计数器,每当一个对象被引用的时候+1,断开引用的时候-1
这种方案可能会出现循环引用的情况
如下图,A对象引用B对象,B对象又引用A对象,此时他们两个是没有被其他对象引用的
应该被回收,但因为互相引用,就会导致无法回收

可达性分析算法

  • Java 虚拟机中的垃圾回收器采用可达性分析算法来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着GC ROOT对象为起点的引用链找到该对象,找不到,表示可以回收
  • 那些对象可以作为GC ROOT

为了分析那些可以作为GC ROOT节点,我们可以使用一个工具,MemoryAnalyzer,它可以更加详细的查看堆信息
使用这个工具,需要配合jmap命令来进行,需要执行下面的命令
jmap -dump:format=b,live,file=1.bin 6364

-dump: 表示本次操作需要把堆信息dump下来
format=b format表示转储文件的格式,b是二进制文件
live 表示抓取快照的时候只关心存活的对象,在抓取快照之前,会进行一次垃圾回收
file=1.bin 要保存文件的位置以及名称
最后的数字是当前运行服务的pid

具体代码如下,分别在创建对象后,对象置空后进行一次抓取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {  
public static void main(String[] args) throws IOException {
List<Object> list = new ArrayList<>();
list.add("a");
list.add("b");
System.out.println(1);
System.in.read();

list = null;
System.out.println(2);
System.in.read();
System.out.println("结束......");
}
}

如下图可以看到,可以作为GC ROOT 节点的对象

  • System Class JVM启动需要的一些核心类,主要是支撑JVM运行
  • JNI Global
  • Thread 活动线程正在使用的一些对象
  • Busy Monitor 被作为锁的对象
  • 其他(并不一定只有这些)
    GC-ROOT查看示意图

四种引用

强引用

我们平时使用的大多数对象引用,都是强引用(使用new关键字创建的对象,当然强引用对象的创建方式并不只有这一种)
特点:如果所有的GC Root对象都没有与某个对象有强引用链接,那么这个对象就可以被垃圾回收了

软引用

通过SoftReference引用对象,这个引用就是软引用
特点:如果一个对象只存在软引用,那么当进行GC之后,如果内存依旧不够使用,则在下次进行GC的时候回收软引用的对象
软引用自身也是一个对象(SoftReference类型的对象),也会占用内存,SoftReference回收可以使用引用队列
当软引用对象所引用的对象被垃圾回收后,软引用对象(SoftReference类型对象)就会被放到引用队列中,可以监听引用队列进行处理

弱引用

通过WeakReference引用对象,这种引用方式为弱引用
特点:在进行GC的时候,无论内存是否充足,都会回收弱引用对象,弱引用本身(WeakReference对象)也会占用内存,他的回收需要使用引用队列

虚引用

使用PhantomReference引用对象时,这种引用方式就是虚引用
虚引用需要结合引用队列ReferenceQueue来使用
比如在ByteBuffer中就使用虚引用来释放直接内存
当对象只剩下虚引用的时候,随时都有可能被垃圾回收

终结器引用

使用 FinalReference 引用对象,这种引用方式为终结器引用
需要配合引用队列使用(终结器引用,用户一般不会使用,是jvm在回收对象的时候使用的)
finalize方法的调用就使用到了终结器引用
当一个对象正在被回收时(还没有回收),虚拟机会创建该对象的终结器引用,并且进入引用队列,在引用队列上有一个优先级很低的线程,扫描到终结器引用,会根据引用找到对象,调用finalize方法实现资源清理,在下次GC时该对象会被回收。
由于使用finalize的对象在被回收时,需要两次,且调用finalize的线程优先级特别低,finalize方法可能迟迟无法调用,所以不推荐使用finalize方法

垃圾回收算法

标记清除

  • 标记:遍历内存区域,对需要回收的对象打上标记
  • 清除:再次遍历内存,对已经标记过的内存进行回收
  • 缺点:
    • 遍历两次内存空间(一次标记,一次清除)
    • 容易产生内存碎片,当需要一块比较大的内存时,无法找到一块满足需求的(此时或许总剩余空间还足够),因而不得不再次发出GC

标记整理

  • 标记:遍历内存区域,对需要回收的对象打上标记
  • 整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存
  • 缺点:
    • 在整理内存空间的时候,牵扯到对象引用地址的改变,速度比较慢

复制

将内存划分为等大的两块,每次只使用其中的一块
当一块用完之后,触发GC,将该快内存中存活的对象复制到另外一块区域,然后一次性清理掉这块没有用的内存,下次GC再重新复制回来,如此往复

  • 先做标记,然后复制对象
  • 相对于标记清除算法,解决了内存碎片化的问题
  • 效率更高(清理内存时,记住首尾地址,一次性抹掉)
  • 缺点:
    • 内存利用率不高,每次只能使用一半内存

分代垃圾回收

定义

当前大多数虚拟机都采用分代收集算法,这个算法并没有新的内容,只是根据对象的存活时间长短,将内存分为了新生代和老年代,这样就可以针对不同的区域,采用相对应的算法,如:

  • 新生代,每次都有大量对象死亡,有老年代作为内存担保,采用复制算法
  • 老年代,对象存活时间长,采用标记整理算法,或者标记清理算法都可以

新生代采用复制算法并不会粗暴的将内存分为两等份使用
而是将内存分为一个伊甸园,两个幸存区,内存占比默认是8:1:1(可以使用jvm参数调整)
真正使用的时候,会使用伊甸园的全部区域,和一个性存区区域,当内存占满,需要执行GC的时候,会将存活对象复制到另一个性存区中,然后清除之前使用两块内存中的全部数据,然后再使用伊甸园和存有幸存对象的性存区,满了之后在进行此番操作,往复循环

  • 对象首先会分配在伊甸园区
  • 新生代空间不足时,触发 minor GC ,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加1,并且交换 from 和 to
  • minor gc 会引发 stop the world,暂停其他用户线程,等垃圾回收结束后,用户线程才恢复运行
  • 当对象寿命超过阀值,会晋升到老年代,最大寿命是15(年龄标记使用4bit存储,最大是15,并不一定就是15晋升,也有其他的原因会导致提前晋升)
  • 当老年代空间不足,会先触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW 的时间更长

相关JVM参数

含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size)
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阀值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

如下内容,是使用参数打印的垃圾回收信息,以及程序结束时堆的信息
-Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
使用了上面这些jvm参数(堆内存初始20m,最大20m,新生代10m,使用Serial垃圾回收器,打印垃圾回收信息)
[GC开头的两行,表示年轻代垃圾回收信息(老年代垃圾回收以Full GC开头),DefNew表示新生代,后面是回收前内存,回收后内存,总内存,使用时间;然后是整个堆回收前占用内存,回收后占用内存,总内存,回收占用时间,时间主要看最后real的时间,平均时间
Heap 程序停止的时候的堆信息
def new generation 新生代堆信息,总内存,已经占用的内存
eden 伊甸区的内存大小,占用比例
from 幸存区from的内存大小,使用占比
to 幸存区to的内存大小,使用占比
tenured generation 老年代的内存带下,总空间,使用空间
老年代的空间大小,使用占比
Metaspace 的相关信息

1
2
3
4
5
6
7
8
9
10
11
[GC (Allocation Failure) [DefNew: 6436K->1024K(9216K), 0.0027524 secs] 6436K->1543K(19456K), 0.0028086 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 8859K->512K(9216K), 0.0041159 secs] 9378K->9223K(19456K), 0.0041326 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
Heap
def new generation total 9216K, used 1106K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 7% used [0x00000000fec00000, 0x00000000fec94930, 0x00000000ff400000)
from space 1024K, 50% used [0x00000000ff400000, 0x00000000ff480048, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8711K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 85% used [0x00000000ff600000, 0x00000000ffe81c18, 0x00000000ffe81e00, 0x0000000100000000)
Metaspace used 3328K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 359K, capacity 388K, committed 512K, reserved 1048576K

垃圾回收器

常见的垃圾收集器

垃圾收集器 开启参数 运行 目标 算法 区域
Serial -XX:+UseSerialGC 串行 响应速度优先 复制算法 年轻代
Serial Old -XX:+UseSerialOldGC 串行 响应速度优先 标记-整理 老年代
ParNew -XX:+UseParNewGC 并行 响应速度优先 复制算法 年轻代
Parallel Scavenge -XX:+UseParallelGC 并行 吞吐量优先 复制算法 年轻代
Parallel Old -XX:+UseParallelOld 并行 吞吐量优先 标记-整理 老年代
CMS -XX:UseConcMarkSweepGC 并行 响应速度优先 标记-清理 老年代
G1 -XX:UseG1GC 并行 响应速度优先 标记整理+复制算法

Serial

  • 新生代单线程垃圾收集器
  • 在收集垃圾的时候需要停止所有工作线程,直至垃圾收集完毕
  • client模式下新生代默认垃圾收集器

Serial Old

  • 单线程老年代垃圾收集器
  • Serial 垃圾收集器的老年代版本
  • CMS 收集器失效之后的备用选择

ParNew

  • 实际上就是Serial收集器的多线程版本(除了多线程外,其他和Serial一模一样,甚至有部分参数都是通用的)
  • 是除了Serial外唯一可以和CMS配合的新生代垃圾收集器(使用CMS作为老年代垃圾收集器,会默认启用该垃圾收集器作为年轻代的收集器)

Parallel Scavenge

  • 吞吐量优先的年轻代垃圾收集器

Parallel Old

  • Parallel Scavenge 垃圾收集器的老年代版本(并发)
  • Parallel Old 出现之前,Parallel Scavenge 地位是非常尴尬的,因为能与之匹配的老年代垃圾收集器只有 Serial Old,一直到 Parallel Old 出现之后,才真正的实现了“吞吐量优先”

CMS

  • 响应速度优先的老年代垃圾收集器
  • 整个垃圾收集过程包括以下四个阶段
  • 其中初始标记重复标记这两个过程任然需要SWT;
    • 初始标记初始标记仅仅就是标记以下GC Root直接关联的对象,这个过程很快
    • 并发标记:并发标记就是从GC Root`的直接关联对象开始遍历整个对象图的过程,整个过程耗时比较长,但是并不需要停顿用户线程,可以和垃圾收集线程并发运行
    • 重新标记重新标记是为了修正在并发标记阶段因用户线程继续执行而导致标记发生变动的那一部分对象的标记记录,这个阶段的停顿时间要比初始标记时间长一点,但也比并发标记的时间要短的多
    • 并发清除并发清除阶段主要是并发清除之前已经标记的已经判定为死亡的对象,因为这个阶段不需要移动对象,所以也可以和用户线程并发执行
  • 浮动垃圾:在并发标记和并发清理阶段,用户线程还是在继续运行的,程序运行自然也就伴随着垃圾产生,但这部分垃圾是出现在标记过程结束之后的,垃圾收集器无法在当次垃圾回收中将他们处理掉,只好等待下一次垃圾收集时再清理
  • 同样,因为在垃圾收集阶段用户线程还是在继续运行的,所以CMS并不能等垃圾把老年代填满之后再进行收集,需要预留一部分的空间给用户线程,默认是92%时进行垃圾回收,但也有并发失败的情况,一旦出现,就会启用Serial Old垃圾收集器进行回收(具体也可以根据实际情况通过参数设置)
  • 由于CSM是基于标记清理算法的,所以会留下内存碎片,当内存碎片过多的情况 下,可能会导致大对象内存分配失败,所以CMS留有参数,可以指定在几次不进行空间整理的垃圾回收后会进行一次空间整理,默认是0,每次都会进行空间整理
  • JDK9开始被标记为不推荐使用

G1

  • 用于整个JVM堆的垃圾收集器
  • 不再遵循固定大小以及固定数量的区域划分,而是把连续的java堆划分成一个个独立的区域(Region
  • 每个 Region 根据需要来扮演新生代、性存区和老年代,根据不同区域扮演的角色不同,使用不用的垃圾收集策略
  • 每个 Region 的大小在1m-32m之间,不过需要是2的n次幂,可以通过JVM参数来指定
  • 划分有单独的Humongous区域来存放大对象,如果一个对象的大小超过Region区域的一半,就会被放进Humongous区域,一般Humongous被当做老年代对待
    • 如果对象的大小超过了Humongous的大小,会被放到N个连续的Humongous区域中
  • 每次垃圾回收并不会再针对整个堆,而是还会针对回收“价值”最大的Region
  • 记忆集:由于是分区域进行垃圾收集,所以每个Region中都有可能会有对象引用了别的Region的对象,也可能被别的Region引用,所以每个Region都会维护一个记忆集,记录着“我指向谁”以及“谁指向我
    • 记忆集的数据结构是哈希表,key是别的Region的起始地址,value是卡表索引
    • 卡表是记忆集的一种实现方式,其结构一般可以用byte数组,数组中的值是记录的对象地址
  • G1为每个Region设计了两个名为TAMS的指针,把Region中的一部分空间划分出来用来存放垃圾收集期间新对象的分配
    • TAMS范围内的对象,默认是存活的,不纳入回收范围
  • 如果内存回收的速度赶不上内存分配的速度,G1也会冻结用户线程,进行STW
  • G1收集器的运作过程大致可以分为四个步骤:
    • 初始标记:仅仅是标记一下GC Root能够直接关联的对象,并且修改一下TAMS指针的值,可以让用户线程在并发阶段在可用的Region中分配对象,并且该阶段是跟Minor GC同步完成的,所以G1在这个阶段并没有额外的停顿
    • 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个对象图谱,找到要回收的对象,这个阶段时间比较长,但是是和用户线程同步执行的。扫描完对象图谱之后,会重新处理原始快照记录下得在并发时有引用变动的对象
    • 最终标记:对用户线程做短暂的暂停,用于处理并发结算结束后遗留下来的少量原始快照记录
    • 筛选回收:对各个Region回收的成本和价值做一个排序,然后根据用户所希望的停顿时间来制定回收计划,可以回收任意多个Region,然后把决定回收的哪一分Region空间中的存活对象复制到空的Region中,再清理掉整个旧的Region空间,这个操作涉及到 对象的移动,需要暂停用户线程

ZGC

  • ZGC在内存布局上和G1一样,也是使用了Region的结构(官方称为Page或者ZPage
  • 不过在Region的大小上和G1有所不同,分为三种:
    • 小型:固定2兆,存放大小小于256kb的对象
    • 中型:固定32兆,存放大小小于4兆的对象
    • 大型:大小不固定,但必须是2的倍数,存放大于4兆的对象,只能存放一个对象(所以最小内存为4M,并不一定就会大于中型)
  • 染色指针:在有些情况下,并不一定真的要拿到对象进行处理,比如给对象进行三色标记Serial等垃圾收集器都是直接标记在对象头上的,G1是使用一个独立的数据结构进行标记的(大概占堆内存1/64),而ZGC则是使用染色指针技术,直接标记在指向对象的指针上
  • 同时也可以在指针上标记该对象是否移动,通过这种方式,可以使得回收垃圾时一旦对象复制移动完毕就可以清理掉当前内存,而不用等到所有的引用更改完毕
  • 染色指针就是一种将少量额外信息直接存储在指针上的技术
  • ZGC运作过程可以分为四步:
    • 并发标记:和G1一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经历类似G1的初始标记最终标记的短暂停顿,而这些停顿所做的事情也是类似的,不过与G1不同的是ZGC的标记是在指针上,而不是在对象上进行,会更新染色指针中的Marked0Marked1的标志位
    • 并发预备重分配:这个阶段会根据特定的查询条件统计出本次收集过程需要清理那些Region,并将这些Region组成重分配集ZGC扫描标记的时候,会扫描全堆,用更大的扫描分为省去了G1记忆集的维护
    • 并发重分配:重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region中,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的关系,得力于染色指针的支持,ZGC仅从指针上的标记就可以得知一个对象处于重分配集中,如果用户并发访问的对象在重分配集中,本次访问就会被内存屏障捕获,然后根据Region上的转发表访问转发到新复制的对象上,并同时修正该更新引用的值,直接指向新对象,ZGC的这种行为,称为指针的“自愈”,这样做的好处是只有第一次访问旧对象会陷入转发,也就只慢一次。
    • 并发重映射:重映射所做的事情就是修正整个堆中指向重分配集中旧对象的所有引用,不过这个任务在ZGC中并不是一个迫切需要完成的任务,因为即使使用旧引用去访问对象,也可以自愈,重映射清理这些旧引用的目的并不是为了不变慢,还是为了回收掉转发表的空间,所以ZGC把并发重映射的阶段合并到了下一次并发标记的阶段,反正他们都是需要遍历所有对象,可以省去一次遍历开销

三色标记

  • 并发的可达性分析(和用户和线程并发进行可达性标记)
  • 可以按照”是否访问过”这个条件标记成一下三种颜色:
    • 白色:表示对象尚未被垃圾收集器访问过,最开始对象都是白色,在分析结束后若还是白色,表示对象不可达
    • 黑色:表示垃圾收集器已经访问过,且该对象的引用对象也都已经扫描过
    • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少还有一个引用没有被扫描
  • 假如用户线程和收集器线程同时工作,一边标记引用,一边修改关系,此时会出现两中情况:
    • 已经被标记为存活的对象被切断了引用关系(转变为死亡),这种情况还可以容忍,只是会产生浮动垃圾
    • 已经被标记为死亡的对象,又重新建立了联系(复活了),这种对象如果被收集掉的话会出现程序错误
      • 同时满足一下两种情况,才会出现这种问题:
        • 赋值器创建了一条或者多条黑色对象到该对象的引用
        • 赋值器删除了全部灰色对象到该白色对象的引用
      • 解决方案
        • 增量更新:当黑色对象插入指向白色对象的引用的时候,就将整个引用记录下来,等并发扫描结束后,再将这些记录过的引用关系中的黑色对象为根重新扫描一次,可以理解为当黑色对象被插入一个白色对象后,转变为灰色对象重新扫描。(打破的是第一条规则,CMS使用的是此种方案)
        • 原始快照:当灰色对象要删除指向的白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束后,在将灰色对象作为根重新扫描,可以理解为,无论引用关系是否删除,都会按照刚开始扫描时的对象图谱来进行搜索。(打破的是第二条规则,G1使用的此种方案)

串行

  • 单线程
  • 堆内存比较小,适合个人电脑(CPU核数少)

串行垃圾收集器示意图

吞吐量优先

  • 多线程
  • 堆内存比较大,需要多核CPU
  • 在单位时间内,STW的时间最短
  • (假设在一个小时内回收垃圾2次,一次0.2秒,一共0.4,一个小时内回收的总耗时是最短的)
  • 垃圾回收时间占程序运行时间的比例越低,吞吐量越高

Parallel-Scavenge垃圾收集器运行示意图

响应时间优先

  • 多线程
  • 堆内存比较大,需要多核CPU
  • 尽可能让单次STW的时间最短
  • (假设在一个小时内垃圾回收5次,一次0.1秒,一共0.5秒,每次回收的时间是最短)

CMS垃圾收集器示意图

Full GC

  • Serial GC

    • 新生代内存不足发生垃圾收集 —— minor GC
    • 老年代内存不足发生垃圾收集 —— full gc
  • Parallel GC

    • 新生代内存不足发生垃圾收集 —— minor GC
    • 老年代内存不足发生垃圾收集—— full GC
  • CMS

    • 新生代内存不足发生的垃圾收集 —— minor GC
    • 老年代内存不足 —— 并发收集(如果并发收集的速度赶不上垃圾制造的速度,才会发生 Full Gc)
  • G1

    • 新生代内存不足发生的垃圾收集 —— minor GC
    • 老年代内存不足 —— 并发收集(如果并发收集的速度赶不上垃圾制造的速度,才会发生 Full Gc)

G1内部做出的优化

JDK 8u20 字符串去重

字符串的底层是基于char数组的,以往的在创建一个新的String对象的时候(不是直接赋值常量),无论字符串是否是一样的,都会创建一个新的char数组,不过在JDK 8u20这个版本中做了优化
当新生代回收的时候会去检查字符串是否相同,相同就引用同一个char数组
G1垃圾收集器的功能

  • -XX:+UseStringDeduplication 默认开启
  • 将所有新分配的字符串放入到一个队列里
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果他们的值一样,让他们引用同一个`char[]``
  • 注意:与String.intern()不一样
    • String.intern()关注的是字符串对象
    • 而字符串去重关注的是char[]
    • 在Jvm内部,使用了不同的字符串表

JDK 8u40 并发标记类卸载

  • 所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个雷加载器的所有类都不再使用,则卸载它所加载的所有类
  • -XX:+ClassUnloadingWithConcurrentMark 默认开启

JDK 8u60 回收巨型对象

  • 一个对象大于Region的一半时,称之为巨型对象
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

JDK9 并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为FullGc
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDk9可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间

垃圾回收调优

预备知识

  • 掌握GC相关的JVM参数,会基本的空间调整
  • 掌握相关工具
  • 明白一点:调优跟应用、环境有关,没有放之四海皆准的法则
  • 查看JVM参数(过滤了关于GC的参数)
    • java -XX:+PrintFlagsFinal -version | findstr “GC”

      调优领域

  • 内存
  • 锁竞争
  • cpu占用
  • io

确定目标

  • 明确自己的项目是要做什么(如果是为了科学计算,一般要求吞吐量高,互联网项目,要求低延迟)
  • 【低延迟】还是【高吞吐量】,选择合适的回收器
    • 低延迟:CMS,G1,ZGC
    • 高吞吐量:ParallelGC

最快的GC是不发生GC

  • 查看Full GC前后的内存占用,考虑下面几个问题
    • 数据是不是太多了?
      • 比如查询数据库,全表查询然后再代码过滤
    • 数据表示是否太臃肿?
      • 对象图
        • 比如只需要表中的几个字段,但是却查询所有字段
      • 对象大小
        • 对象最少是16个字节,比如intrger24个字节,可以考虑是否使用基本类型int
  • 是否存在内存泄漏?

新生代调优

  • 新生代的特点:
    • 所有的 new 操作的内存分配非常廉价
      • TLAB thread-local allocation buffer
      • 每个线程会在伊甸园张中分配一块私有区域,每次创建对象都是在自己的私有区域分配,避免并发
    • 死亡对象的回收代价是零
    • 大部分对象用过即死
    • Minor GC 的时间远远低于Full GC

新生代越大越好吗?

新生代如果设置的非常小,会导致不停的做垃圾回收,那这个时候我们是不是把新生代设置的大一点就好了呢?

显然新生代也不是越大越好

  • 首先堆得内存空间是一点的,如果新生代设置的比较大,那么老年代空间必然会被压缩,从而导致老年代大量触发GC,老年代的GC是要比新生代代价大的多
  • 另外如果新生代设置的过大,就会导致有大量的对象在新生代中留存,虽然有着年龄15的限制,但并不是所有的对象都留到15才晋升老年代,如果新生代过大,就会导致这一部分原本应该提前晋升老年代的对象在新生代中留存下来(比如大对象)
  • 新生代使用复制算法,如果存留的对象过多,也会导致复制时间过长,包括复制之后更新引用的时间也会增加,从而导致STW时间过长
  • 不过在一定范围内,适当的增加新生代的大小,确实也可以实现一定量的优化
  • 新生代比较合适的范围:【并发量×(请求-响应)】
  • 性存区大到能保存【当前活跃对象+需要晋升对象】
  • 晋升阀值配置得当,让长时间存活的对象尽快晋升(调整晋升阈值)

老年代调优

以CMS为例:

  • CMS的老年代内存越大越好
  • 先尝试不做调优,如果没有Full GC那么表示已经是最优,否则尝试调优新生代
  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大

案例

Full GC和Minor GC频繁

  • 老年代频繁GC,但又没有OOM表示老年代每次回收掉了一些
  • 所以可以考虑是否是年轻代内存空间设置过小,导致大量短寿命对象晋升老年代
  • 或者是年轻代晋升阈值设置过小

请求高峰期发生Full GC,单次暂停时间特别长(CMS)

  • 因为CMS的垃圾回收是分几个阶段的,所以可以通过GC日志查看是哪个阶段导致的
  • 一般来说只有重新标记和并发清理阶段,如果是重新标记阶段时间过长,可能是因为重新标记的时候会去遍历年轻代,可以通过JVM参数设置在每次老年代垃圾收集之前,进行一次Minor GC
  • 也有可能是因为高峰期产生了大量的垃圾,导致CMS来不及回收,降级为了Serial Old垃圾收集器,可以考虑增大内存空间,或者减小回收时机的百分比

老年代内存充裕的情况下,发生Full GC(CMS jdk 1.7)

  • jdk1.7永久代还在JVM堆中,考虑可能是因为永久代内存设置过小,导致永久代内存不足而触发GC