JVM

内存模型

jvm模型将jvm的内存划分为几个区:java堆、java线程栈、元数据区、java本地方法栈、程序计数器、直接内存

  • java堆:jvm中最大的一个内存区域,查询运行时创建的对象和数组都放在这里;
  • java线程栈:线程的运行时内存,每个线程都有对应的线程栈。栈中存储的是栈帧,栈帧保存局部变量表、操作数栈、方法出口等信息, 运行时每次方法调用都会生产一个栈帧压入栈;
  • 元数据区,在jdk8之前叫方法区,是存放类信息、常量、编译的类字节码的区域;
  • 本地方法栈,主要是native调用时使用的;
  • 程序计数器,用于记录线程运行时的行号;
  • 直接内存
    • 直接内存不属于 JVM 运行时数据区,但常被 NIO(如ByteBuffer.allocateDirect())使用,其分配受系统总内存限制。当直接内存不足时,会触发 Full GC 间接回收(通过Cleaner机制),若回收后仍不足则抛出OutOfMemoryError: Direct buffer memory

象创建方法,对象的内存分配,对象的访问定位

  • 内存分配有两种方式:
    • 1、指针碰撞:规整的内存管理,通过一个指针标志已使用与未使用的内存,分配内存只需移动指针
    • 2、空闲列表

垃圾回收

java开发者不需要关心对象的存活周期,而是由jvm的一个**进行对象回收,在空闲或者内存不足时会触发对象存活扫描,并对死亡的对象进行回收

SafePoint

  • 又称为线程安全点,jvm在运行时,在某些时候需要对堆、栈的数据进行操作,比如gc、dump、偏向锁撤销、。如果线程一直在运行,操作数据可能会影响线程的运行,造成程序不稳定。如果jvm操作数据时不能影响线程,这个时间称为SafePoint。比较常见的是gc的Stop the world阶段,所有线程进入到SafePoint。
  • 实现方式:通过线程主动式中断的方式,线程在运行的时候会在某个适当的时间点检查是否需要进入到SafePoint。
  • 检查safepoint时机:在方法调用之前或之后、在循环迭代之前或之后、在对象分配之前或之后
  • 安全区域(safe region):有些线程此时并未执行,例如处于sleep或blocked状态的线程,就无法响应JVM的中断请求。当线程运行到安全区域时会将自己标识,在JVM准备进行GC时将视这些线程为安全的,不影响GC,当线程运行完毕要离开安全区域时,线程会检查JVM是否在枚举根节点,若是,则等待完成后再离开安全区域继续执行。

GC 判定方法

  • 根搜索方法,从gc-root的对象开始进行引用检查。gc root包括:栈中的变量、方法区的静态变量、方法区的常量引用、本地方法栈中的引用
  • 引用计数

GC 算法

  • 复制算法:将存活的对象复制到区域,不会产生内存碎片,速度略慢;ygc,年轻代算法,eden:survivor=8:1:1
  • 标记整理算法:先标记,然后将存活对象移动到一起;
  • 标记清理算法:先标记,然后将死亡对象删除。速度快、产生内存碎片。老年代算法
  • 优化思路?

JVM内存管理中的空间担保

是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代

老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较来决定是否进行Full GC来让老年代腾出更多空间。

垃圾回收器

  • Serial 垃圾收集器(单线程)年轻代垃圾收集器:单线程,执行期间stop the world,单CPU下效率高,适用于内存小对象相对少的客户端应用,基本已淘汰

  • ParNew 垃圾收集器(多线程)年轻代垃圾收集器:多线程,执行期间stop the world,多CPU下效率高,serial的多线程版本

  • Parallel Scavenge垃圾收集器 年轻代垃圾收集器 (多线程):追求CPU吞吐量,交互应用不友好。

    • 可调整CPU吞吐量的垃圾收集器(增加gc次数,但每次gc时间很短),通过配置调整MaxGCPauseMillis(最大gc时间)、GCTimeRatio(gc时间占比,即cpu吞吐量)使得JVM自动调整各个内存区域的大小,通过调整各个内存区域的大小控制gc时间,gc时间短则内存空间小gc频率高,gc时间长则空间内存大gc频率低。
  • Parallel Old 老年代垃圾收集器,采用"标记-整理"算法;-XX:+UseParallelGC -XX:+UseParallelOldGC

  • CMS 垃圾收集器 老年代代垃圾收集器:-XX:+UseConcMarkSweepGC

    • “标记-清除"算法,交互应用友好,以获取最短回收停顿时间为目标的收集器(追求低停顿),在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
    • 通过四个阶段进行垃圾回收,将标记阶段分成三步处理(1、标记gcroot直连;2、并发根据标记可达性分析;3、新对象标记),减少了停顿时间。最后并发清理死亡对象。
    • 标记清理算法会产生内存碎片。可通过配置jvm参数进行内存合并,-XX:+UseCMSCompactAtFullCollection -XX:+CMSFullGCsBeforeCompaction
    • 缺点:耗时、吃CPU;吞吐量较低(添加配置,在第一步之前,进行一次ygc,减少并发标记的时间);
  • G1垃圾回收器:

    • 对堆、元空间区进行region分区:保留分代概念,年轻代、老年代都切分成region,region有大有小2^n,默认把堆内存分成2048份
    • 垃圾跟踪:后台线程并发标记region,使用RSet跟踪外部引用,bitmap记录存活对象存活情况
    • 可控停顿:在用户设定的时间内(预期停顿时间),进行年轻代复制算法回收,老年代优先回收垃圾多的region
    • 整体上使用标记-整理算法,可以设置预期停顿时间(Pause Time)来控制垃圾收集时间
    • 运作分两条主线: 并发标记周期性触发或者当老年代占比 > IHOP阈值(默认45%);通过三阶段(初始标记→并发标记→最终标记)识别可回收Region
    • 垃圾回收
      • Young GC:Eden区满时触发(高频),仅回收年轻代,全程stw但是时间很短
      • Mixed GC:并发标记完成后触发,回收年轻代+高收益老年代
        • 初始标记(stw)>并发标记>最终标记(stw)>(筛选region(stw)+复制清除)
      • Full GC:空间不足时退化触发(需避免)
  • ZGC垃圾回收器:

    • 大堆垃圾回收器,Java11 中引入,Java15 正式使用。暂停时间控制在几毫秒以内,且暂停时间不受堆内存大小的影响,但代价是牺牲了一些吞吐量

    • 采用标记-复制算法,目前最大支持 16TB 的堆内存

      • 因为染色指针占用了指针的高四位
    • 流程:

      1. 初始标记(stw):从gcroot进行第一轮标记,设置指针M0或M1位(染色
      2. 并发标记:在1的基础上并发标记,被gc线程或者用户线程访问到的都会对指针染色
      3. 再标记(stw):补偿标记
        • 标记阶段存在两个地址视图M0M1,每次回收周期会轮换地址视图
        • 使用M0/M1零成本状态管理:通过指针就可以知道状态
        • 使用M0/M1完美 ABA 防护:不存在ABA问题,实现回收的内存可以直接分配给新对象,而不用消耗CPU重置内存地址指针
          • ABA问题:回收对象的染色指针并不会重置,如果直接分配给新对象则新对象指针的染色错误的导致垃圾判断错误
      4. 转移准备:扫描所有region收集要清理的Region,组成重分配集;新region预分配
      5. 初始转移(stw):处理gcroots存活对象,把重分配集中的存活gcroots复制到新Region,并维护一个转发表记录新旧对象的转向关系
      6. 并发转移:在并发执行重分配集中转移剩余的对象,也加使用转发表记录新旧对象的转向关系
      7. 并发重映射:根据转发表修复堆中存活对象的引用
        • 重映射清理这些旧引用的主要目的是为了不变慢,所以说这并不是很「迫切」。因此,ZGC把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉
    • 染色指针(Colored Pointer)

      • 使用ZGC的JVM,对象的存活信息保存在指向对象的指针中,对比其他要么使用数据结构维护要么使用对象头维护,都需要额外访问
      • 由于存活信息保存在指针上,回收时只需要:1、转移对象到新region;2、维护转向关系。而不需要等到更新堆中所有引用才能回收region
    • 读屏障(Read Barrier)

      读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。需要注意的是,仅“从堆中读取对象引用”才会触发这段代码。

      • 得益于染色指针的信息,ZGC收集器能仅从指针上就明确得知一个对象是否处于重分配集之中,并发重映射未完成时如果用户访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表将访问转发到新复制的对象上并同时修正该引用的值,使其直接指向新对象。
      • 称为指针的「自愈(Self-Healing)」能力,只有第一次访问旧对象会陷入转发,也就是只慢一次
    • 缺点:CPU吞吐下降,ZGC是整堆回收器每次处理的对象更多,更耗费CPU资源;第二,ZGC使用读屏障,读屏障操作需耗费额外的计算资源。

    • ZGC有多种GC触发机制

      • 阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时会导致部分线程阻塞。应当避免出现这种触发方式。日志关键字Allocation Stall
      • 基于分配速率的自适应算法:最主要的GC触发方式,其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”。自适应算法的详细理论参考彭成寒《新一代垃圾回收器ZGC设计与实现》一书中的内容。通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC。我们通过调整此参数解决了一些问题。日志中关键字是Allocation Rate
      • 基于固定时间间隔:通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。日志中关键字是Timer
      • 主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,我们的服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。 日志中关键字是Proactive
      • 预热规则:服务刚启动时出现,一般不需要关注。日志中关键字是Warmup
      • 外部触发:代码中显式调用**System.gc()**触发。 日志中关键字是System.gc()
      • 元数据分配触发:元数据区不足时导致,一般不需要关注。 日志中关键字是Metadata GC Threshold

常用的内存调试工具

jmap、jstack、jconsole、jhat

  • jmap可以查看jvm的内存
  • jstack可以查看线程的运行情况
  • jstat 可以查看各jvm的状态,比如类加载状态、gc状态
  • jconsole 监控性能分析工具,需要开放端口
  • jhat 可dump jvm

jvm优化

  • java程序做好gc日志记录,添加启动参数 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC
  • 做好程序性能监控:
    • 系统级的监控,cpu、内存、jvm情况;
    • dump分析,命令分析,阿里arthas;通过接口actuator监控;
    • 应用级监控,调用链路、耗时接口、耗时sql(日志监控、sleuth);
    • 数据库监控
  • 根据日志和性能监控数据定位修改程序
  • 使用专业工具通过不同的JVM参数进行压测并获得最佳配置。
  • 常见参数:-Xmx -Xms -Xss -Newsize -ratio -XX:MaxTenuringThreshold(新生代晋升阈值)
  • cms调整点-
    • 使用cms:XX:+UseConcMarkSweepGC()
    • 并发垃圾收集器的线程数 XX:ParallelGCThreads, -XX:ParallelCMSThreads
    • cms内存碎片整理: -XX:+UseCMSCompactAtFullCollection
    • 设置cms的gc阈值:-XX:+UseCMSInitiatingOccupancyOnly,-XX:CMSInitiatingOccupancyFraction
  • 永生带垃圾回收:-XX:+CMSClassUnloadingEnabled
  • FullGC之前进行一次minorGC: -XX:+ScavengeBeforeFullGC
  • cms第三阶段remark时先进行一次minorGC: -XX:+CMSScavengeBeforeRemark

jvm常见错误

  • OutOfMemoryError:Java heap space
  • OutOfMemoryError:PermGen space
  • OutOfMemoryError:Unable to create new native thread

逃逸分析与栈上分配(TLAB)

通过添加参数-XX:+DoEscapeAnalysis开启

  • 逃逸分析:首先逃逸分析是一种算法,这套算法在即时编译器(JIT)编译 Java 源代码时使用。通过逃逸分析算法,可以分析出某一个方法中的某个对象,是否会被其它方法或者线程访问到。如果某对象并不会被其它线程访问,则有可能在编译期间对其做一些深层次的优化,
  • 逃逸分析优化:
    • 栈上分配:JVM的一种优化技术,基本思想:“对那些线程私有对象(指的是不可能被其他线程访问的对象),将它们直接分配在栈上,而不是分配在堆上”,栈分配可以快速地在栈帧上创建和销毁对象,不用再将对象分配到堆空间,可以有效地减少 JVM 垃圾回收的压力
    • 分离对象或标量替换:栈上分配时,即时编译可以将对象打散,将对象替换为一个个很小的局部变量。将对象替换为一个个局部变量后,就可以非常方便的在栈上进行分配了。
      • 标量:不可分割的变量,一般基础数据类型
      • 聚合量:一个对象由多个标量(基础数据类型)组成,对象打散就是将对象拆解成n个标量,使用对象的地方使用这些标量替换
    • 同步锁消除(如:StringBuffer),如果JVM通过逃逸分析发现一个对象不会逃逸,则访问这个对象时可以不加同步锁。如果程序中使用了synchronized锁,则JVM会将synchronized锁消除。同步消除需要添加jvm参数-XX:+EliminateLocks