总结一下JVM的知识点

218 阅读8分钟

大纲

需要ximnd文件的同学可以到我的github下载,建议大家配合《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》阅读

JVM

运行时数据区

程序计数器

虚拟机栈

局部变量表:所有的基本类型,对象的引用。

本地方法栈

堆内存

几乎所有对象实例和数组分配的区域。

方法区

静态变量、常量、类信息

  • 运行时常量池

直接内存

对象的创建

堆内存规整:指针碰撞

堆内存不规整:空闲列表

TLAB

如何判断对象已死

对象的引用类型

  • 1.强引用

  • 2.软引用

    当强引用的关系不存在后,软引用在下次GC的时候如果内存不足会被回收。

  • 3.弱引用

    当强引用的关系不存在后,弱引用将会在下次GC的时候被回收。

  • 4.虚引用

引用计数算法

循环引用问题。

可达性分析算法

可达性分析算法(Reachability Analysis)的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。

  • 1.方法区中常量引用的对象
  • 2.方法区中静态变量引用的对象
  • 3.虚拟机栈中本地变量表引用的对象
  • 4.在本地方法栈中JNI(即通常所说的Native方法)引用的对象
  • 5.被同步锁持有的对象

finalize()方法

  • 1.覆盖finalize()方法
  • 2.只执行一次

内存分配和回收策略

堆内存分配

  • 优先在Eden分配,Eden空间不足进行Minor GC

  • 大对象直接分配到老年代

    HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。 -XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效。

  • 长期存活的对象进入老年代

    虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象每熬过一次Minor GC对象年龄增加1。 默认对象年龄为15,进入老年代。 -XX:MaxTenuringThreshold

  • 动态对象年龄判断

    如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

  • 空间分配担保

    Minor GC之前,检查老年代最大连续空间是否大于新生代所有对象总空间

    • 不大于

      -XX:HandlePromotionFailure,检查是否允许空间分配担保失败。

      • 允许

        检查老年代最大连续空间是否大于历次新生代晋升到老年代对象大小的平均值。 大于则尝试进行一次Minor GC

      • 不允许,进行一次Full GC

    • 大于,证明安全

栈上分配

如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。 栈上分配可以支持方法逃逸,但不能支持线程逃逸。

  • 逃逸分析

    • 全局变量赋值逃逸
    • 方法返回值逃逸
    • 实例引用发生逃逸
    • 线程逃逸
  • 标量替换

    假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。

    • 标量
    • 聚合量
  • 同步消除

TLAB分配

垃圾收集算法

可达性分析算法

  • 根节点枚举

    • STW

    • OopMap

      记录栈中的引用类型

    • SafePoint

      生成OopMap 只有在安全点才能进行垃圾回收

      • 主动式中断

      • 抢先式中断

        • 和安全点重合
        • 创建对象和分配堆内存的点
    • 安全区域

      安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。

      • 解决Thread不占用cpu,无法响应JVM中断的问题
    • 卡表

      • 卡表是记忆集的实现方式,每个记录精确到一块内存区域,该区域内有对象含有跨代指针
    • 写屏障

      • 写入前屏障

      • 写入后屏障

        • 处理卡表变脏的问题,相当于虚拟机层面为“引用类型赋值”增加切面,赋值后更新卡表
    • 伪共享

      现代中央处理器的缓存系统中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。 卡表占1个字节,中央处理器缓存行是64个字节。如果不通线程更新的卡表正好位于同一个缓存行将会发生伪共享。

  • 并发的可达性分析

    • 三色标记

      • 白色

        在可达性刚刚开始所有的对象都是白色的,若在结束还是白色的则不可达。

      • 灰色

        正在被标记,但是这个对象至少还存在一个引用没有被标记完成。

      • 黑色

        对象已经被标记,并且它的所有引用都被标记过了,黑色对象必定会经过灰色。如果有其他对象的引用指向了黑色,则无需扫描。

    • 浮动垃圾

    • 存活对象错误标记

      • 黑色对象新建立了到白色对象的引用
      • 全部灰色对象删除了到白色对象的直接或间接引用
    • 增量更新

      • CMS
    • 原始快照SATB

      • G1

标记-复制算法

内存碎片 执行效率

标记-清除算法

浪费一半内存

标记-整理算法

执行效率

回收堆内存

记忆集

新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。

新生代

  • Eden
  • Survivor1
  • Survivor2

老年代

垃圾收集器

G1

逻辑分区,物理不分区。

  • CSet

    面向整堆的任何部分组成回收集进行回收。

  • MixedGC

    哪块内存垃圾最多,回收效益最大就回收哪个。

  • Region

    • Humongous

      分配大对象。 超过Region的一半就是大对象。

    • TAMS

      用于解决在并发标记阶段用户线程新分配对象的问题。

    • RSet

      卡表,解决跨Region引用问题。

Shenandoah

ZGC

新生代收集器

  • Serial

    • STW
    • 单线程
  • ParNew

    • STW
    • 多线程
  • PS

    • STW
    • 多线程

老年代收集器

  • Serial Old

    • STW
    • 单线程
  • PSO

    • 多线程
    • STW
  • CMS

    • 多线程
    • 并发清除

跨代引用问题

CMS收集器

并发清除

  • 标记-清除算法
  • 内存碎片,导致提前触发Full GC
  • Serial Old收集会导致所有用户线程停顿

重新标记

  • STW

并发标记

  • 问题1:并发标记阶段会产生浮动垃圾
  • 问题2:会将和GCRoot关联着的对象错误标记
  • 耗时比较长

初始标记

  • STW但时间很短
  • 标记GCRoot直接关联的对象

堆内存

老年代

  • Major GC/Old GC

新生代

  • Minor GC/Yong GC

Full GC

G1收集器

筛选回收

  • 对象复制阶段是STW的
  • 多线程
  • 对Region的回收成本进行排序,制定回收计划,进行回收
  • 标记-整理算法

最终标记

  • 短暂的STW
  • 处理最后的少量SATB记录

并发标记

  • 进行对象可达性分析
  • 处理并发标记时SATB记录下有引用变动的对象

初始标记

  • STW
  • 标记GCRoot直接关联的对象
  • 移动TAMS

G1 compare CMS

优点

  • 可以制定最大停顿时间
  • 没有内存碎片问题
  • 并发标记的时候使用SATB,减少了重新标记阶段的耗时。
  • =6G到8G,处理大存更有优势

缺点

  • 每个Region都要维护卡表,内存占用多
  • 负载高,除了使用写后屏障维护卡表外,还需要使用写前屏障更新SATB记录,异步写。

关于JVM优秀的文章:

Java Hotspot G1 GC的一些关键技术

JAVA逃逸分析、栈上分配、标量替换、同步消除

伪共享问题

线上问题排查实践

CPU过高

  1. top 查看cpu使用率高的进程PID
  2. top -Hp pid 查看具体的线程 tid
  3. printf "%x\n" tid 转为tid为16进制
  4. jstack pid | grep tid -A 100 查看线程堆栈

内存使用过高

  1. top 查看cpu使用率高的进程PID
  2. jstat -gcutil pid 时间间隔查看gc情况
  3. 导出jmap文件
  4. 使用MAT分析jmap文件