JVM基本概念(二)

87 阅读9分钟

上一篇讲到java的类装载过程以及如何解析classFile,本篇文章重点讲解Runtime Data Area 运行时数据区

image.png

程序计数器

存储的是指令,线程私有的。记录程序需要执行的指令。

  • java指令: 记录虚拟机字节码指定地址
  • native指令:undefined

本地方法栈

线程私有的

虚拟机栈 stack

线程私有的,每个方法都对应一个虚拟机栈,方法中定义的变量即局部变量

方法区

多线程共享的,方法区是jvm规范规定的一块空间,只是逻辑划分。jdk1.7中方法区的实现称为永久代, jdk1.8以后方法区的实现称为元空间。方法区存储的是每个类的结构信息。eg:运行时常量池。 1.6 永久代 常量池存放在方法区中 1.7 永久代 常量池存放在堆中 1.8 元空间 64位的jvm默认元空间为21MB,使用的是本地内存。如果超过默认值也会触发full gc, full gc 回收后如果空间依然不足,元空间会动态扩容。理论上使用本地内存可以扩容到物理内存大小。

堆空间

程序中创建的对象大部分存放在堆空间。

重点讨论堆空间,讨论点如下

  • java中如何创建对象 new 反射 clone 序列化
  • 创建对象放哪儿?大部分情况放在堆中,以下情况不同
    2.1 没有方法逃逸的对象直接栈上分配
    test对象作用域仅限于方法内,test对象并没有逃逸到方法外,因此new User直接在栈上分配。 这种优化方式称为逃逸分析
public void demo(){
    Test test = new Test();
    test.test();
}

2.2 没有线程逃逸,可以进行syn同步擦除。

public void demo(){
    StringBuffer sb = new StringBuffer();
    synchronized (this){
        sb.append("advise");
    }
}

2.3 当栈中的对象进行栈上分配后,如果对象中的属性有很多,进行标量替换优化,替换的聚合量
2.4 JVM优化中关于逃逸分析的参数
-XX:+DoEscapeAnalysit: 开启逃逸分析
-XX:+PrintEscapeAnalysit: 如果开启了逃逸分析查看逃逸分析结果
-XX:+EliminateAllocations: 开启标量替换
-XX:+EliminateLocals: 开启同步擦除

  • 创建出来的对象如何在内存中存储
    对象的组成部分:
    对象结构:对象头+示例数据+对齐填充
    对象头:markword + kclass + [数组长度] (如果是数组对象时才有)
    实例数据:相同宽度的数据放到一起
    对齐填充:8字节的整数倍

image.png

对象开始创建时是无锁状态,在markword结构中使用 001 标识 第一位 0 表示偏向锁位 后两位01表示锁标志位 对象使用时,如果竞争不激烈 修改偏向锁位标识 升级为偏向锁 1 01
当竞争开始激烈时 偏向锁升级为轻量级锁 此时只有锁标志位标识锁状态 00
当参与竞争的线程达到了CPU核数的1/2或者轻量级锁自旋达到阈值10,则升级位重量级锁 标志位为 10
当对象已不需要使用 需要gc释放时 锁标志位 11 表示GC可以回收

  • 如何分配空间?
    • 指针碰撞法
    • 空闲列表法
    • 多线程安全性 TLAB ---- Thread Local Location Buffer (start、top、end) -XX:UseTLAB 默认情况下占用了eden空间的 1% 记录 top start end 的位置 -XX:TLABWasteTargetPercent 查看TLAB占用Eden区的百分比 如果需要分配的内存大小已经超过了TLAB规定大小 此时jvm不会采用TLAB分配好的buffer,此时使用CAS竞争的方式申请堆空间

image.png

  • 栈与堆的关系建立 栈中的变量与堆中的对象建立引用关系的方式
    • 直接引用方式
      一次指向:当对象发生变化时,需要重新指向
    • 句柄访问方式 两次指向,通过句柄池维护指向关系,对象发生变化时无需重新指向

GC垃圾回收算法

  • 引用计数法
    • 对象创建时初始值为0 每当有一个引用指向栈内对象地址则引用计数+1 当方法出栈,引用不指向对象则引用计数-1,当某个对象的引用计数值<1 时 代表可回收。 但存在循环引用的问题
  • 可达性分析算法
    • 在定义好的 GC ROOTS节点 向下查找节点,可触及的节点作为活跃对象,不可达的节点则可回收
      • 栈帧本地变量表
      • 方法区常量池
      • 方法区静态属性
      • 活跃线程引用对象
      • 本地方法栈JNI对象
  • 标记清除算法
    • 根据颜色区分标记出 存活对象 未使用空间 可回收空间。 使用简单,但是会产生内存碎片
  • 复制算法
    • 将内存空间分为两部分,GC识别后将 一部分存活对象移入另一半未使用的空间,然后释放这一部分空间。解决了内存碎片问题,但是利用率不高。
  • 标记整理算法
    • 没有内存碎片,利用率高,算法相对复杂。回收的同时进行整理

堆空间分代

JVM内存中堆空间是GC主要工作区域,分为新生代和老年代

image.png

新建的对象,首先在Eden区中开辟空间,当Eden区空间不足时触发一次minGC。 - 将Eden区和from中存活的对象移入到to区,对象 age + 1 - 然后将from与to区交换 from变为to区 to变为from区 - 将eden区和原来的from区回收 - 当to区空间不足 或者 对象的 age>=15 将其移动到老年代 - 当老年代空间不足时 则触发MaxGC,老年代进行垃圾回收

GC垃圾回收器

image.png

Serial 是一个新生代串行回收器,Serial Old 是一个老年代串行回收器 Parnew 、Parallel Scavenge 是新生代并行回收器,Parallel Old是老年代并行回收器 CMS 是一个并发回收器

垃圾回收组合

  • Serial(复制算法) + Serial Old(标记整理算法) GC回收时,暂停所有用户线程。GC线程是串行回收
  • Parallel Scavenge/ParNew (复制算法) + Serial Old(标记整理算法) GC回收时,暂停所有用户线程。GC线程并行回收。
  • CMS回收器 (针对老年代回收)
    • 初始标记:stop-the-world,标记GC ROOTS直接关联的对象
      • 工作线程暂停对外服务,只标记GCroots直接关联的对象,停顿非常短暂
    • 并发标记:并发追溯标记,程序不会停顿
      • 并发的对GC直接关联的对象向下查找并标记,由于此时GC线程与业务线程同时工作,GC标记后可能被业务线程修改,因此需要重新标记。
    • 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
      • 纠正并发标记阶段可能被业务线程使用的对象,但仍可能存在错标与漏标情况
    • 并发清理:清理垃圾对象,程序不会停顿
    • 并发重置:重置CMS收集器的数据结构
    • JDK8默认是 Parallel Scavenge + Parallel Old
    • JDK9 出现 G1 回收器 模糊了新生代和老年代的概念
    • JDK14 废弃了CMS
    • CMS优点在于压缩了GC的停顿时间短,缺点在于消耗cpu资源,因此使用CMS最少用4核。CMS还会产生浮动垃圾,为了解决该情况,老年代需要设置一个阈值避免空间不足,jdk1.6设置阈值为 92%,当预留的8%不够存储浮动垃圾,那么CMS将退化为Serial Old。 CMS 因使用标记清除算法还会产生碎片。
    • -XX:CMSFullGCsBeforeCompaction=5 设置CMS进行5次FullGC后使用标记整理算法
  • ZGC 回收器
    • 三色标记算法
      • 从GC ROOTS开始查找,将对象分为 白、黑、灰三种颜色。白:未访问;黑:对象已访问,引用也已访问;灰:对象已访问,但对象中属性未访问
      • 错标:当GC标记一个对象变黑色,业务线程同时又把该对象重新指向一个对象时,由于是黑色GC认为可以回收 那么这样会对程序造成严重影响,因此 CMS采用增量更新的方式解决,在重新指向的时候 利用写屏障 修改该对象 标记为灰色
    • 针对于错标情况 不同垃圾回收器策略不同
      • CMS 采用 写屏障 + 增量更新 --- 三色标记算法
      • G1 采用 写屏障 + SATB 快照读 --- 三色标记算法
      • ZGC 采用 读屏障 --- 颜色指针算法
  • G1 回收器
    • G1采用逻辑分代法,将jvm内存分为多个大小相等的region
    • jdk9以后默认垃圾回收器
    • 整体上使用标记整理算法,局部使用复制算法
    • 每个region在1-32M之间 -XX:G1HeapRegionSize= n 指定区大小。
    • 总的 Region 个数最大可以存在 2048 个,即heap最大能够达到 32M × 2048 = 64G
    • 0.5 < obj < 1,那么放到old区,old 标记为H,1 < obj < n,连续的n个region,作为H
    • 概念 - rset: 每个region都有一个叫rset的小区,它代表了其他region引用了当前region对象的记录 - cset: 本次GC需要清理的Region集合
    • mixGC的过程
      • 初始标记:标记处GCRoot对象,以及GCRoot所在的Region (RootRegion) ---短暂停顿
      • Root Region Scanning:扫描整个old的Region
      • 并发标记:并发追溯标记,进行GCRootsTracing的过程
      • 最终标记:修正并发标记期间,因程序运行导致标记发生变化的一部分对象 ---短暂停顿
      • 清理回收:根据时间来进行价值最大化的回收,重置rset --- 短暂停顿
  • G1 相关参数配置
    • -XX:+UseG1GC: 设置使用G1垃圾回收器
    • -XX:MaxGCPauseMillis = n 最大GC停顿时间,毫秒值
    • -XX:InitatingHeapOccupancyPercent = n 当堆空间占用到 n 兆时就触发
    • -XX:GoncGCThreads = n 并发GC使用的线程数
    • -XX:G1ReserverPercent = n 设置作为空闲空间的预留内存百分比

ZGC回收器

  • 目标
    • jdk11 推出的垃圾回收器,旨在降低延时
    • 支持TB级别空间,最大停顿时间控制在10ms
    • 大约15%吞吐量的损耗
  • 特点
    • 读屏障
    • 着色指针算法
    • 单代
    • 采用局部压缩方式
    • 基于region
    • 内存即用
    • NUMA-aware
  • 颜色指针算法
    • ZGC只支持64位,后42位存储的是对象信息 2^42 = 4TB,unused借2位即可 2^44 = 16TB
    • 读屏障 image.png
  • ZGC回收过程
    • 并发标记
      • 将使用过的对象从初始的remapped状态修改为 marked状态
    • 并发预备重分配
      • 统计GC需要回收的Region集合 RelocationSet (重分配集)
    • 并发重分配
      • 核心阶段:将集合中需要回收的region里不需要回收的对象换到一个新的region中,并且维护forward table, 这forward table 记录了对象的迁移过程信息。由于GC线程与业务线程并发执行,当GC线程将对象转移时修改了对象的marked状态 从 0 改为 1,那么业务线程访问时如果 marked状态为1 则访问forward table 根据记录的新地址访问对象,这个过程称为 "自愈"
    • 并发重映射
      • 修正堆中指向重分配对象的旧的引用
  • ZGC触发机制
    • 阻塞内存分配触发请求
    • 分配速率自适应算法
    • 固定时间间隔
    • 主动触发
    • 外部触发
  • ZGC 相关参数配置
    • -XX:ReservedCodeCacheSize -XX:InitialCodeCacheSize JIT时的代码缓存 一般64~128M
    • -XX:+UnlockExperimentalVMOptions 解锁jvm额外参数
    • -XX:ConcGCThreads 并发线程数 默认值为总核数的 12.5%
    • -XX:ZCollectionInterval 回收间隔时间
    • -XX:ZAlloocationSpikeTolerance 回收自适应修正参数 默认为2 数越大触发时机越早
    • -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive 是否主动进行ZGC回收