Jvm基础理论

不同语言的内存管理方式

C、C++

手工管理内存

  • 容易忘记释放内存——产生内存泄漏,最终导致内存溢出
  • 释放多次
  • 开发效率低,运行效率高

Java、Python、Go

GC(Garbage Collector)管理内存

  • 开发效率高,运行效率相对低
  • 使用门槛低

Rust

无需管理内存(栈帧推出时自动清理)

  • 运行效率高,使用门槛高

所有权(ownership):同一时刻只有一个变量指向一个对象

共享变量: 使用独特的写法处理并发的问题

GC(Garbage Collector)

垃圾:没有任何引用指向的对象(包括循环引用)

查找算法

  • 引用计数法(无法定位循环引用的垃圾)
  • 根可达算法
    • 通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(对象不可达),则定位为垃圾
    • GC Roots
      • 虚拟机栈中引入的对象(局部变量)
      • 方法区中的类静态属性引用的对象(静态变量 static)
      • 方法区中常量引用的对象(常量 final)
      • 本地方法栈中JNI(native方法)引用的对象

回收算法

  • 标记清除算法(效率高,位置不连续,产生碎片)
  • 拷贝算法(效率高,空间利用率低)
  • 标记整理算法(效率低,空间利用率高)

垃圾回收器

  • 分代模型(将堆内存按比例划分为新生代、老年代等)
    • 串行回收器(单线程GC)
      • Serial 新生代
      • SerialOld老年代
    • 并行回收器(多线程GC)
      • ParallelScavenge新生代
      • ParallelOld老年代
      • ParNew新生代(提供给CMS使用)
      • CMS老年代
  • 分区模型(将堆内存划分为多个大小一致的独立区域)
    • G1
    • ZGC

JDK1.8默认垃圾回收器为: ParallelScavenge + ParallelOld

CMS (Concurrent Mark Sweep)

​ 此回收器在分代模型回收器中最大的区别是使用了并发GC,即业务线程和GC线程同时执行,以此进一步缩短 STW的时间,但同时也产品了致命的问题。

GC运作阶段

  1. 初始标记(CMS initial mark)

    • 仅标记GC Roots能直接关联的对象
    • STW
  2. 并发标记(CMS concurrent mark)

    • 使用三色标记算法进行并发标记
  3. 重新标记(CMS remark)

    • 重新标记在并发标记中产生变动的对象

    • STW

  4. 并发清除(CMS concurrent sweep)

    • 使用标记清除算法进行垃圾清理

三色标记算法

​ 为了能让业务线程和GC线程并发执行,使用了三色标记算法,即在标记节点时添加一个状态,记录当前查找的进度

例:有三个对象A、B、C,A对象引用B,B对象引用C

问题一(浮动垃圾)

​ 例:当A、B、C三个节点都已经被GC线程完全标记的情况下,业务线程把B到C的引用删除了,按照根可达算法此时C就是垃圾,但是在三色标记它确是完全标记的状态,这种节点称之为浮动垃圾

解决方案:下次GC时处理,并且提前GC,预留空间存放浮动垃圾

问题二(漏标)

​ 例:A(黑)、B(灰)、C(白)三个节点,业务线程把B到C的引用删除了,同时增加了A到C的引用,此时C是存在引用关系的,但是上级节点A已经是完全标记的状态,GC线程不会再扫描A的下级节点了,导致C即使存在引用,也会被当成垃圾清理,这种情况称之为漏标

解决方案:Incremental Update增量更新方案,即GC线程检测A增加引用C时,将A置为半标记状态(灰色),后续GC线程即可扫描下级节点

问题三(Incremental Update的问题)

​ Incremental Update新增引用时将该节点置为半标记状态(灰色)在单线程是没有问题的,但是CMS除了是GC线程和业务线程并发执行外还是多线程GC

​ 例:还是A(黑)、B(灰)、C(白)三个节点,业务线程把B到C的引用删除了,同时增加了A到C的引用,GC线程1检测到A增加了引用则将A置为半标记状态(灰色),此时上下文切换GC线程2,GC线程2由A扫描到B,GC线程2是在GC线程1执行之前就开始,只知道A节点只有一个B的子节点需要扫描,而且并不知道GC线程1将A置为半标记状态(灰色),GC线程2扫描完B之后则将A置为完全标记状态(黑色),此时C节点还是漏标了

解决方案:记录并发标记中产生变动的对象,在重新标记时处理,从GCRoots重新扫描一遍这种类型的对象

并发清除

​ 使用三色标记算法标记好后, 就需要清除垃圾,为了实现并发清除,只能选择使用标记清除算法,因为标记整理算法复制算法都不适用于并发清理的情况。

​ 标记清除算法最大的问题就是会存在内存碎片,如果CMS遇到了内存碎片无法分配大对象时,此时jvm会自动切换Serial Old回收器使用单线程进行标记整理算法执行垃圾回收

CMS根本问题:

1、无法彻底解决漏标的问题, 导致需要重新标记 ,影响效率

2、为了解决内存碎片的问题,切换Serial Old回收器进行回收,严重影响效率

G1(Garbage First)

​ 在G1之前的垃圾回收器全部都是采用分代模型进行内存划分,而G1则采用分区模型进行内存划分,同时保存了分代模型的概念,相比CMS内部实现逻辑更复杂,同时性能也更高,适用于管理大内存的程序。

分区模型

​ 将堆划分为多个大小相同的区域(Region),最多是2048个Region。每个Region大小 = 堆大小/ 2048,例如 对内存为4G(4096M),则Region大小为2M。也通过参数-XX:G1HeapRegionSize指定

Region的角色

  • Eden
  • Survivor
  • Old
  • Humongous(专门存放大对象的Region,当对象大小超过Region大小的50%时存放至此)

每一个区的角色都是不固定的,在GC后动态变化

Region的分配

​ 默认年轻代的占比为5%,系统运行时会自动增加,最大占比为60%,可通过参数-XX:G1NewSizePercent调整初值

​ 年轻代中的区域分配和之前分代模型一样,默认是8:1:1。例:年轻代分配到了1000个Region,则Eden类型的Region数量为800个,S0类型的Region数量为100个,S1类型的Region数量为800个

GC运作阶段

  1. 初始标记(Initial mark)

    • 仅标记GC Roots能直接关联的对象
    • STW
  2. 并发标记(CMS concurrent mark)

    • 使用三色标记算法进行并发标记(使用SATB解决漏标的问题)
  3. 最终标记(Remark)

    • 重新标记在并发标记中产生变动的对象

    • STW

  4. 筛选回收(Clean Up)

    • 使用复制算法进行垃圾清理
    • STW

SATB(Snapshost At The Beginning)

​ CMS处理漏标的方式是使用Incremental Update方式进行处理,而G1则使用SATB堆栈快照的方式进行处理。同在CMS问题二漏标的情况中, 不需要去关心A是否新增了引用,而是去在乎B指向C消失的引用,有点解铃人还须系铃人的意思。当B指向C的引用消失时,则把该引用放置当前GC线程的栈中,相当于在删除时了一次备份,在最终标记阶段中重新确认这些被删除的节点是否还存在引用。

筛选回收

​ 一个很有意思的清理垃圾方式,主要使用的是复制算法,回收开始阶段对各个Region的回收价值和回收成本进行排序,配合-XX:MaxGCPauseMillis(用户所期望的STW时间,默认是200ms)参数制定回收计划。

​ 例1:1000个Old类型的Region都使用满了,而STW时间最多允许的是200ms,根据之前回收成本的计算,只能回收500个Region,那么只会回收这500个Region,从而达到控制STW的时间

​ 例2:两个Region,一个需要花费50ms的时间释放10M的空间,而另一个只需要花费30ms的时间即可释放10M的空间,G1则会优先选择后者进行回收(First,G1名字的由来)

这种方式能极大提高在有限STW时间下的回收效率,这也是分区带来的最大优势

可预测的停顿(-XX:MaxGCPauseMillis):由用户指定STW的时间是G1一个很强大的功能,但是个值不能设置过低,如设置为20ms,响应是快了,但是回收的时间短,导致每次只能回收一小部分,当运行时间长后大部分无法回收Region占满内存,导致内存泄漏,提前Full GC,严重降低性能,所以一般设置为200ms上下是比较合理的。

GC过程分类

1、当年轻代类型的Region存放满后估算回收时间是否远远小于用户设置的STW时间,如果小于,则新增Region,直到估算回收时间接近STW时间则触发Young GC

2、当老年代类型的Region占有率达到了一定的值,触发Mixed GC回收所有年轻代类型的Region和部分老年代的Region(根据STW时间优先选择)以及大对象类型的Region

3、当Mixed GC时发现提供复制算法拷贝的空间不足,则触发Full GC,停止系统程序,采用单线程进行标记、清理、和压缩整理,这点类似于CMS

ZGC(Z Garbage Collector)

​ 在JDK11中推出,STW时间>=10ms,支持TB级别的内存管理,但在吞吐量方面相会有所降低

分区模型

也是划分为多个Region,和G1的区别是在于完全没有了分代的概念,改用了一种页面的概念

  • 小页面(Small Region):2MB,放置小于 256 KB 的小对象。
  • 中页面(Medium Region):32MB,放置大于等于 256 KB 小于 4 MB 的对象。
  • 大页面(Large Region):N * 2MB,放置大于4MB的对象

为什么要分代:如果每个对象每次GC都要扫描一遍,是很浪费性能的,有些对象存活时间长,有些存活时间短,分代,就是用来区分这些对象,而ZGC目前因技术原因还没有实现,只能使用页面的概念

染色指针(Colored Pointer)

ZGC之前的垃圾回收器都是在堆的对象头Markword中记录GC的状态,而ZGC是在对象引用地址上进行标记

在64位系统中这个地址长度为64,而实际上高位的地址我们是用不上的,ZGC则利用这些空闲的高位地址来进行标记

读屏障

​ 读屏障是JVM向应用代码插入一小段代码的技术(aop)。当应用线程从堆中读取对象引用时,就会执行这段代码,用于处理并发标记和并发转移时产生的问题

GC运作阶段

  1. 初始标记

    • 仅标记GC Roots能直接关联的对象
    • STW
  2. 并发标记 /对象重定位

    • 采用三色标记算法实现并发标记(读屏障方式处理漏标问题)
  3. 再标记

    • 重新标记在并发标记中产生变动的对象

    • STW

  4. 并发转移准备

    • 相当于是G1筛选回收的开始阶段,找到回收价值高的Region
  5. 初始转移

    • 使用复制算法移动GC Roots能直接关联的对象
    • STW
  6. 并发转移

    • 将剩页面中剩余节点进行并发转移(转发表+读屏障方式处理并发问题)
  7. 对象重定位

    • 下次GC是在并发标记中处理

ZGC在此只是了解基本的概念,详细的工作流程可参考以下文章

https://www.cnblogs.com/ciel717/p/16190585.html
https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html

JVM参数

Dockerfile的配置

# FROM openjdk:8
FROM registry.cn-shenzhen.aliyuncs.com/xurongze/jre:8
ARG JAR_NAME
ADD ${JAR_NAME}/ms-starter/target/${JAR_NAME}.jar app.jar
ENTRYPOINT java -jar \
-Dfile.encoding=UTF-8 \
-Dsun.jnu.encoding=UTF-8 \
# GC日志相关配置
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-XX:+PrintGCCause \
-Xloggc:/logs/gc/gc-%t.log \
#配置Jvm发生致命错误时生成hs_err_pid文件的路径(栈异常信息)
-XX:ErrorFile=/logs/gc/hs_err_pid%p.log \
#Dump异常快照以及以文件形式导出(堆异常信息)
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/gc \
#禁用Jvm对异常栈信息的优化 https://www.jianshu.com/p/cc1bd35466cb
-XX:-OmitStackTraceInFastThrow \
#启用多线程并行处理Reference https://www.jianshu.com/p/79d4a0516f11
-XX:+ParallelRefProcEnabled \
#设置堆外内存 https://www.jianshu.com/p/007052ee3773
-XX:MaxDirectMemorySize=64m \
# 启用G1
-XX:+UseG1GC \
# STW时间
-XX:MaxGCPauseMillis=100 \
# 栈深度
-Xss256k \
-Xms256m \
-Xmx256m \
# 堆内存设置为 物理服务器(或容器)中的总可用内存大小的80% (容器内存限制大于250M时使用此配置)
#-XX:MaxRAMPercentage=80.0 \
# 堆内存设置为 物理服务器(或容器)中的总可用内存大小的80% (容器内存限制小于250M时使用此配置)
#-XX:MinRAMPercentage=80.0 \
#元空间(存储类的元数据)
#-XX:MetaspaceSize=512m \
#元空间大小调整需要Full GC,直接与最大值设置一样避免
#-XX:MaxMetaspaceSize=512m \
/app.jar

注意:最大堆内存+堆外内存设置 不能超过 Kubernetes中Pod的Memory Limit

posted @ 2022-05-15 09:08  天朝读书人  阅读(95)  评论(0编辑  收藏  举报