JVM系列(六)[JVM调优前置基础理论知识,堆内存逻辑分区(分代)模型,对象分配逻辑,常见的垃圾回收算法,常见的垃圾回收器以及组合参数设定]

GC听起来很高大上,这些看起来很难的东西,真的去了解后,其实很简单.我们和GC就隔了一层窗户纸,捅破它!
这节学完,可以在简历上写:熟悉GC常用算法,熟悉常见垃圾回收器,具有JVM调优实战经验.
薪水+1K,哈哈哈

下面有些参数,或者一些数字的细节可能不准确,可能随着JDK的版本而变化,但是大致思路是对的,观其大略,这些细节也不可能全记住,也没必要
需要求证的地方,可以查JVMS
或者有些信息java可以打印出来,比如 java -XX:+PrintFlagsFinal -version

基础知识

1.什么是垃圾?

一个对象创建出来了,刚开始有个变量指向它,比如Object obj = new Object();这个obj变量就是这个对象的引用.
当一个对象,没有引用指向它了,它就是垃圾了,
只有循环引用的一堆对象,也是垃圾.
(两个对象循环引用,但是没有其他引用指向他俩了,也是垃圾)
(三个对象循环应用,a->b->c->a,除此之外没有其他引用,也是垃圾)

java vs c++
java:

  • 由GC处理垃圾,一般都不是马上回收.GC有自己的想法 😃
  • 开发效率高,执行效率低

c++

  • 代码中手动处理垃圾 😦
  • 可能忘记回收,导致内存泄漏
  • 可能回收多次,可能非法访问并回收了别人正在用的内存
  • 开发效率低,执行效率高

堆内存逻辑分区(分代)

  • 除Epsilon,ZGC,Shenandoah之外的GC都是使用逻辑分代模型(ZGC太新了,生产上还很少用)
  • 除了上面那仨,剩下的都是分代的GC:
    G1是逻辑分代,物理不分代
    其他的不仅逻辑分代,而且物理分代

主要是分为两大块: 新生代和老年代

  • 新生代/年轻代 new/young
    新生代又分为三块:
    1.Eden;2.Survivor-S1;3.Survivor-S2
    S1和S2一般一起聊,也有人把他俩叫做S0和S1,或者from和to,都一个意思,就是两块survivor区
  • 老年代 Old

具体对象的分配和生命周期见下文
这个图比较老了可能,现在JDK1.8的NewRatio=2,即老年代/新生代=2
在这里插入图片描述

2.怎么找到垃圾

1.Reference Count 引用计数

对象头上有个reference count,上面记着有几个引用,RC为0时这个对象就是垃圾

RC不能解决循环引用的问题,那咋办呢?看第二种方案
(Python用的引用计数法,内部应该有其他方法解决循环引用问题)

2.Root Searching 根可达算法 很重要

  • 何为根对象
  1. JVM stacks,线程的栈帧里引用的对象
  2. static reference in method area,Clazz,方法区中的静态变量和类结构
  3. run-time constant pool,常量池
  4. native method stask,本地方法栈(调用C C++方法)用到的对象

从根(Root)上的对象开始,顺着线往下捋,看根对象引用谁,能找到的都是有用的,剩下的就是垃圾
一般这些有用的对象,就称作存活对象.
在这里插入图片描述

3.常见的垃圾回收算法 GC Algorithms

  1. 标记清除(mark sweep) - 位置不连续 产生碎片 效率偏低(两遍扫描)
  2. 拷贝算法 (copying) - 没有碎片,浪费空间
  3. 标记压缩(mark compact) - 没有碎片,效率偏低(两遍扫描,指针需要调整)

Mark-Sweep 标记清除

  • 是什么
    把有用的和没用的用标记标出来(RootSearching),然后清除没用的那些
  • 特点
  1. 算法相对简单,适用于存活对象比较多的情况,因为需要回收的对象很少,
  2. 两遍扫描,效率偏低;(第一遍找到有用的加上标记,第二遍找到没用的清除)
  3. 容易产生碎片.
  4. 不浪费空间
  5. 不需移动复制对象

Eden区里面的存活对象比较少,这个算法不适合

在这里插入图片描述
在这里插入图片描述

Copying 拷贝

  • 是什么
    把区域分成两半:a区存放对象,b区留着啥也不放
    GC时,先从a区找到存活对象,把它拷贝到b区,然后把a区的对象全部回收;
    下次回收时从b拷贝到a,然后回收b
  • 特点
  1. 算法相对简单,适用于存活对象比较少的情况
  2. 只扫描一次,效率较高
  3. 不会产生碎片
  4. 空间浪费,内存减半
  5. 移动复制对象,需要调整对象的引用,效率较低

适合于Eden区
在这里插入图片描述
在这里插入图片描述

Mark-Compact 标记压缩

  • 是什么
    把存活对象往区域的前边压缩,剩下的全部清除回收
  • 特点
  1. 扫描两次,第一遍标记,第二遍移动压缩
  2. 不会产生碎片
  3. 不浪费空间
  4. 需要移动对象,效率偏低

在这里插入图片描述
在这里插入图片描述

4.JVM内存分代模型(用于分代垃圾回收算法)

除Epsilon ZGC Shenandoah之外的GC都是使用逻辑分代模型
G1是逻辑分代,物理不分代
除此之外不仅逻辑分代,而且物理分代

区域划分

  • 新生代/年轻代 new/young
    新生代又分为三块:
    1. Eden
    2. Survivor-S1
    3. Survivor-S2
      S1和S2一般一起聊,也有人把他俩叫做S0和S1,或者from和to,都一个意思
  • 老年代 Old

永久代VS元数据区
JDK<=1.7时,还有个永久代Perm Generation,永久代不参与GC
JDK>=1.8后,永久代被元数据区替代

  1. 永久代 元数据 - 存放 Class结构
  2. 永久代必须指定大小限制 ,元数据可以设置,也可以不设置,无上限(受限于物理内存)
  3. 字符串常量 1.7 - 永久代,1.8 - 堆
  4. MethodArea逻辑概念 - 永久代、元数据

在这里插入图片描述

GC相关的概念

经常听到MinorGC,YGC,FGC等词,他们是啥意思呢?

  • MinorGC/YGC,新生代空间耗尽时触发

  • MajorGC/FullGC/FGC,老年代无法继续分配空间时触发,新生代和老年代同时进行回收,比较慢,重量级.

  • 一般垃圾回收的过程:

    1. YGC回收之后,Eden中大多数的对象会被回收,活着的进入s0
    2. 再次YGC,活着的对象eden + s0 -> s1
    3. 再次YGC,eden + s1 -> s0
    4. 年龄足够 -> 老年代 (15 CMS 6)
    5. s区装不下 -> 老年代
    6. 老年代满了FGC Full GC
  • JVM调优指什么?GC Tuning (Generation)

    1. 尽量减少FGC
    2. MinorGC = YGC
    3. MajorGC = FGC

在这里插入图片描述

一个对象的生命之旅(对象分配过程)

一个对象产生时,首先尝试在栈上分配,如果符合条件 分配在栈了,当方法结束时栈弹出,对象就终结了;
如果没在栈上分配,就判断对象,如果特别大直接进入Old区,否则的话就分配至Eden区(TLAB也属于Eden区);
如果进入Eden区:
经过一次GC后.Eden区中的存活对象进入S1;
每次GC,会把S1的存活对象扔进S2,S2的存活对象扔进S1,每换个区对象的年龄+1;
多次垃圾回收后,对象的年龄到了,就进入Old区.

在这里插入图片描述

什么情况下,对象会分配在栈上?

直接分配在Eden区的话,会存在多线程的竞争,效率较低.
为了提高效率,减少多线程的竞争,会优先考虑分配在栈上和TLAB上

  • 栈上分配

    1. 线程私有小对象
    2. 没有逃逸,只在某段代码中使用
    3. 支持标量替换,这个对象可以用几个简单的变量替换

    无需调整
    多线程没有竞争
    方法结束,栈弹出,对象直接拜拜了,不用GC回收

  • 线程本地分配TLAB(Thread Local Allocation Buffer)

    1. 如果栈空间不够了,会优先分配在TLAB
    2. 占用Eden,默认是Eden的1%
    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中年龄最大的对象放入老年代
    https://www.jianshu.com/p/989d3b06a49d
  3. 分配担保(不重要)
    YGC期间 survivor区空间不够了 空间担保直接进入老年代
    参考:https://cloud.tencent.com/developer/article/1082730

对象的分配总结图

在这里插入图片描述

5.JVM参数查看

JVM的参数分为三种:

  1. 标准参数,-开头的参数, 所有版本JDK都支持,直接输入java 查看
  2. 非标准参数, -X开头的参数,输入java -X 查看
  3. 不稳定参数 -XX:(+/-), 每个版本可能不同, java -XX:+PrintFlagsFinal 查看,特别多,几百个

常见的垃圾回收器

先说几个名词解释:

  • Serial单线程
  • Parallel多线程
  • STW:top-the-world,时间停止!只有我(GC)能动! 即GC时需要暂时停止JVM中的程序指令
  • safe point安全点:要GC时,程序中很多线程在运行,并不马上stop the world,而是让各个线程都做好准备去暂停,暂停的点就是安全点.

历史

  1. JDK诞生,Serial追随
  2. 为了提高效率,诞生了ParallelScavenge
  3. 为了配合CMS,诞生了ParNew,
  4. CMS是1.4版本后期引入,CMS是里程碑式的GC,它开启了并发回收的过程,但是CMS毛病较多,因此目前任何一个JDK版本默认是CMS
    并发垃圾回收是因为无法忍受STW
    在这里插入图片描述

一般的的组合有:
1. Serial + Serial Old,早期使用
2. Parallel Scavenge + Parallel Old,简称PS+PO,默认的GC组合,现在很多生产环境用
3. PartNew + CMS
4. G1
5. ZGC

1.Serial (年轻代,串行回收)

a stop-the-world,copying collector which uses a single GC thread
单CPU效率最高
虚拟机时Client模式

2.Parallel Scavenge (年轻代,并行回收)

a stop-the-world,copying collector which uses multiple GC threads

3.ParNew(年轻代 配合CMS的并行回收)

a stop-the-world,copying collector which uses multiple GC threads
It differs from “Parallel Scavenge” in that it has enhancements that make it useable with CMS
For example,ParNew does the synchronization needed so that it can run during the concurrent phases of CMS
其实就是Parallel Scavenge 的增强版,可以和CMS配合使用

ParNew 和 Parallel Scavenge的具体区别

4.Serial Old(老年代,串行回收)

5.Parallel Old (老年代,并行回收)

a compacting collector that uses multiple GC threads

6.CMS (非常重要,里程碑)(老年代,和工作线程并发回收)

concurrent mark sweep
a mostly concurrent,low-pause collector
垃圾回收和应用程序同时运行,降低STW的时间(200ms)

算法:三色标记 + Incremental Update

常见的4个阶段(phases):

  1. initial mark 初始标记,STW,只标记根上的对象,很快
  2. concurrent mark 并发标记,工作线程同时进行,所以会产生新对象,标记所有的对象,占用时间最长,大概GC的80%
  3. remark 重新标记,STW,标记"并发标记"中新产生的对象,很快
  4. concurrent sweep,并发清理,多线程,工作线程同时进行,所以会产生新对象,产生的新垃圾叫浮动垃圾,浮动垃圾交给下次GC

垃圾回收时其他的工作线程都要暂停,等GC完毕后才继续
JDK1.4后期引入了CMS,开启了并发回收,垃圾回收线程和其他工作线程可以同时进行.

CMS问题比较多,所以现在没有一个版本默认是CMS,只能手工指定
CMS既然是MarkSweep,就一定会有碎片化的问题,碎片到达一定程度,CMS的老年代分配对象分配不下的时候,使用SerialOld 进行老年代回收
想象一下:
PS + PO -> 加内存 换垃圾回收器 -> PN + CMS + SerialOld(几个小时 - 几天的STW)
几十个G的内存,单线程回收 -> G1 + FGC 几十个G -> 上T内存的服务器 ZGC

CMS的问题:

  1. 内存碎片
    CMS设计出来的时候,机器内存一般很小,其实不适用于现代大内存的机器了
    产生很多碎片后,当老年代装不下对象了,会请出Serial Old 标记清除,很慢,STW
  2. 浮动垃圾
    浮动垃圾很多,内存分配不下了,会请出Serial Old

7.G1(10ms)

算法:三色标记 + SATB

8.ZGC (1ms) PK C++

算法:ColoredPointers + LoadBarrier

9.Shenandoah

算法:ColoredPointers + WriteBarrier

10.Eplison

11.PS 和 PN区别的延伸阅读

https://docs.oracle.com/en/java/javase/13/gctuning/ergonomics.html

12.垃圾收集器跟内存大小的关系

  1. Serial 几十兆
  2. PS 上百兆 - 几个G
  3. CMS - 20G左右
  4. G1 - 上百G
  5. ZGC - 4T - 16T(JDK13)

1.8默认的垃圾回收:PS + ParallelOld

常见垃圾回收器组合参数设定(JDK1.8)

  • -XX:+UseSerialGC = Serial New (DefNew) + Serial Old

    • 小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器
  • -XX:+UseParNewGC = ParNew + SerialOld

    • 这个组合已经很少用(在某些版本中已经废弃)
    • https://stackoverflow.com/questions/34962257/why-remove-support-for-parnewserialold-anddefnewcms-in-the-future
  • -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
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值