JVM调优,调的是什么?目的是什么?

Posted 寂寞旅行

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM调优,调的是什么?目的是什么?相关的知识,希望对你有一定的参考价值。

文章目录


前言

jvm是java语言可以跨平台运行的基础

jvm 是什么,他是一个可以运行字节码文件的机器;
调优调的是什么?
调整的jvm内存模型中的参数,以及GC垃圾回收器的选择,甚至可以选择使用哪种垃圾回收算法;
那么调优的目的是什么?
调优调的是: 减少GC 的次数,以及GC的STW 时间,这里的GC 大多数指FULL GC

当然minor gc 可能时间会非常长,不过这个情况较为特殊,之后文中会说;


一、jvm是如何运行代码的?

大概步骤为

  • java源文件编译为.class文件
  • .class文件被各种平台版本的jvm编译为本地机器码(字节码)
  • 类加载机制将这些字节码加载后,放到运行时数据中(内存模型中)
  • 字节码执行器,通过内存中的入栈出栈执行这些字节码

二、jvm的内存模型

1 整体内存模型结构图

● 堆: 存放对象实例 常量池
● 方法区: 方法信息头,静态变量,常量
● 本地方法栈: native 保留方法运行时候的内存空间
● 程序计数器: 存放执行字节码的行号指示器 (字节码指令的地址)
● java虚拟机栈: 对象的引用,指针,八大基本类型 局部变量

2 堆中的年代区域划分

我们只需要关注我们大多数调整的就好了,那就是年轻代 ,老年代

  • 默认新生代 老年代比例 1:2
  • 默认新生代中 eden 和 s (s0 s1) 区域的比例为 8:1:1

3 对象在内存模型中是如何流转的?

  1. 首先new 一个对象的时候,对象一般会在堆中开辟一块内存存储
  2. 然后这个线程结束,这个对象不再被引用之后,就会被纳入到年轻代,当其中的一块s区域满了,发生轻gc,也就是会发生stw;
  3. 多种情况会导致对象进入老年代: 例如 一个对象的分代年龄大于15; 对象的整体大小大于s0/s1区域的50% ,不会放入s区,直接进入老年代 等等;
  4. 分代年龄: 在s区域中的对象,每经过一次轻gc,分代年龄加1;
  5. 轻gc处理对象的方式: 一部分被加入到老年代,大多数都是从一个s区域复制到另个一s区域(标记复制算法)
  6. s区域中的两块区域,总有一块是空的;

4 什么是FULL GC,STW? 为什么会发生FULL GC?

  1. 与轻gc 类似,FULL GC 是发生在老年代的gc
  2. stw 是 stop the word,所有用户线程都会停止,现象例如: 你在淘宝添加一个物品到购物车,卡住了;
  3. 发生full gc 与轻gc类似,也就是老年代空间被填满了,必须进行垃圾回收,将无用对象全部移除,释放空间
  4. 如果释放的空间不够,程序仍然在申请大量的内存,那么此时会发生 oom;
  5. full gc 一般采用 可达性算法回收(自行百度);

5 要调优,首先要知道有哪些垃圾收集器及哪些算法

  1. 常用垃圾回收算法汇总
  2. 常用的垃圾回收器
    ● Serial 是一个新生代收集器,基于标记-复制算法实现
    ● Serial Old 是一个老年代收集器,基于标记-整理算法实现
    ● 两者都是单线程收集,需要「Stop The World」
    ● Parallel Scavenge 收集器是一款新生代收集器,基于标记-复制算法实现
    ● Parallel Old 收集器是一款老年代收集器,基于标记-整理算法实现
    ● 两者都支持多线程并行收集,需要「Stop The World」
    ● CMS(Concurrent Mark Sweep)是一个老年代收集器,基于标记-清除算法实现
    ● G1 是一款主要面向服务端应用的垃圾收集器。
    ● 从整体来看是基于「标记-整理」算法实现的收集器,但从局部(两个 Region 之间)上看又是基于「标记-复制」算法实现
    ● G1 即是新生代又是老年代收集器",无需组合其他收集器。
    可以看到垃圾回收器一般不采用单独的一个算法实现
    JDK9 前,我们会在 CMS 和 G1 间选择,对于大概 4GB 到 6GB 以下的堆内存,CMS 一般能处理得比较好,而对于更大的堆内存,可重点考察一下 G1
  3. 其中G1比较重要,我们详细说下相关参数
 -XX:+UseG1GC
启用 G1 垃圾回收器
-XX:InitiatingHeapOccupancyPercent=<45>
当整个 Java 堆的占用达到参数的值时,开始并发标记阶段
-XX:MaxGCPauseMillis=200
G1 暂停时间目标 ( >0 的毫秒数)
-XX:NewRatio=n
新生代与老生代 (new/old generation) 的大小比例 (Ratio). 默认值为 2
-XX:SurvivorRatio=n
Eden/Survivor 空间大小的比例 (Ratio). 默认值为 8
-XX:MaxTenuringThreshold=n
提升年老代的最大临界值 (tenuring threshold). 默认值为 15
-XX:ParallelGCThreads=n
设置垃圾收集器在并行阶段使用的线程数,默认值随 JVM 运行的平台不同而不同
-XX:ConcGCThreads=n
并发垃圾收集器使用的线程数量。 默认值随 JVM 运行的平台不同而不同
-XX:G1ReservePercent=n
作为空闲空间的预留内存百分比,以降低目标空间溢出的风险。默认值是 10%
-XX:G1HeapRegionSize=n
指定每个 Region 的大小。默认值将根据 heap size 算出最优解。最小值为 1Mb, 最大值为 32Mb
  1. 我们能够调整的参数有哪些(常用参数汇总)
//常见配置汇总 
//堆设置 
-Xms:初始堆大小 
-Xmx:最大堆大小 
-XX:NewSize=n:设置年轻代大小 
-XX:NewRatio=n:设置年轻代和年老代的比值.如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值.注意Survivor区有两个.如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 
-XX:MaxPermSize=n:设置持久代大小
-XX:MetaspaceSize:设置元空间大小
-XX:MaxMetaspaceSize:设置元空间最大大小
-Xss128k: 设置每个线程的堆栈大小。
//收集器设置 
-XX:+UseSerialGC:设置串行收集器 
-XX:+UseParallelGC:设置并行收集器 
-XX:+UseParalledlOldGC:设置并行年老代收集器 
-XX:+UseConcMarkSweepGC:设置并发收集器
//垃圾回收统计信息 
-XX:+PrintGC 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-Xloggc:filename
//并行收集器设置 
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数.并行收集//线程数. 
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比.公式为1/(1+n)
//并发收集器设置 
-XX:+CMSIncrementalMode:设置为增量模式.适用于单CPU情况. 
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数.并行收集线程数.
-XX:+CMSParallelRemarkEnabled:并发清理

6 调优不是盲目的,要有依据,几款内存诊断工具

  1. jmap jmap使用
  2. jstack jstack使用
  3. 阿里arhtas
    百度即可;
    下载,运行 arthas
    ● 线程占用过高原因: thread pid
    ● 死锁信息: thread -b
    ● 查看当前代码: jad 文件名称

7 结束语

通过看此文章不会让你知道具体怎么调优,但是应该知道如果调优的大体学习路线,具体怎么调优,我是经过自己的学习外加 观看 诸葛老师的课程 传送门
没错,我也是一个毕业于哔哩哔哩的人;

8 出个问题,也是课程中的

调优的目的是为了减少full gc 的次数和时间,尽量通过minor gc处理,这样就可以了,没有问题哈;
问题如下:
我现在有 8核64G的一个服务器,上面运行的程序不是BAT那种级别的,但是也不小;
问题: 堆内存可以设置很大,为了尽量通过轻gc解决,是不是年轻代设置的越大越好?

答案是否定的,因为当年轻代足够大之后,发生minor gc 的时候,也需要stw ,这个时间也会非常久,所以还是需要做适合的大小配置;这也就是我们为什么要调优的原因; 合适的大小才最重要!!!

9 设置项目的jvm参数

命令

1 查看java进程 jsp

2 设置jvm参数 -Xms512m -Xmx512m

3 查看当前进程的具体内存分配 jmap -heap 22096

堆的总大小为512m
NewRatio 新生代与老年代的比例为1:2 则: 新生代为 170m 老年代为 340m
由于eden: s0 : s1 等于 8:1:1 所以 s0=s1 大概等于17m左右,剩余为eden区域大小 大概为134m左右
哎…但是出现的结果不一致 s0 =s1 大概为 21m eden区域大概为 129m

有大神知道为啥嘛? 与正常结果差4

  • 再次加大堆大小为 1024m 和 2048m
    结果分别为:
    eden: s0 : s1 为271:34:34 与正常结果s0差 8
    eden: s0 : s1 为512:85:85 与正常结果s0差16
  • 设置jvm参数 相互冲突
  • 设置堆大小为 1024m 且设置新生代大小为500m
-Xms1024m -Xmx1024m -XX:NewSize=500m

1 在不考虑新生代大小的情况下,正常来说结果应为 eden: s0 : s1 为271:34:34 与正常结果s0差 8
2 由于设置了新生代大小,所有新生代为固定大小 500m
3 那么 eden: s0 : s1 仍为8:1:1,所以大小应该为 400:50:50
4 由于与结果相差8 ,所以大概应该为386:58:58
上图验证结果

我真的是,无法把握它的准确大小了


总结

经过此番学习,对于jvm内存模型,代码运行,对象流转,内存分配有了更高层次认知,对于jvm调优,为什么要调优有清晰的认知,继续学习这篇文章;
改jvm参数不难,难的是你要知道参数的大小的计算; 要明白参数大小的由来

JVM调优 - 理解GC

关于JVM调优系列

Hey Guys,我们将开启这个JVM的调优系列,那么什么是JVM调优呢?其实说起来我们能调的东西也不多,因为这个不是让我们去修改JVM的源码或者修改其GC算法,这里主要是去针对生产出现的OOM、卡顿、CPU飙升、假死等情况去排查处理,以及减少排查问题、GC带来的过多性能消耗影响到生产应用的运行。


这个系列我们依然先从基础入手,然后针对各种生产问题,去看看如何排查、定位、解决。


GC

GC就是垃圾回收,试想我们的系统在不断地创建新的对象与引用,这些都会占用内存空间,如果不对不再有用的对象进行清理,内存很快就会占满(OOM),或者一直有线程在不断占用CPU不被释放(CPU飙升)。


到这里,我们已经知道GC要解决的问题以及重要性了,但是通常情况下我们是不是没有太关注GC的问题呢?因为这个在JVM底层都帮我们做了,但是在高性能(高并发、低延迟)的要求下,GC常常是问题很关键的点,那么我们一起来看看吧。


什么是垃圾

要做一件事,我们首先要明确对象是谁,GC是垃圾回收,那么就要确定什么会被JVM定义为垃圾。


垃圾就是不再被引用的对象,那么怎么确定是否有被引用呢?这里有两种主要的方式:


  • reference count:看看这个对象被引用了多少次,引用次数不为0便不回收,好处是执行比较快,坏处是如果出现循环引用会导致无法被回收。JS Python 是这种方式的代表。

  • root searching:从根节点一个一个找是否有引用,java便是这样的。

垃圾回收算法

我们大概讲一下垃圾回收的算法,因为不同的GC类型,基本都是这三种算法的组合与变形。


Mark-Sweep 标记清除

这个算法最简洁快速,但是缺点是碎片化严重,因为我们标记出了这是垃圾后,就直接进行了清除。


Copying 拷贝

这个算法会对内存进行整理,留一半用一半,清理时把非垃圾的整理迁移到空的一半,把垃圾全清掉。

这样的好处是内存连续性,坏处是内存的浪费。


Mark-Compact 标记压缩

对有用的对象进行标记,然后进行整理压缩,再将垃圾进行回收,这在现在的GC重会使用比较多。


GC的模型类型

GC的算法主要就是上面三种,基于上面三种算法衍生出了几大类型以及其不同的实现,那么这些不同的类型与实现到底是在解决什么问题呢?其实他们想要解决的问题也就三大类:


  • Heap 区间的内存占用(堆外辅助内存空间大小)

  • 无 GC 情况下的吞吐量(读写屏障的影响)

  • 延迟停顿(STW 时长)


有没有算法能针对这三点做到我全都要呢?这是比较难的,就像做项目想要又好又快又便宜一样。所以各种做法都在尝试去寻找三者之间的平衡最优解。


分代模型

传统的经典GC都是采用的分代模型,包括JDK 8,如果我们没有做过参数修改,其默认的GC方式Parallel也是分代模型。


分代模型将堆内存分为新生代与老年代,一个对象从创建开始,是如何被分配到新生代与老年代呢?GC又是如何回收这些内存呢?


我们看到比较多的以下类型都是分代模型,他们虽然可以交叉使用,但是一般成对使用,一个针对新生代一个针对老年代:


  • Serial 收集器、Serial Old 收集器

  • Parallel Scavenge 收集器、Parallel Old 收集器

  • ParNew 收集器、CMS 收集器


Parallel

我们以Parallel来讲讲分代模型,因为这是JDK 8 的默认方式。



新生代

  • Eden区,一个新建的对象会先进入eden区(这里已经入堆了,如果在栈内便不再被使用,OS直接将其从栈中拿出删除,不需要进行GC);

  • 如果一个对象过大,直接进入老年期,不进入新生代;

  • 当eden重的对象在扫描时不被引用,则触发GC,如被引用则进入 survivor区;

  • survivor区之间的迁移使用的拷贝算法变种,只拷贝活跃对象;

  • GC发生在新生代区;

老年代

  • 前面说的如果过大的新生对象也会直接进入老年代;

  • 当每次扫描时,一个对象被引用,到15代时进入老年代(15代因其代数存储为4bit);

  • 当老年代满了时,发生FGC;

CMS

CMS是一个承前启后的收集器,前面的收集器都是发生收集时,会停止业务逻辑处理,而CMS是并发处理,但是CMS会有两个严重问题:


浮动垃圾

错标,错标会更严重一些,看起来有点像ABA问题

分代分区模型

分代、分区模型的代表是 Garbage First(G1),这也是JDK 11的默认GC,虽然其最早出现是在JDK 7。


G1

G1也能并发进行垃圾回收,与CMS相比,其优点如下:


  • G1在GC过程中会进行整理内存,不会产生很多内存碎片

  • G1的STW更可控,可以指定可期望的GC停顿时间


G1 将 Java 堆空间分割成了若干相同大小的区域,即 region,包括 Eden、Survivor、Old、 Humongous 四种类型。其中,Humongous 是特殊的 Old 类型,专门放置大型对象,在G1中将内存区域划分为多个不连续的区域(Region),每个Region内部是连续的。


在划分的区域中H区(Humongous),这表示这些Region存储的是巨大对象(humongous object,H-obj),大小大于等于region一半的对象。


一个Region的大小可以通过参数-XX:G1HeapRegionSize复制代码设定,取值范围从1M到32M,且是2的指数。如果不设定,那么G1会根据Heap大小自动决定,JVM 会尽量划分2048个左右、同等大小的 Region。


分区模型

Shenandoah

分区模型的代表是 Shenandoah,Shenandoah 与 G1 有很多相似之处,比如都是基于 Region 的内存布局,都有用于存放大对象的 Humongous Region,默认回收策略也是优先处理回收价值最大的 Region。Shenandoah 使用连接矩阵 (Connection Matrix) 记录跨 Region 的引用关系,替换掉了 G1 中的记忆级 (Remembered Set)。Shenandoah 的内存模型是不分代的。


Shenandoah 主要目标是99.9%停顿小于10ms,暂停与堆大小无关,其发生GC的步骤大致如下:


  • Init Mark 启动并发标记。它为堆和应用程序线程准备并发标记,然后扫描根集。这是流程中的第一个暂停,最主要的消耗是根集扫描。因此,其持续时间取决于根集大小。


  • Concurrent Marking 遍历堆,并跟踪可访问的对象(三色标记对象)。此阶段与应用程序一起运行,其持续时间取决于活动对象的数量和堆中对象关系的结构。由于应用程序可以在此阶段自由分配新数据,因此在并发标记期间堆占用率会上升。这里主要运用的是SATB(snapshot-at-the-beginning)


  • Final Mark 通过清理所有等待的标记,更新队列,重新扫描根设置三个并发的来完成标记。(这里主要是处理一些SATB没有处理完的引用)它还通过确定要撤离的区域(收集集合),预先疏散一些根来初始化疏散,并且通常为下一阶段准备运行时间。这项工作的一部分可以在Concurrent Cleanup阶段同时完成。这是周期中的第二个暂停,这里消耗最主要的时间是清理队列并扫描根集。


  • Concurrent Cleanup 回收立即垃圾区域。即在并发标记之后检测到的没有活动对象的区域。


  • Concurrent Evacuation 将对象集合从集合集复制到其他区域。这是与其他OpenJDK GC的主要区别。此阶段再次与应用程序一起运行,因此应用程序可以自由分配。其持续时间取决于为流程选择的集合集的大小。


  • Init Update Refs 初始化更新引用阶段。除了确保所有GC和应用程序线程都已完成疏散,然后为下一阶段准备GC之外,它几乎没有任何作用。这是周期中的第三次暂停,最短暂停。


  • Concurrent Update References 遍历堆,并更新对并发撤离期间移动的对象的引用。这是与其他OpenJDK GC的主要区别。它的持续时间取决于堆中的对象数,但不取决于对象图结构,因为它会线性扫描堆。此阶段与应用程序同时运行。


  • Final Update Refs 通过重新更新现有根集来完成更新引用阶段。它还从集合集中回收区域,因为现在堆没有对它们的(陈旧)对象的引用。这是循环中的最后一次暂停,其持续时间取决于根集的大小。


  • Concurrent Cleanup 回收集合集区域,现在没有引用。


但是该方式需要大内存以及较高的CPU占用,可能也会导致发生GC时的吞吐量降低(GC都会有这样的问题)


分层分区模型

ZGC

ZGC是该类型的代表,但是目前还没有成熟应用,而其也因为RSS可能达到堆内存3倍的问题,对于其生产应用有一定的困难。

ZGC是为大内存、多cpu而生,它通过分区的思路来降低STW,但是实际生产运用还需要再观察观察。


下一篇内容

下一篇我们将讲述一下如何进行JVM调优,针对不同问题如何找出其问题并进行优化。

以上是关于JVM调优,调的是什么?目的是什么?的主要内容,如果未能解决你的问题,请参考以下文章

JVM调优 - 理解GC

JVM调优 - 理解GC

JVM调优 - 理解GC

jvm性能调优都做了啥

JVM原理与深度调优

Day797.JVM内存模型 -Java 性能调优实战