(JVM)全面深入Java GC!!带你完全了解 Java 的‘ 灵魂 ‘ GC垃圾回收机制!!
1. GC
1.1 如何判断对象可以回收
1.1.1 引用计数法
对类进行计数,如果这个类被调用了,那么就给这个类加1权重
可是这种计数法,会有一个弊端,就是循环调用
循环调用后就会不停的+1,因此这种算法的弊端在java这种调用中是很大的
因此,Java并没有使用这种引用计数法
1.1.2 可达性分析算法
会将不能被当作垃圾的对象称之为根对象
在垃圾回收以前会对堆中的所有对象进行一个扫描,是否会被根对象直接引用,如果没有被引用,那么就判断可以被垃圾回收
-
JVM中的垃圾回收器采用可达性分析来探索所有存货的对象
-
扫描堆中的对象,看是否能够沿着GC Root 对象为起点的引用链找到该对象,找不到,表示可以回收
-
哪些对象可以作为 GC Root?
可以发现,根对象也分为了几类,基本类、针对操作系统的类、线程类、锁类
这几种类型就是根对象的大致分类
1.1.3 四种引用
蓝色直线:强引用
1.1.3.1 强引用
当两个根对象都调用了同一个对象,那么该对象就被说明是强引用
当两个根对象对它的调用都断掉了,那么就可以被垃圾回收了
只有所有GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
public static final int _4MB = 4 * 1024 * 1024;/*** 强引用* 场景:当将多张图片放进一个list集合中时,就可能发生对内存溢出问题* @throws IOException*/
@Test
public void strong() throws IOException {ArrayList<byte[]> list = new ArrayList<>();for (int i = 0; i < 5; i++) {list.add(new byte[_4MB]);}System.in.read();
}
1.1.3.2 软引用
-
当存在一个根对象A软引用了一个对象B,另一个根对象C强引用了对象B,那么这时候并不会被回收。
-
而当根对象C不再强引用对象B时,在内存不够时,对象B就会被回收了。
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时才会再次发出垃圾回收信号,回收软引用对象
可以配合引用对象来释放软引用自身
/*** 软引用* 当新加入对象时,内存不够,那么进行回收*/
@Test
public void soft(){ArrayList<SoftReference<byte[]>> list = new ArrayList<>();for (int i = 0; i < 5; i++) {SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);System.out.println(ref.get());list.add(ref);System.out.println(list.size());}System.out.println("循环结果:"+list.size());for (SoftReference<byte[]> ref : list) {System.out.println(ref.get());}
}[GC 5632K->1389K(19968K), 0.0011407 secs]
[GC 7021K->2299K(19968K), 0.0012354 secs]
[GC 7931K->3134K(19968K), 0.0011630 secs]
[GC 8766K->3901K(19968K), 0.0010361 secs]
[B@2eda0940
1
[B@3578436e
2
[B@706a04ae
3
[GC -- 17437K->17670K(19968K), 0.0012092 secs]
[Full GC 17670K->14596K(19968K), 0.0214907 secs]
[GC -- 14596K->14702K(19968K), 0.0010001 secs]
[Full GC 14702K->2121K(16896K), 0.0149012 secs]
[B@6eceb130
4
[B@10a035a0
5
循环结果:5
null
null
null
[B@6eceb130
[B@10a035a0
可以看到最后的集合中,只存在后两个了
- 软引用配合引用队列
/*** 软引用配合引用队列*/
@Test
public void softQueue(){ArrayList<SoftReference<byte[]>> list = new ArrayList<>();ReferenceQueue<byte[]> queue = new ReferenceQueue<>();for (int i = 0; i < 5; i++) {// 关联了引用队列,当软引用所关联的byte[]被回收时,软应用自己会加入到queue中去SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB],queue);System.out.println(ref.get());list.add(ref);System.out.println(list.size());}// 弹出最先进入的对象(队列中无用的软引用对象)Reference<? extends byte[]> poll = queue.poll();while(poll != null){list.remove(poll);// 在集合中清除poll = queue.poll();// 重新规定弹出对象(刷新一次避免重复循环)}System.out.println("=========================循环结果:"+list.size()+"=========================");for (SoftReference<byte[]> ref : list) {System.out.println(ref.get());}
}
1.1.3.3 弱引用
- 当存在一个根对象A软引用了一个对象B,另一个根对象C强引用了对象B,那么这时候并不会被回收。
- 而当根对象C不再强引用对象B时,不论内存是否充裕,对象B都会被回收。
这里,当 软/弱 引用的对象都被回收后,其自身 软/弱 对象也是一个对象,这时这两个对象也都会分别被放进引用队列,被回收(因为这两个对象也会占用一定的内存)
软/弱 引用是可以选择是否被加进引用队列中回收的
/*** 弱引用*/
@Test
public void weak(){// list > WeakReference > byte[]ArrayList<WeakReference<byte[]>> list = new ArrayList<>();for (int i = 0; i < 10; i++) {// 关联了引用队列,当软引用所关联的byte[]被回收时,软应用自己会加入到queue中去WeakReference<byte[]> ref = new WeakReference<byte[]>(new byte[_4MB]);list.add(ref);for (WeakReference<byte[]> w:list){System.out.println(w.get()+"======");}System.out.println(list.size());}System.out.println("=========================循环结果:"+list.size()+"=========================");
}
它删除自身,还是得配合引用队列来删除,具体代码跟软引用代码类似,也是在创建弱引用对象时,关联一个引用队列即可.
1.1.3.4 虚引用(Cleaner)直接内存地址
软/弱 引用可以选择是否加紧引用队列,而虚引用必须配合引用队列使用
最显著的就是 ByteBuffer 》直接内存
在创建虚引用时,就会将虚引用对象地址放进引用队列中被监控,之后队列会间接调用虚引用中的方法
当ByteBuffer被强引用使用完毕后,清除后,还存留有直接内存
强虚引用调用结束(被清除),虚引用对象进入引用队列,执行 Unsafe.freeMemory() 方法回收直接内存
必须配合引用队列使用,主要配合 ByteBuffer使用,被引用对象回首时,会将虚引用对象入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
1.1.3.5 终结器引用
终结器引用必须配合引用队列使用
当引用对象的不被调用(强引用结束),终结器引用就会被放进引用队列中等待回收
这时候引用对象并没有被删除,JVM 会给一个优先级很低的线程来监控终结器引用对象
这个线程会每隔一段时间就会检测一次,当检测到引用队列中存在终结器引用对象,那么就会顺着引用找到引用对象把它回收
工作效率低,由于是先将终结器引用放入队列,并且监控线程优先级还低,这时候就有可能造成引用对象长时间占用内存不被释放
不推荐
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象
1.2 回收算法
1.2.1 标记清除
-
第一阶段:
根据GC Root对象,查看哪些对象没有被引用,没有被引用的对象都标记一次
-
第二阶段:
将被标记的对象释放
清除速度快,因为只需要做一个标记清楚的处理,所以相对来说比较快。
但是在清除后有个小弊端,就是会产生清除碎片
1.2.2 标记整理
-
第一阶段
根据GC Root对象,查看哪些对象没有被引用,没有被引用的对象都标记一次
-
第二阶段
将被标记的对象释放
不同于标记清除算法,在释放后会整理对象,清理内存碎片后,多余的空间就可以更高效的放入更多的对象
它的优点就是空间利用率更加高效,缺点就是速度偏慢:如果对象整理过程中,新出现了引用对象,那么引用地址就会发生改变,这时候又需要一些时间来处理
1.2.3 复制算法
- 首先,在内存中,分别为FROM和TO两个区域。
- 将引用的对象移动至TO区域,没有被引用的对象则留在FROM区域
-
留在FROM区域的对象(未被引用的对象)会被回收清除
最后TO和FROM区域互换位置,最后达到上图的效果
这种方法优点是不会有内存碎片,而缺点则是会在内存中占据双倍的空间
1.2.4 小结:
-
标记清除算法(Mark Sweep)
- 优点:速度较快
- 缺点:会造成内存碎片
-
标记整理(Mark Compact)
- 优点:没有内存碎片
- 缺点:速度慢
-
复制算法(Copy)
- 优点:不会有内存碎片
- 缺点:需要占据双倍的内存空间
1.2.5 分代回收算法
在内存中,分为了两个内存区域,
- 新生代:存储存活时间较短的对象
- 老年代:存储存活时间较长的对象
这样就可以针对生命周期不同的对象做出不同的内存处理算法,提高效率
1.2.5.1 开始
当有新的对象创建时,会将这个新对象存放到伊甸园内存空间中,
当伊甸园空间中逐步被占用,下一个对象再被创建发现没有足够内存存放了,就会首次触发一个GC回收
这第一次GC也被称为:Minor GC
Minor GC所做的,就是利用上面三种基本算法进行清理。
当选择使用复制算法后,会把未清理的对象存放进幸存区TO中,然后再进行更换位置,To与FROM更换位置。
1.2.5.2 继续
当过一段时间又一次新生区(伊甸园)满了
触发第二次垃圾回收,第二次GC,不仅会看伊甸园中的对象,还会看幸存区中的对象
如果有对象已经死亡,那么就会被释放
若有对象还活着,那么就会转入幸存区From中,并且生命次数+1
然后 幸存区From 和 幸存区To 两个区域互相交换
在幸存区From中,如果对象超出了一个阈值。
会将该对象转移到老年代中
1.2.5.3 Full GC
当老年代内的空间也不够多了,新生代空间也不够了
那么这时候会触发一次 Full GC
Full GC 会对新生代和老年代全部都做一次清理
1.2.5.4 总结
-
对象首先分配在伊甸园区域
-
新生代空间不足时,触发minor GC,伊甸园和from存货的对象使用copy复制到to中,存货的对象年龄加一,并且交换from和to区域
-
minor gc触发时,会暂停用户线程的使用。等垃圾回收结束,用户线程才恢复运行
因为在处理gc时,幸存区地址会发生改变,在处理新对象时就会可能发生地址错误
-
当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
-
当老年代空间不足,会尝试触发minor gc,如果之后空间仍然不足,那么就会触发full gc
Full GC,在处理垃圾回收时,STW的时间更长
最后,如果Full GC处理完后,如果新生代和老年代空间仍然不足!那么就会报内存溢出错误
1.2.6 相关VM参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或(-XX:NewSize=size+ -XX:MaxNewSize=size) |
幸存区比例(动态) | -XX: InitialSurvivoRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX: SurvivoRatio=ratio |
晋升阈值 | -XX: MaxTenuringThreshold=threshold |
晋升详情 | -XX: +PrintTenuringDisribution |
GC详情 | -XX: +PrintGCDetails -verbose:gc |
Full GC 前 Minor GC | -XX: +ScavengeBeforeFullGC |
1.2.6 大对象存入
当生成一个大对象时,如果新生代放不下,那么就会直接晋升到老年代区域
而当出现多个大对象或者一个巨大的对象,老年代也放不下。
那么这时候就会报错:OutOfMemoryError 内存溢出
而在报错前,JVM 会触发一次Full GC,尝试清理一次对象。
@Test
public void test1() throws InterruptedException {new Thread(()->{ArrayList<byte[]> list = new ArrayList<>();list.add(new byte[_7MB]);}).start();System.out.println("Sleep....");Threa d.sleep(1000L);
}
当GC在某个线程中报错后,并不会导致整个java的运行结束
1.3 垃圾回收器
- 串行
- 单线程
- 使用场景:堆内存较小,适合个人电脑
- 吞吐量优先
- 多线程
- 堆内存较大,多核cpu
- 让单位时间内,STW的时间最短 0.2 0.2 = 0.4
- 响应时间优先
- 多线程
- 堆内存较大,多核cpu
- 尽可能让单词STW的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
1.3.1 串行
-XX: +UseSerialGC = serial + serialOld
复制算法 标记整理算法
上图CPU就可以看作是多个线程。
当每个线程运行时,若某个线程需要进行GC,那么其余线程都会在一个安全节点停止运行,等待GC结束后,所有线程才会开始正常运行
1.3.2 吞吐量优先
-XX: +UseParallelGC ~ -XX: +UseParallelOldGC
-XX: +UseAdaptiveSizePolicy # 动态调整新生代和伊甸园的空间比例
// 调整吞吐量的目标:1/1+ratio
// GC工作的时间不能够超过总时间的100分之一
// 如果超过了这个时间,那么就会调整堆的大小
-XX: GCTimeRatio=ratio
// 最大暂停毫秒数:200ms
-XX: MaxGCPauseMillis=ms
-XX: ParallelGCThreads=n #控制进行GC的线程数
不同于串行垃圾回收器,这种回收的方式会更加极端。
当某线程需要垃圾回收时,会调用全部线程都进行一次垃圾回收,这时候CPU的占用率是非常高的。(所谓做到尽快将垃圾清理完毕)
1.3.3 响应时间优先
-XX: +UseConcMarkSweepGC ~ -XX: +UseParNewGC ~ SerialOld
标记清除 并发执行
/ / 并发数线程设置,一般设置为1/4
-XX: ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
// 控制何时来处理垃圾回收的时间=内存占比(当达到阈值后就会进行一次垃圾回收 一般是6%)
-XX: CMSInitiatingOccupancyFraction=percent
// 在重新标记之前,做一次垃圾回收 (主要目的是减少重新标记的时间)
-XX: +CMSScavengeBeforeRemark
该策略针对于老年代。
-
假设有线程A、B、C,当 线程B 需要垃圾回收时,线程A和C就会先进行阻塞暂停
然后 线程B 会对根对象进行标记
-
到达第二个安全点,所有线程都开始运行
而 线程B 会进行并发标记,继续标记没有标记完全的根对象
-
当 线程B 所有根对象都标记完毕后,由于线程都在运行可能会对地址进行干扰,所以在标记完毕后所有线程会重新标记一次。
-
当所有标记完成,所有线程重新运行,线程B 并发进行清理
该策略对cpu占用率不高,但是用户线程的吞吐量会降低
因为当处理垃圾回收时,总有一个线程会占用一部分cpu使用率。而这部分cpu使用率被垃圾回收线程占用了,那么用户线程就会被减少占用。
当内存碎片过多的时候 ConcMarkSweepGC 就不工作了,这时候就会退化为 SerialOld 做一次串行的垃圾回收
1.3.4 G1
定义:Garbage First
- 2004 论文发布
- 2009 JDK 6u14 体验
- 2012 JDK 7u4 官方支持
- 2017 JDK 9 默认
适用场景:
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的Region
- 整体上是标记+整理算法,两个区域之间是复制算法
相关JVM参数
-XX: +UseG1GC
-XX: G1HeapRegionSize=size // 设置G1在堆中Region的大小 例:1024,2048
-XX: MaxGCPauseMillis=time
1.3.4.1 G1 垃圾回收阶段
当新生代区域满了后,会对新生代做一个并发标记,当并发标记做完,就会进行一次混合收集。混合收集结束,那么就会对新生代和老年代区域集体做一次垃圾回收
1.3.4.2 新生代垃圾回收(Young Collection)
G1把整个堆分成了大小相同的 region,每个堆大约可以有 2048 个region,每个 region 大小为 1~32 MB (必须是 2 的次方)。如下图:
- STW (Stop The World): 通常意思来说就是暂停线程
假设图中就是堆中的一块内存区域,绿色格子(E)就代表一个伊甸园区域。
当伊甸园区域逐渐被占满,这时候就会触发一次新生代的垃圾回收
触发垃圾回收后,会使用复制算法,将对象拷贝进蓝色格子(S)幸存区中
逐渐的,当幸存区中的对象也过多时,那么就会将幸存区符合要求的对象存放进橙色格子(O)老年代区域中。
同时会在堆中开辟出一块新的幸存区用于接受新的对象,老的幸存区会将不符合要求的对象存放进新幸存区中。
1.3.4.3 新生代垃圾回收+并发标记(Young Collection+CM)
-
在Young GC时会进行GC Root的初始标记
-
老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
-XX: InitiatingHeapOccupancyPercent=percent(默认45%)
1.3.4.4 混合标记(Mixed Collection)
会对E、S、O进行全面垃圾回收
- 最终标记(Remark)会STW
- 拷贝存货(Evacuation)会STW
-XX: MaxGCPauseMillis=ms
在处理新生代时,伊甸园区域会存放进幸存区,一部分幸存区为符合标准的对象也会被转移至另一个幸存区,符合标准的对象才会晋升为老年代区域,其余幸存区将会进行回收
在处理老年代时, JVM会优先考虑垃圾最多的一块区域,然后使用复制算法拷贝将符合存活标准的对象拷贝进另一块老年代区域中,剩下的的老年代区域就会被清除。
主要目的就是为了减少时间消耗
并不是会清理所有的老年代区域,只是老年代中空间不够了的区域才会被回收
1.3.4.5 Full GC
-
SerialGC
- 新生代内存不足发生的垃圾回收 -minor gc
- 老年代内存不足发生的垃圾回收 -full gc
-
ParallelGC
- 新生代内存不足发生的垃圾回收 -minor gc
- 老年代内存不足发生的垃圾回收 -full gc
-
CMS
- 新生代内存不足发生的垃圾回收 -minorgc
- 老年代内存不足
-
G1
-
新生代内存不足发生的垃圾回收 -minor gc
-
老年代内存不足
-
CMS 和 G1 引入了并发标记过程,一般情况下只要业务吞吐量没有超过并发标记的数量就不会触发Full GC。
而如果吞吐量超过了并发标记数量,那么在CMS中就会进行一个串行垃圾回收,全部线程进行一次垃圾回收。当超出阈值后,内存依旧不足,那么就会进行一次Full GC
不同于CMS,G1在老年代区域中会使用复制策略进行垃圾回收。当超出阈值后,内存依旧不足,那么就会进行一次Full GC
1.3.4.6 Young Collection 跨代引用
- 解决 新生代回收的跨代引用(老年代引用新生代)问题
在老年代中,对于一些对象会引用新生代中的对象,这些对象会被标记为脏卡。
而在寻找这些跨代调用问题时,就不会关注老年代中所有的对象,只会关注那些被标记为脏卡的对象。
- 卡表与Remembered Set
- 在引用变更时通过 pre-write barrier(写屏障)+dirty card queue(脏卡队列)
- concurrent refinement threads (并发重写线程)更新 Remembered Set(记忆表格)
当被调用的新生代对象失效时,会主动的更新脏卡队列。卡表的更新是很频繁的,当出现调用关系的取消时,脏卡队列就会进行一次更新。
1.3.4.7 Remark(标记)- 三色标记法
- pre-write barrier(写屏障)+dirty card queue(脏卡队列)
- 创建:白、灰、黑 三个集合。
- 将所有对象放入白色集合中。
- 从GC Root开始遍历所有对象,把遍历到的对象从白色集合放入灰色集合(备注:这里放入灰色集合的都是GC Root的对象)。
- 遍历灰色集合,将灰色对象引用的对象(其实就是灰色对象的字段)从白色集合放入灰色集合,然后将分析过的灰色对象(所有字段都处理完毕的)放入黑色集合。
- 直到灰色中无任何对象。
- 通过写屏障(Pre-Write Barrier)检测对象有变化,重复以上操作(因为 mark 和用户程序是并行的,所以在上一步执行的时候可能会有新的对象分配)。
- 回收掉所有白色对象(垃圾)
在不停重复晋升灰、黑色集合中,留在白色集合中的对象必定是没有被调用的对象,那么最后进行回收步骤时就可以大方回收掉白色集合中的所有对象了
可以看到三色标记法有很多步骤,而这些步骤是和用户线程并发运行的,也就是说在标记过程中,用户还在创建新对象,或者抛弃老对象。
先讲创建新对象的情况:
- A对象已经被标记为黑色
- 用户线程:A.field = new X()
这种情况下,X是白色的,而且按照三色标记法的规则,黑色的A是不会再次被标记的。如果不能把X变成灰色,那么它就会被垃圾回收掉,这个是是存在问题的。
因此,在标记开始之后,需要在对象引用更新的地方添加一个Pre-Write Barrier,用来将X直接标记为灰色。
C对象被放进队列中后,在垃圾回收时会再次进行一次检查,发现有写屏障并有调用关系,那么就会将该对象标记为黑色。这样就会避免被垃圾回收掉
1.3.4.8 JDK 8u20 字符串去重
- 优点:节省大量内存
- 缺点:略微多占用了cpu时间,新生代回收时间略微增加
-XX: +UseStringDeduplication
String s1 = new String("hello"); // ['h','e','l','l','o']
String s2 = new String("hello"); // ['h','e','l','l','o']
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复
- 如果它们值一样,让它们引用同一个char[];
- 注意,与String.inern()不一样
- String.intern()关注的时字符串对象
- 而字符串去重关注的是char[]
- 在JVM内部,使用了不同的字符串表
1.3.4.9 JDK 8u40 并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX: +ClassUnloadingWithConcurrentMark // 默认启用
1.3.4.10 JDK 8u60 回收巨型对象
- 一个对象大于region的一半时,称之为巨型对象
- G1 不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1 会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
1.3.4.11 JDK 9 并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为Full GC
- JDK 9 之前需要使用 -XX: InitiatingHeapOccupancyPercent
- JDK 9 可以动态调整
- -XX: InitiatingHeapOccupancyPercent 用来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空挡空间
1.3.4.12 更多的更新
- 针对于Java22 版本的的GC指南Java22 - Garbage Collector Implementation
1.3.5 垃圾回收调优
预备知识:
- 掌握GC相关的VM参数,会基本的空间调整
- 掌握相关工具
- 明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则
1.3.5.1 调优领域
- 内存
- 锁竞争
- cpu 占用
- io
1.3.5.2 确定目标
-
低延迟 还是 高吞吐量 ,选择合适的回收器
-
CMS、G1
-
ParallelGC
-
Zing(几乎没有延迟时间)付费
允许Java应用程序利用他们需要执行的任何数量的内存,仅受系统中物理内存或虚拟机管理程序可识别的数量的限制。 由于Zing独特的垃圾收集器,基于Azul的开创性C4 (连续并发压缩收集器)技术,GC暂停与JVM堆的大小无关,并且不限制应用程序可伸缩性。并且得益于Azul ReadyNow! 技术,可以使得Java程序快速的启动,解决Java热身问题。
-
ZGC(执行效率超低延迟)
ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。ZGC 第一次出现是在 JDK 11 中以实验性的特性引入,这也是 JDK 11 中最大的亮点,使用 –XX:+UseZGC 可以启用 ZGC。内存多重映射和染色指针的引入,使 ZGC 的并发性能大幅度提升。
ZGC 只有 3 个需要 STW 的阶段,其中初始标记和初始转移只需要扫描所有 GC Roots,STW 时间 GC Roots 的数量成正比,不会耗费太多时间。再标记过程主要处理并发标记引用地址发生变化的对象,这些对象数量比较少,耗时非常短。可见整个 ZGC 的 STW 时间几乎只跟 GC Roots 数量有关系,不会随着堆大小和对象数量的变化而变化。
-
1.3.5.3 最快的GC?
最快的GC,其实时不发生GC。能不GC是最好的
查看FullGC前后的内存占用,考虑下面几个问题?
-
数据是不是太多了?
-
数据表示是否太臃肿?
- 对象图
- 对象大小
-
是否存在内存泄漏?
-
强/弱
-
第三方缓存实现
-
1.3.5.4 新生代调优
新生代的特点
- 所有的new操作的内存分配非常廉价
- TLAB thread-local allocation buffer
- 死亡对象的回收代价是0
- 大部分对象用过即死
- Minor GC 的时间远远低于Full GC
1.3.5.4.1 空间越大越好?
-Xmn
先说结论,不是。
当新生代空间越来越大,那么老年代的空间就会被压缩。同时一旦FullGC,会拉长FullGC的执行时间,并且运行曲线下降会越来越慢
这里Oracle官方给出的大小建议:新生代空间需要在老年代大小的25%~50%之间
- 理想情况:新生代能容纳所有【并发量*(请求-响应)】的数据
1.3.5.4.2 幸存区
- 幸存区大到能保留【当前活跃对象+需要晋升对象】
如果幸存区空间太小,可能会导致将一些活跃对象提前晋升到老年代,那么这时候就需要等到老年代GC时,这些对象才会被清除,这无疑增加了这些对象的生命周期
-
晋升阈值配置得当,让长时间存货对象尽快晋升
-XX: MaxTenuringThreshold=threshold
-XX: +PrintTenuringDistribution
1.3.5.5 老年代调优
以CMS为例
-
CMS的老年代内存越大越好
-
先尝试不做调优,如果没有Full GC,那么已经足够OK,否则先尝试调优新生代
-
观察发现Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3
-XX: CMSInitiatingOccupancyFraction=percent
1.3.5.6 案例
-
案例1:Full GC和 Minor GC 频繁
- 调整新生代或老年代的空间
-
案例2:请求高峰期发生Full GC,单词暂停时间特别差(CMS)
- 先查看CMS中哪个阶段耗时太长,再看看那个阶段是哪里耗时太长,并根据问题进行实际解决
-
案例3:老年代充裕情况下,发生Full GC(jdk1.7)
- 1.7中使用的是元空间,不如1.8的空间那么宽裕,所以只能扩展原空间的大小
2. 😊👉前篇知识点
- 深入JAVA底层 JVM(Java 虚拟机)!带你认识JVM、程序计数器、JVM栈和方法栈还有堆内存!
- 在JVM中,类是如何被加载的呢?带你认识类加载的一整套流程!
- 带你一起研究JVM的语法糖功能 和 JVM的即时编译器
- 我们该如何认识 Java的内存模型(Java Memory Model(JMM))? 本篇文章告诉你答案 !带你全面了解JMM
3. 💕好文相推
- 还不了解Git分布式版本控制器?本文将带你全面了解并掌握
- 带你认识依赖、继承和聚合都是什么!有什么用?
- 2-3树思想与红黑树的实现与基本原理
- !全网最全! ElasticSearch8.7 搭配 SpringDataElasticSearch5.1 的使用