1. JAVA寻找垃圾的算法
1.1 引用计数法
1.2 根可达算法
2. JAVA清除算法
2.1 标记清除MS(Mark Sweep)
顾名思义,标记-清除算法分为两个阶段,第一阶段为标记(mark)阶段, 第二阶段为清除(sweep阶段).
在标记阶段,collector从根对象开始进行遍历,对从根对象可以访问到的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象。
而在清除阶段,collector对堆内存(heap memory)从头到尾进行线性的遍历,如果发现某个对象没有标记为可达对象-通过读取对象的header信息,则就将其回收。
优点:
算法相对简单
存活对象比较多的情况下效率比较高
2.2 复制清除 copying
在堆区中,对象分为新生代(年轻代)、老年代和永生代。而复制算法发生是发生在新生代的。 新生代的内存区域又被划分为Eden区Surviver0区和Surviver1区,划分比例为8:1:1。
新建的对象一般分配在新生代的Eden区,当Eden快满时进行一次小型的垃圾回收。存活的对象会移动到 Survivor0区(以下简称S0)并把年龄+1,然后清除Eden区。
当再次发生 GC 时,Eden和S0区的存活对象将复制到先前闲置的S1区,同时把年龄+1。并清除Eden和S0区。 以后每次发生YGC时,就把Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor上, 最后清理掉Eden和Survior,就是说,每次能使用90%的新生代内存容量。
当然,也可能会出现剩余10%的Survivor空间不够复制原有存活对象的情况,那就需要依赖其它内存(这里指老年代)进行分配担保(Handle Promotion)。通过分配担保机制,这些对象会直接进入老年代。
实际上,说的再通俗一点,就是将Eden和一块有内容的Survivor上存活的对象,复制到没有内容的Survivor上,然后有内容的Survivor变成没有内容的Survivor,没有内容的Survivor变成有内容的Survivor。Eden区和两个幸存区域的 S1和S2区将交替的作为存活对象的存放区和闲置区。并且如果存活对象的寿命达到某个阈值,它将被分配到老年代中。(注意在JDK8中已经没有老年代的概念,使用的是metaspace的概念,感兴趣请参考 jdk8 Metaspace 调优)
优点:
扫描一次效率偏高
没有碎片
2.3 标记整理
制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,“标记-整理”算法的示意图如下图所示。
优点:
不会产生碎片
不会内存减半
缺点:
因为扫描2次所以,效率偏低。
3. JVM 内存分代模型
YGC(yong GC):新生代发生的GC
FGC(Full GC):整个JAVA内存空间发生的的GC, 触发FGC会stop the world。
JVM分代模型分为:新生代、老年代、永久代(JDK1.7)/Metaspace(元数据区 JDK1.8)
分代 | 组成 | 内存存放位置 | 存放的数据 | GC | 存储大小 |
---|---|---|---|---|---|
新生代 | Eden区 + 2个Survivor区 | 堆内 | 新生对象 | YGC+FGC | 指定 |
老年代 | 一块 | 堆内 | 老年对象 | FGC | 指定 |
永久代(JDK 1.7) Metaspace(JDK 1.8) |
一块 | 堆外 | 元数据 - Class MethodArea |
无GC | JDK1.7以前 必须指定大小 JDK1.8及以后,不限制。 |
4. 分配担保机制
https://cloud.tencent.com/developer/article/1082730
5. 一个对象的生命周期
当一个新生对象申请内存时首先会尝试在栈上分配,如果分配失败则在Eden区分配。 当对象每次进行一次copying时如果还存活会在S0和S1之间移动,每移动一次则年龄+1, 当年龄超过15时进入老年代。
5.1 栈上分配
什么样的对象会分配到栈上呢? 需要满足以下几点:
- 线程私有的小对象
- 无逃逸(没有方法之外的变量指向方法内分配的对象)
- 支持标量替换
5.2 TLAB (Thread Local Allocation Buffer)
什么是TLAB?**TLAB**
的全称是:Thread Local Allocation Buffer
(线程本地分配缓冲)
当无法在栈上分配时会尝试使用TLAB分配, TLAB是每个线程会在Eden区申请一个1%的Eden区大小的内存当作线程自己的本地内存来分配对象。
程序证明`TLAB**`:
//程序1, 不逃逸。
public class ProveTLAB {
//内部对象
class User {
long id;
String name;
public User(long id, String name) {
this.id = id;
this.name = name;
}
}
//创建新的User对象 使用i让对象不会被缓存, 保证每次都是新创建
void alloc(long i) {
u = new User(i, "name " + i);
}
public static void main(String[] args) {
TestTLAB t = new TestTLAB();
long start = System.currentTimeMillis();
for(long i=0; i<10_0000_0000L; i++){
t.alloc(i);
}
long end = System.currentTimeMillis();
System.out.println((end - start)/1000);
}
}
结果为:
29
程序2:
需使用JVM启动参数:
-XX:-DoEscapeAnalysis: 关闭逃逸分析
-XX:-EliminateAllocations: 关闭标量替换
-XX:-UseTLAB: 关闭TLAB
//程序2
//-XX:-DoEscapeAnalysis -XX:-EliminateAllocations -XX:-UseTLAB -Xlog:c5_gc*
// 逃逸分析 标量替换 线程专有对象分配
//变量逃逸
public class ProveTLAB {
User u;
//内部对象
class User {
long id;
String name;
public User(long id, String name) {
this.id = id;
this.name = name;
}
}
//创建新的User对象 使用i让对象不会被缓存, 保证每次都是新创建
void alloc(long i) {
//内部变量不逃逸
u = new User(i, "name " + i);
}
public static void main(String[] args) {
TestTLAB t = new TestTLAB();
long start = System.currentTimeMillis();
for(long i=0; i<10_0000_0000L; i++){
t.alloc(i);
}
long end = System.currentTimeMillis();
System.out.println((end - start)/1000);
}
}
5.3 对象何时进入老年代
超过 -XX:MaxTenuringThreshold
指定的次数(默认时15)。
为什么时15? 因为对象头只有4位表示对象年龄,4位能表示的最大值是15。
当FGC发生时如果目标survivor区的对象占用超过50%, 则会把符合年龄最大的对象转移至老年区,也不管是不是15岁的对象了。
虚拟机并不是永远地要求对象的年龄必须达到了-XX:MaxTenuringThreshold
才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold
中要求的年龄。
5.4 详细
- 新生对象直接尝试在栈上分配,
- 分配不上则看是否是大对象,如果是大对象直接进老年区,如果是小对象则尝试使用TLAB,不管是不是在TLAB这个对象都是在Eden区,
- Eden区进行FGC时采用copying算法把对象在Eden区和两个Survivor区来回移动,每移动一次则年龄+1。
- 如果年龄超过15岁则把对象放入老年区或者目标Survivor区已满50%则不管最大的对象年龄是多少都会把最大年龄的对象移动至老年区。
- 最老年区发生GC。
6. 常见的垃圾回收器
JDK诞生 Serial追随 提高效率,诞生了PS(Parallel Scavenge),为了配合CMS(Concurrent Mark Sweep) ,诞生了PN(ParNew),CMS是1.4版本后期引入,CMS是里程碑式的GC,它开启了并发回收的过程,但是CMS毛病较多。因此目前任何一个JDK版本默认都不是CMS。 并发垃圾回收是因为无法忍受STW
Serial
年轻代使用的垃圾回收器, 单线程串行回收
a stop the world, copying collector which use a single GC thread. 使用单线程GC采用复制清除算法时会卡顿。
- SerialOld
老年代使用的垃圾回收器, 单线程串行回收
a stop the world, mark-sweep-compact collector which use a single GC thread. 使用单线程GC采用标记清除/标记整理算法时会卡顿。
- PS(Parallel Scavenge):
年轻代使用的垃圾回收器, 多线程并行回收
a stop the world, copying collector which use mutiple GC thread. 使用多线程GC采用复制清除算法时会卡顿
- ParallelOld:
老年代使用的垃圾回收器 多线程并行回收
a stop the world, a compacting collector which use mutiple GC thread. 使用多线程GC采用标记整理算法时会卡顿
- ParNew(Parallel New):
年轻代的垃圾回收器, 配合CMS的多线程并行回收
Parallel Scavenge 优化版本, 针对CMS配个使用做了一些增强兼容,
a stop the world, copying collector which use mutiple GC thread. 使用多线程GC采用复制清除算法时会卡顿 It differs from “Parallel Scavenge” in that it has enhancements that make it usable with CMS. 与 “Parallel Scavenge”不同的是,针对CMS配合使用时做了一些增强 For example “ParNew” does the sychronization needs so that it can run doing the concurent phases of CMS. 例如:CMS在某个特定阶段的时候 “ParNew”会同时运行。
- CMS(Concurrent Mark Sweep):并发标记清除。 老年代并发的垃圾回收器, 垃圾回收和应用程序同时运行,降低STW的时间(200ms) CMS问题比较多,所以现在没有一个版本默认是CMS,只能手工指定 CMS既然是MarkSweep,就一定会有碎片化的问题,碎片到达一定程度,CMS的老年代分配对象分配不下的时候,使用SerialOld 进行老年代回收 想象一下: PS + PO -> 加内存 换垃圾回收器 -> PN + CMS + SerialOld(几个小时 - 几天的STW) 几十个G的内存,单线程回收 -> G1 + FGC 几十个G -> 上T内存的服务器 ZGC 算法:三色标记 + Incremental Update
- G1(10ms) 算法:三色标记 + SATB
- ZGC (1ms) PK C++ 算法:ColoredPointers + LoadBarrier
- Shenandoah 算法:ColoredPointers + WriteBarrier
- Eplison
- PS 和 PN区别的延伸阅读: ▪https://docs.oracle.com/en/java/javase/13/gctuning/ergonomics.html#GUID-3D0BB91E-9BFF-4EBB-B523-14493A860E73
collector | 串行并行 | 最大处理内存 | 运行区域 | 算法 | 特点 |
---|---|---|---|---|---|
Serial | 单线程串行 | 几十M | 年轻代 | copying | |
SerialOld | 单线程串行 | 几十M | 老年代 | mark-sweep-compact | |
Parallel Scavenge | 多线程并行 | 几百M — 几个G | 年轻代 | copying | |
ParallelOld | 多线程并行 | 几百M — 几个G | 老年代 | compacting | |
ParNew | 多线程并行 | 几百M — 几个G | 年轻代 | copying | |
Concurrent Mark Sweep | 多线程并发 | 20G | 老年代 | mark-sweep | |
G1 | 上百G | ColoredPointers+ SATB | 10ms内响应 | ||
ZGC | 4T - 16T(JDK13) | ColoredPointers + LoadBarrier | 1ms内响应 | ||
Shenandoah | ColoredPointers + WriteBarrier | ||||
Eplison |
1.8默认的垃圾回收:PS + ParallelOld
常见垃圾回收器组合参数设定:(1.8)
- -XX:+UseSerialGC = Serial New (DefNew) + Serial Old
- 小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器
- -XX:+UseParNewGC = ParNew + SerialOld
- -XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old
- -XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】
- -XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
- -XX:+UseG1GC = G1
- Linux中没找到默认GC的查看方法,而windows中会打印UseParallelGC
- java +XX:+PrintCommandLineFlags -version
- 通过GC的日志来分辨
- Linux下1.8版本默认的垃圾回收器到底是什么?
- 1.8.0_181 默认(看不出来)Copy MarkCompact
- 1.8.0_222 默认 PS + PO
7. CMS
CMS全称是”Concurrent Mark Sweep”,并发标记清除。CMS是一个可并发的GC。被称作“a mostly concurrent mark collector(一个几乎全程都是并发的标记清除收集器)”。 在程序运行期间不需要程序停顿就可以GC,也就是说程序运行可以与GC并行运行,程序可以同时运行、产生垃圾和GC。 CMS是里程碑式的GC,它开启了并发回收的过程,但是CMS毛病较多。因此目前任何一个JDK版本默认都不是CMS。 并发垃圾回收是因为无法忍受STW7.1 CMS的4个阶段
- initial mark: 初始标记
- concurent mark: 并发标记
- remark: 重新标记(用于标记在concurent mark阶段产生的新垃圾)
- concurent sweep: 并发清理
在并发清理阶段还会产生少量垃圾, 这叫做浮动垃圾, 浮动垃圾会在下一次GC是清理。
7.2 CMS的缺陷
- Memory Fragmentation: 内存碎片化
CMS本身使用的还是标记清除算法
,所以在空间越大时候会碎片化问题会越严重。
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction 默认为0 指的是经过多少次FGC才进行压缩
- Floating Garbage 浮动垃圾
Concurrent Mode Failure 产生:if the concurrent collector is unable to finish reclaiming the unreachable objects before the tenured generation fills up, 如果在终身代填满之前并行收集器无法回不可达的对象 or if an allocation cannot be satisfied with the available free space blocks in the tenured generation, 或者终身带的闲置空间块不能满足分配的的时候 then the application is paused and the collection is completed with all the application threads stopped 然后应用程序暂停并且收集器会完全停止行用程序的所有线程。
解决方案:降低触发CMS触发的阈值让CMS频繁回收内存 -XX:CMSInitialtingOccupancyFraction 92% 翻译后是:CMS初始化发生比例为92%, 意思是CMS在内存占用达到92%时发生FGC,因此可以调低此值。
PromotionFailed 解决方案类似,保持老年代有足够的空间 –XX:CMSInitiatingOccupancyFraction 92% 可以降低这个值,让CMS保持老年代足够的空间
8. G1
The Garbage First Garbage Collector (G1 GC) is the low-pause, server-style generational garbage collector for Java HotSpot VM. The G1 GC uses concurrent and parallel phases to achieve When G1 GC determines that a garbage good throughput. When G1 GC determines that a garbage collection is necessary, it collects the regions with the least live data first (garbage first).
G1是一种服务端应用使用的垃圾收集器,.目标是用在多核、大内存集器上. 在大多数情况下司以卖现指定的GC暂停时间,同时还能保持较高的吞吐量。
在G1以前,JVM的内存都是直接申请一大块,然后再内部划分各种区域,因此在**G1以前的垃圾回收器都是逻辑分代,物理内存也分代**
。 但是这种情况有一个天然的缺陷无论什么垃圾回收器都无法避免, 那就是随着内存越来越大,要回收的空间越来越大,碎片化就会越来越严重,所以STW的时间会越来越长,最终都会达到无法忍受的地步。 所以必须改变内存模型来避免这个问题。 G1 就是采用内存分块的模型来解决这个问题的。 他把内存分为很多很多小块, 再把每个小块标记为JVM的Eden区,Old区等,以小块单位来一个一个扫描、标记、 回收。因此从这个原型可以看出,**G1是逻辑分代,但是物理已经不分代**
。
8.1 G1特点
什么是CardTable?
JVM把每个Region的索引记录在一张表里面。这张表被称为CardTable
。
为什么会有Card Table?
我们知道,JVM在进行垃圾收集时,通过GC Roots需要先标记所有可达对象,然后再清除不可达对象,释放内存空间。
GC Roots是垃圾收集器寻找可达对象的起点,通过这些起始引用,可以快速的遍历出存活对象。
现代JVM,堆空间通常被划分为新生代和老年代。由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么,需要跟踪从老年代到新生代的所有引用。如果使用CardTable
的话会记录所有Region的索引,如果发生了老年代对象引用了新生代的对象,那么CardTable
上面的老年代的region索引会被标记两位Dirty, 下次GC再扫描时会直接扫描CardTable
上面的dirty索引即可,不用全表扫描, 从而避免每次YGC时扫描整个老年代,减少开销。
一句话总结CardTable:由于做YGC时,需要扫描整个Old区,效率非常低,所以JVM设计了CardTable, 如果一个``Old``区CardTable中有对象指向Young区,就将它设为Dirty,下次扫描时,只需要扫描Dirty的Card即可。在结构上,Card Table用BitMap来实现。
8.3 CSet
什么是CSet?
CSet全称是:Collection Set
- CSet=Collection Set
- G1会把要回收的分区用一张表记录下来,等需要回收时直接扫描这张表内存储的所有分区的位置即可。这张表叫做CSet。
- 在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间、或者老年代。CSet会占用不到整个堆空间的1%大小。
8.3 RSet
- RSet=Remembered Set
- Remembered Set 是在G1的region区中,每一个region区内部都维护了一个自己的一张表,这张表存储了其他region对本region的引用。
- RSet的价值在于:使得垃圾收集器不需要扫描整个堆中找到谁引用了当前分区中的对象, 只需要扫描RSet即可。
8.4 三色标记
G1会通过GC Root扫描对象,如果扫描过了就会把对象标记一个颜色。 颜色总共分3个,每个颜色的含义如下:
- 白色:从未被标记过的对象。
- 灰色:自身被标记,但是成员变量未被标记。
- 黑色:自身和成员变量都已被标记。
漏标问题:
在remark过程中, 黑色对象重新指向了一个白色对象,与此同时,黑色对象对这个白色对象的引用删除了, 此时由于垃圾回收期不会再次扫描黑色对象,因此,此时如果垃圾清理过程中删除了黑色的对象,则永远没有对象引用白色对象, 此时白色对象会被漏标,永远不会被回收。
解决漏标:
从漏标的产生原因来看只有同时符合两个条件是才会产生漏标
- 黑色对象引用指向了灰色所指向的白色对象。
- 灰色对象删除了对白色对象的引用。
想要解决漏标问题,只要打破上面2个条件中的任何一个就可以。所以解决方案有2个
- 跟踪黑色对象指向白色对象的引用增加。
- 跟踪灰色对象指向白色对象的引用消失。
所以针对2个解决方案产生了2个算法:
incremental update
: 增量更新,关注引用的增加,把黑色重新标记为灰色,下次重新扫描对象属性。CMS使用此算法。SATB算法(snapshot at the beginning)
: 关注引用的删除,当灰色对象指向白色对象的引用消失时,要把这个引用推到GC的堆栈,保证白色对象还能被GC扫描到。G1使用此算法。
为什么G1不采用 增量更新 算法?
因为把黑色对象重新标记为灰色后黑色对象要重新扫描所有属性,这样会损耗性能。使用SATB
方式的缺点是会浪费一部分内存,但是现在时代最不缺的就是硬件了。
9. 实际问题
- 有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器32位,1.5G的堆,用户反馈网站比较缓慢,因此公司决定升级,新的服务器为64位,16G的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了这是为什么?
- 为什么原网站慢?
因为服务器内存太小,会频繁触发GC所以会stop-the-world。
- 为什么升级了新服务器之后反而会卡顿?
因为FGC会扫描全部内存,所以内存越大stop-the-world越长。
a. 如何解决?
JVM默认是PS的单线程收集器, 换成PN+CMS+G1。
- 系统CPU经常100%,如何调优?(面试高频)
CPU100%一定是有线程在严重占用CPU资源。解决步骤如下:
- 找出哪个进程CPU占用高(top)
- 找出找出哪个线程CPU占用高(top -Hp)
- 导出该线程的堆栈数据(jstack)
- 查找哪个方法(栈针)消耗时间长(stack)
- 结论无非两种 1. 工作线程占比高 2.GC线程占比高
- 系统内存飙高,如何查找问题?(面试高频)
- 导出堆内存 (jmap)
- 分析 (jhat jvisualvm mat jprofiler … )
- 如何监控JVM
- jstat jvisualvm jprofiler arthas top…