JVM

作者: 小丸子的呆地 | 来源:发表于2021-11-01 14:16 被阅读0次

    JVM组成

    jvm由类加载器+内存+执行引擎

    JVM内存区域

    • 堆 线程共享 存储对象
    • 虚拟机栈 线程私有,生命周期跟随线程
    • 栈帧 虚拟机栈中元素;局部变量表,操作数栈(做运算),动态链接(指向常量池中方法引用),方法出口
    • 本地方法栈 线程私有
    • 程序计数器 线程私有,无oom
    • 方法区 1.7之前是永久代、1.8之后是元空间
    • 直接内存

    对象内存布局OOP

    对象头 markword 8字节;klasspointer 4字节(开启指针压缩)/8字节;数组长度
    内部引用
    对齐

    为什么要对齐

    指针对齐指的是,对象的大小要是8字节的整倍数,如果不足会使用空位不足
    应该是为了寻址更方便,可能跟指针压缩有关,指针压缩的原理就是将原地址/8,使用更短的地址表示

    指针压缩原理

    64位地址分为堆的基地址+偏移量,当堆内存<32GB时候,在压缩过程中,把偏移量/8后保存到32位地址。在解压再把32位地址放大8倍,所以启用CompressedOops的条件是堆内存要在4GB*8=32GB以内。
    CompressedOops,可以让跑在64位平台下的JVM,不需要因为更宽的寻址,而付出Heap容量损失的代价。 不过它的实现方式是在机器码中植入压缩与解压指令,可能会给JVM增加额外的开销。

    java中有哪些类加载器

    BootstrapClassLoader启动类加载器(非java实现,无法获取,无法操作),负责加载<Java_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包
    ExtClassLoader扩展类加载器,负责加载<Java_Home>/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库 Launcher静态加载
    AppClassLoader系统类加载器,负责加载系统类路径-classpath或-Djava.class.path变量所指的目录下的类库 Launcher静态加载
    URLClassLoader,ext和app是他的子类,他实现了findClass方法,可以根据一组url找到对应的文件
    ThreadContextClassLoader线程上下文类加载器,这不是一个真正的类加载器,而是在项目启动阶段设置到Thread中的,子线程会继承父线程线程上下文类加载器

    java的类加载机制

    全盘负责 谁触发的加载,由谁的类加载器质性加载
    双亲委派 向上查找,向下加载;加载类时,先看自己有没有,再看父加载器有没有,都没有,则从父加载器开始向下加载
    缓存机制 加载一次之后的类将会被缓存在类加载器中

    java类加载过程

    装载 将class文件找到,并加载方法区(1.8之后放在元空间中的 klassMetaSpace)
    验证 验证class文件的格式、魔数、语义、语法
    准备 为class的静态变量分配内存,并附默认值,int=0 boolean=false
    解析 替换常量池中的符号引用为直接引用
    初始化 为class的静态变量赋值,执行static块方法
    使用
    销毁

    破坏双亲委派机制

    指的是ThreadContextClassLoader可以在任何场景下被获取并执行类加载,本质上其实破坏的是全盘负责机制,双亲委派还是完整的

    java SPI机制,指的是java定义了一些核心接口比如JDBC,具体实现由各大厂商进行实现,厂商将驱动启动类配置在一个文件中,jvm启动阶段会读取此文件执行加载,而负责加载的类在核心类路径上,根据全盘负责机制,应该有启动类加载器进行加载,那这肯定是加载不到的,所以会使用线程上线文类加载器
    spring也使用的是线程上下文类加载器
    线程上下文类加载器在启动阶段被设置为appclassloader

    双亲委派的好处

    1.唯一性,类只会被加载一次
    2.安全性,核心类库不会被篡改
    3.隔离性,不同自定义类加载器的类不共享,但是可以共享父加载器的,参考tomcat实现隔离不同webwork

    如何确定是否应该回收对象

    1. 引用计数法 每个对象维护一个数字代表被多少对象引用,循环引用场景有问题
    2. 可达性分析 从gcroot出发,能访问到的对象代表不是垃圾

    java中引用类型

    强软弱虚引用,软引用在内存不足时会被回收,弱引用在GC时会被回收,虚引用用来管理对外内存

    TLAB

    Thread Local Allocation Buffer 防止线程竞争内存地址,所以给每个线程分配一块buffer

    PLAB

    Promotion Local Allocation Buffers 年轻代对象晋升时,申请的一块老年代地址

    GCRoot

    虚拟机栈和本地方法栈中的局部变量持有的对象,方法区静态变量和常量持有的对象

    GC算法

    1. 标记-复制
    2. 标记-清除
    3. 标记-整理

    三色标记法

    完全扫描的对象标记为黑色,部分扫描的对象标记为灰色,未扫描的对象为白色;黑色对象在如果发生引用变化,会被标记为灰色

    垃圾收集器

    • Serial 年轻代串行收集器 复制
    • SerialOld 老年代的串行收集器 整理
    • PS 年轻代并行收集器 复制 吞吐量优先,可以设置吞吐量、最大停顿时间,开启自适应自动配置分区大小,1.8默认
    • PO 老年代并行收集器 整理
    • PN 年轻代并行收集器 复制 配合CMS
    • CMS 老年代并发收集器 清除 追求最小停顿,可以与用户线程并发执行,cup敏感,浮动垃圾,内存碎片
    • G1 可以回收年轻代和老年代,复制/整理 追求最小停顿和吞吐量的平衡,弱化分代,使用分区,内存划分为2000个区域,每个区域可能是s、e、o、p,可以开启自适应自动配置分区大小,1.9默认
    • ZGC 彻底摒弃分代回收 1.11

    对象晋升老年代的几种情况

    1. 分配担保机制
    2. 达到年龄
    3. 大对象直接分配 需要配置 默认不开启
    4. 动态年龄 当s区小于某一个年龄的对象大小超过s区一半,大于等于此年龄的对象直接进入老年代

    STW

    stop the world在gc过程中,为了保证某些一致性,要强制暂停所有用户线程

    安全点和安全区域

    安全点和安全区域指代码中一些引用不会发生改变的地方,当线程运行到这类位置时,堆对象状态是确定一致的,JVM可以安全地进行操作,比如return之前、调用方法之后、抛出异常之前、循环的末尾;阻塞中(安全区域);
    一些操作会触发线程执行到安全点后停顿,GC,偏向锁接触,JIT优化等

    CMS回收过程

    1.初始标记 会导致STW,标记直接被GCRoots引用的对象
    2.并发标记 与用户线程并发执行,使用三色标记法标记对象
    3.并发预清理 清理用户线程并发过程中产生的对象引用变化,对象引用变化会触发写屏障,将对应的cardtable位置标记为dirty区域,
    4.可中断的并发预清理 重复执行步骤3,尽可能的减少并发产生的脏页,预期一次YGC,减少年轻代对象,可以通过参数配置,一般会在5s左右
    5.重新标记 这个阶段是 STW 的,因为并发阶段引用关系会发生变化,所以要重新遍历一遍新生代对象、Gc Roots、卡表、ModUnionTable等,来修正标记。
    6.并发清除 清除垃圾对象
    7.线程重置

    CMS的问题

    • 浮动垃圾 在并发标记和并发预清理阶段会产生浮动垃圾,就是本身已经被标记为黑色的对象,在并发标记阶段被用户线程修改为没有引用,本质上应该是白色的,但是由于无法触达,所以一直是黑色的,会在下一次CMS被回收,浮动垃圾过多也会触发fullgc,参数配置CMS触发机制目前是75%
    • 内存碎片 CMS使用清除算法,被清除的地方形成一个一个不连续空洞,这就是内存碎片,内存碎片会影响大对象的内存分配,严重时会触发SerialOld,强制进行内存整理,这就是fullgc
    • promotion failed 进行ygc时,年轻代放不下,通过分配担保机制,检查老年代是否有足够空间,如果没有则抛出promotion failed,此时old区还没有开始CMS,降级为Serail OldGC,主要是内存随碎片导致的
    • concurrent mode failure 在CMS执行过程中,出现对象晋升失败,抛出concurrent mode failure 降级为Serail OldGC

    CMS中的Incremental update

    cms通过incremental update write barrie 在并发标记期间,将发生引用变化的黑色对象编程灰色对象。

    CMS中的cardtable

    Hospot将老年代内存按512字节划分为一个一个card,而卡表就是一个字节数组,数组中每一个元素对应一个card;CARD_TABLE[address>>9]=DIRTY
    YGC需要卡表记录老年代对象对年轻代对象的引用,用以规避全量扫描老年代对象;
    CMS用卡表记录老年代对象在并发标记过程中对象引用的变化

    CMS中的ModUnionTable

    ModUnionTable为了补充CMS和YGC同时发生时对cardtable的操作,YGC利用卡表记录老年代对年轻代对象的引用,YGC扫描到这个card之后会重置,这样CMS标记的dirty会丢失,所以使用ModUnionTable

    触发fullgc的情况

    • 担保失败promotion failed
    • CMS期间 concurrent mode failure

    G1垃圾收集器

    分区又分代,将内存等分成2000多个region,每个region可以是Eden、S、Old、超大对象区(大小超过region的一半)
    younggc和mixedgc模式
    吞吐量和最小停顿权衡

    Cset

    一组需要被回收的region集合,YGC只包含e和s,MixedGC包含e、s和回收价值最大的一些o区

    Rset

    每个region维护一个remmberset,存储引用了本region对象被其他region区对象的引用
    points-into 和 points-out ;CMS的卡表是out,G1的rset是in

    SATB

    标记期间,一旦引用关系发生变化,将此引用记录下来,就认为他是活的,Snapshot-at-beginning,从标记开始,认为可达的对象都是活的,即使并发过程中出现引用变化,产生浮动垃圾;
    标记开始时生成两个指针,一个pre一个next,这两个指针区间的对象是标记期间创建的,视为活着的对象

    G1回收过程

    YGC
    stw 从gcroots+rset出发扫描整个年轻代,此处是多线程并行完成,将存活对象copy到其他s区,清除垃圾
    存活对象会被复制或移动到一个或者更多的survivor区域。如果这个阈值被满足了,其中的一些对象就会晋升到老年代中。
    这里会出现STW的暂停。Eden和survivor的大小会被计算来供下一个年轻代的GC所使用。整体的信息会被保存起来来计算大小。暂停时间目标的这个事就会被考虑进来。
    这种方式使得我们很容易的来调整区域的大小,使得它们根据实际需要变得更大或者更小。

    MixedGG
    global concurrent mark 全局并发标记

    • 初始标记 stw 标记gcroots 使用外部bitmap,不用对象头 这里复用了ygc的stw
    • 并发阶段 从gcroots和rset出发 三色标记,SATB开始工作
    • 最终标记 stw 处理SATB里的引用
    • 清理 stw 清理为空region 置为freelist
    • 复制 stw 将存活对象复制到其他的区域

    G1主要运行模式

    YGC
    MixedGC
    fullGC

    YGC何时发生

    新生代不够用了

    MisedGC何时发生

    G1HeapWastePercent:在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。
    G1MixedGCLiveThresholdPercent:old generation region中的存活对象的占比,只有在此参数之下,才会被选入CSet。
    G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数。
    G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多old generation region数量。

    G1何时发生fullGC

    当晋升失败、疏散失败、大对象分配失败、Evac失败时,有可能触发Full GC,在JDK10之前,Full GC是串行的,JEP 307: Parallel Full GC for G1之后引入了并行Full GC。

    G1其他参数

    设置region大小
    设置个
    手机目标时间 默认200ms
    新生代最小值 5%
    新生代最大值 60%
    并行GC线程数
    并发标记阶段并行线程数

    JIT是什么

    JIT即使编译,是JVM虚拟机解释引擎的一部分,我们知道java是一种先编译后解释的语言,在运行阶段,由JVM将class文件中的程序解释成机器语言来执行,每次解释还是会影响效率,JIT就是将一些热点代码直接翻译成机器语言后进行缓存,下次执行这个缓存就好了,在翻译和缓存过程中,还可以对代码进行一些动态的优化,比如逃逸分析、标量替换、锁消除、锁粗化、方法内联等

    JIT的client和server

    jit有client和server模式,client模式启动快,针对少量代码进行即使编译,适合客户端程序;server模式会针对大量大吗进行优化,启动慢,适合做服务器程序

    如何确定那些代码需要编译

    当 JVM 执行一个 Java 方法,它会检查方法调用计数器和回边计数器的总和,以决定这个方法是否有资格被编译。如果有,则这个方法将排队等待编译。这种编译形式并没有一个官方的名字,但是一般被叫做标准编译。
    client模式是1500 server模式是10000

    何时编译

    异步,允许边执行边编译
    栈上替换OSR,应对长循环或者死循环的编译,编译完成之后,下一次循环直接替换为编译好的机器码

    JIT还有哪些功能

    逃逸分析、标量替换 对象栈上分配
    锁消除 对于永远不会并发访问的区域去除synconized
    锁粗化 对于循环内部的synconized可能会迁移到外部
    方法内联 将方法内调用的其他方法直接纳入本方法中,减少出战入栈
    profile优化 如果这这方法一直只进行一个分支的判断,虚拟机就会推断,程序不会走另一个分支,即时编译器就会省略掉一个,只对一个分支进行编译为机器码。

    JVM调优经验

    • cv优化过程
      cv项目的oldgc优化,cv的内存使用本身具有一定的特色,在简历查询场景,组装一份简历会产生大量的临时对象,简历对象本身存在一些文本信息,一份简历的平均长度在5000+char左右,在批量查询场景中,一组简历被返回,如果有200份简历那么这个数据大概会有2m左右的一个集合对象,并且接口响应较长,在一个长时间中内存会持有这些简历对象,并且cv的qps很高,峰值会有1w的qps。cv项目尽管已经加到15个实例,仍然每天伴会有几次cms进行执行

    第一次 优化cv的jvm的目的,其实就是降低cms执行,cms尽管是低停顿收集器,但是回收过程中不免进行stw导致一些线程超时
    针对cv的优化从以下几点入手,
    1.使用宽表缓存,减少组装简历过程中产生的临时对象
    2.限制接口size数量
    3.调整jvm参数,策略是适当调大年轻代,也可以减少ygc的产生,降低因为年龄达到进入老年代,调整s区比例,减少因s区不足进入老年代的场景,这个过程是一个慢慢调试的过程
    第一次优化使cv已经cms次数降低到了1周1次

    第二次 优化cv是为了进一步降低服务的停顿
    1.发现日志中有大量的安全点stw日志,通过日志发现很多偏向锁的释放导致进入的stw,删除代码中的一些无用的synconized,这里其实无伤大雅,大部分都是框架产生的
    2.cms的最终标记时间user time过长,了解到公司服务器最多允许使用3核,将cms并发度调整到了3核,之前是15,频繁切换线程确实会影响

    第三次 优化cv是为了解决concurrent mode failure问题
    配置了每执行5次cms 执行一次整理算法,现阶段的cv已经能做到1周1次cms

    第四次 优化cv是为了进一步减少服务压力
    将简历缓存封装成轻量级接口,直接织入业务端代码,以前的模式是RPC-》缓存,改成缓存-》RPC,减少服务压力

    • user平台优化
      user平台也十分具有特点,那就是qps超高,峰值能达到2w
      正常情况下,user平台虽然qps很高,但是没有响应时间很长的那种接口,经过调整jvm参数,对象会很快被回收掉,每天仍然会有1次cms
      通过排查内存快照,定位到某个内部定时任务会生成超大对象集合,处理过之后,old区每日几乎不会有什么变化

    相关文章

      网友评论

          本文标题:JVM

          本文链接:https://www.haomeiwen.com/subject/toakgltx.html