JVM监控篇(一)- JVM相关理论详解【运维角度】

1- JVM是什么

  • Java(Java Virtual Machine)虚拟机,是Java运行环境的一部分。

1.1 JVM由以下几个部分构成

JVM监控篇(一)- JVM相关理论详解【运维角度】_第1张图片

类加载器(Class Loader)

负责加载class文件,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构。ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine 决定。

运行时数据区(Runtime Data Area)

  • 所有的数据和程序都是在运行数据区存放。由下面五个部分构成:
  • java栈

1)即栈内存,是Java程序的运行区,是在线程创建时创建,它的生命期和线程的生命期一致,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束,该栈就“消失”了。

2)8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行。堆内存包含2个区:

1)新生区:
又叫新生代。新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。

2)养老区:
也叫老年代。用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。

JVM监控篇(一)- JVM相关理论详解【运维角度】_第2张图片

  • 方法区

方法区也可以叫做永久存储区,又叫永久代。是被所有线程共享的,该区域保存所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。Java8以前叫永久代,java8以后改名为元空间。元空间与永久代之间最大的区别在于:永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。

  • 程序计数器

每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址),由执行引擎读取下一条指令。

  • 本地方法栈

与Java栈基本类似,区别在于Java栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。

执行引擎(Execution Engine)

执行引擎负责解释命令,提交操作系统执行。

本地接口(Native Interface)

本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。

本地方法库(Native Libraies)

负责登记native方法,以便在Execution Engine 执行时加载本地方法库。

垃圾回收子系统

垃圾回收器可以对方法区、java堆和直接内存进行回收。其中,java堆是垃圾收集器的工作重点。和C/C++不同,java中所有的对象空间释放都是隐式的,也就是说,java中没有类似free()或者delete()这样的函数释放指定的内存区域。对于不再使用的垃圾对象,垃圾回收系统会在后台默默工作,默默查找、标识并释放垃圾对象,完成包括java堆、方法区和直接内存中的全自动化管理。

1.2 GC垃圾回收

运维主要关注GC,设置正确的垃圾回收机制以最大化利用内存资源
GC目的:回收堆内存中不再使用的对象,释放资源
GC回收:应该主要关注当前的 GC 是否停止了所有应用程序的线程,还是能够并发的处理而不用停掉应用程序的线程。

GC在何时被触发

  • Minor GC

从新生代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。

当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,即当新生代区的Eden 区满了就会触发Minor GC。

  • Major GC

从年老代空间回收内存被称为Major GC。
当年老代空间满了就会触发Major GC。也可以说,Major GC会被Minor GC触发。

  • Full GC

清理整个堆空间—包括新生代,年老代和永久代。触发条件
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、survivor space1(From Space)区向survivor space2(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
(6)当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载。

如何判断哪些是垃圾

  • 引用计数法

为每一个创建的对象分配一个引用计数器,用来存储该对象被引用的个数。当该个数为零,意味着没有人再使用这个对象,可以认为“对象死亡”。每当有一个地方去引用它时候,引用计数器就增加1。

  • 可达性分析

基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots (如Java栈中的本地变量,方法区中静态属性引用的对象等等)出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。不能到达的则被可回收对象。

JVM监控篇(一)- JVM相关理论详解【运维角度】_第3张图片

垃圾回收算法

  • 标记-清除算法

在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

JVM监控篇(一)- JVM相关理论详解【运维角度】_第4张图片

优点:存活对象较多的情况下比较高效,适用于年老代(即旧生代)

缺点:容易产生内存碎片,且需要扫描整个空间两次

JVM监控篇(一)- JVM相关理论详解【运维角度】_第5张图片

  • 复制算法

从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉。现在的商业虚拟机都采用这种收集算法来回收新生代。

JVM监控篇(一)- JVM相关理论详解【运维角度】_第6张图片

优点:存活对象较少的情况下比较高效,扫描了整个空间一次(标记存活对象并复制移动)。适用于年轻代(即新生代):基本上98%的对象是"朝生夕死"的,存活下来的会很少

缺点:需要一块儿空的内存空间,需要复制移动对象

JVM监控篇(一)- JVM相关理论详解【运维角度】_第7张图片

  • 标记-整理算法

标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。

首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

JVM监控篇(一)- JVM相关理论详解【运维角度】_第8张图片

标记整理算法解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。标记整理算法对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

JVM监控篇(一)- JVM相关理论详解【运维角度】_第9张图片

  • 分代收集算法

分代收集算法就是目前虚拟机使用的回收算法,它解决了标记-整理法不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为年老代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。

在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率搞,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。

垃圾回收机制

JVM监控篇(一)- JVM相关理论详解【运维角度】_第10张图片

不设置垃圾回收机制时,默认使用的一般是+UseParallelGC ,即Parallel Scavenge收集器和Serial old收集器( java -XX:+PrintCommandLineFlags -version 可以查看默认使用的垃圾收集器)

  • Serial收集器

Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了(新生代采用复制算法,老年代采用标志整理算法)。

进行垃圾收集时,必须暂停所有工作线程,直到完成。

单线程收集器,主要针对新生代

  • ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和Serial收集器完全一样。

除了Serial收集器外,目前只有它能与CMS收集器配合工作。

参数设置 +UseParNewGC
//指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相:
“-XX:ParallelGCThreads”

  • CMS(Concurrent Mark Sweep)收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。它非常适合在注重用户体验的应用上使用。

针对老年代,基于"标记-清除"算法(不进行压缩操作,会产生内存碎片),并发收集、低停顿,需要更多的内存。

对CPU资源比较敏感(并发程序的特点)。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。

CMS所需要的内存空间比其他垃圾收集器大; 可以使用"-XX:CMSInitiatingOccupancyFraction",设置CMS预留老年代内存空间。

参数设置: +UseConcMarkSweepGC

此外,jdk9开始,废弃了CMS,但还能用;jdk14直接删除了CMS。

  • Parallel Scavenge收集器

属于新生代收集器,使用复制算法,且是并行的多线程收集器。主要适用于不需要太多交互的任务。

关注点是吞吐量,即减少垃圾收集时间,让用户代码获得更长的运行时间;

吞吐量=运行用户代码的时间/(运行用户代码时间+垃圾收集时间。比如,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

  • Serial Old收集器

Serial收集器的老年代版本,它同样是一个单线程收集器。采用"标记-整理"算法,主要用于Client模式。

用于server模式的话,主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

  • Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

  • G1收集器

G1主要针对多核CPU以及大容量内存的机器,是jdk9以后的默认回收器,取代了CMS、Parallel + Parallel Old组合,被称为全功能的垃圾收集器。

jdk8中不是默认的,使用参数设置 -XX:+UseG1GC。

并行性:G1回收期间,可以用多个GC线程同时工作,有效利用多核计算能力,此时用户线程被暂停

并发性:G1有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此在GC时不会完全阻塞应用程序

G1回收器可以采用应用线程承担后台运行的GC工作,也就是当GC线程处理速度慢时,系统会调用应用程序线程帮助加速GC过程

G1依旧会区分年轻代和老年代,年轻代依旧分为伊甸园区和幸存者区。但从堆结构上看,它不要求整个伊甸园区、整个年轻代或老年代都是连续的,也不坚持固定大小和固定数量

G1将堆空间分为若干区域,这些区域中包含了逻辑上的老年代和年轻代,回收以区域为单位。区域之间采用复制算法,整体上可看成是采用了标记-压缩算法。

在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则会发挥其优势。平衡点在6-8GB之间。

1.3 JDK、JRE、JVM三者的关系是什么

JDK是Java程序员常用的开发包、目的就是用来编译和调试Java程序的。

JRE是指Java运行环境,也就是我们的写好的程序必须在JRE才能够运行。

JVM负责将字节码解释成为特定的机器码进行运行。

此外,在运行过程中,Java源程序需要通过编译器编译为.class文件,否则JVM不认识。

2- 为什么监控JVM

1)为排查问题提供科学可靠的线索
2)结合jvm监控数据可以对应用程序功能及性能进行一定程度的优化
3)监控哪些数据:
GC的次数,一次GC所需要的时间
GC各个时代的数据
进程占用的CPU
进程占用的内存
堆内存
线程数
类加载情况
业务名

3- JVM监控方案调研

  • 如何采集数据 —— 如何存储历史数据 —— 如何展示数据 —— 如何告警

1)传统的监控软件,如zabbix也可以通过配置 zabbix java gateway 对jvm进行监控,但是在k8s集群场景下就不如prometheus灵活好用。

2)Open-Falcon也可以通过MxBeans采集jvm数据达到监控的效果,但是该软件功能并不完善,社区运营也相对欠缺。

3)pinpoint+HBase方案,除了能监控jvm,还可以做程序调用的链路追踪,可以快速启动。

4)skywalking+elasticsearch方案,除了能监控jvm,还可以做程序调用的链路追踪。

5)当下流行的prometheus既可以在传统架构下对jvm进行监控,也能够在k8s集群场景下完成jvm监控作业。还可以结合influxdb+grafana对历史监控数据进行展示。

4- 常见问题及排故

4.1 生产环境发生了内存溢出该如何处理?

4.1.1 错误:java.lang.OutOfMemoryError. Java heap space

JVM在启动的时候会自动设置JVM Heap的值,Heap的大小是Young Generation 和Tenured Generaion 之和。

在JVM中如果98%的时间是用于GC,且可用的Heap size 不足2%的时候将抛出此异常信息。此时分2种情况,因为某个JVM线程申请不到足够的一块大内存而抛出异常,JVM进程内其它线程能够正常访问,只是当前线程OOM;另外一种是某个JVM线程连很小内存都申请不到,所有堆内存都被用光,进行full gc都不能回收内存,此时会导致整个JVM进程退出。

java.lang.OutOfMemoryError: GC overhead limit exceeded就是这种情况,可以使用-XX:-UseGCOverheadLimit 去掉JVM默认的参数设定,但最后JVM还是因为OOM会退出。

此时可以通过Thread.setDefaultUncaughtExceptionHandler()方法来保留当前JVM线程退出前的未捕捉到的错误信息到指定文件(保留现场),但是当前JVM线程必定会因为抛出的error而退出的。当所有非守护线程都退出时,JVM就会退出!

  • 解决方法:修改JVM Heap的设置即Xms Xmx

Xms:JVM启动时初始化堆内存的大小
Xmx:JVM分配的堆内存的最大值

Xms设置的值过小,可能会导致应用启动时内存不够,从而应用启动失败,Xmx值过小,可能会导致应用启动后运行一段时间,内存不够用,一般设置Xmx大小为总机器内存的80%。同时将Xms的值和Xmx的值设置为一样,从而减少系统新增heap内存带来的性能损耗。

4.1.2 错误:java.lang.OutOfMemoryError. Java PermGen space

sun的GC不会在主程序运行期对PermGen space进行清理,所以如果载入很多CLASS的话,就很可能出现PermGen space溢出。

  • 解决方法:修改XX:MaxPermSize的大小

XX:PermSize:永久代的初始化内存大小
XX:MaxPermSize:永久代的内存空间最大值

永久代是用来存放加载的class及meta元素,如果应用加载的类较多,则需要调大一些。

4.1.3 错误:java.lang.StackOverFlowError

通常来讲,一般栈内存远远小于堆内存的,因为函数调用过程往往不会多于上千层,而即便每个函数调用需要 1K的空间(这个大约相当于在一个C函数内声明了256个int类型的变量),那么栈内存也不过是需要1MB的空间。通常栈内存的大小是1-2MB的。一般地,如果程序的递归层次太多,或者出现了死循环,就会报这个错。

  • 解决办法:可以先试着把Xss的值调到2M,如果还没有解决问题就要修改程序了。如果不容易查出问题所在,可以dump线程下来分析

Xss:这个参数表示分配给每个线程的栈的大小,JDK5.0以后默认都是分配1M的空间,一般这个值足够用了

4.1.4 收集内存溢出Dump文件

  • 两种方式:

1)设置JVM启动参数:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/jvmdump

2)使用jmap命令收集 通过 jmap -dump:live,format=b,file=/opt/jvm/dump.hprof pid
在获取Dump文件后,可以使用工具MAT(MemoryAnalyzer)进行分析

4.2 生产环境应该给服务器分配多少内存?

对于Java8而言,一般堆内存的初识容量为机器实际内存大小的1/64, 最大内存不超过机器实际内存的1/4。这个视具体业务而定,单机内存占用超过16G就会考虑分布式环境了。

4.3 生产环境CPU负载飙升如何处理?

一般是发生了死循环或死锁,这种一般都需要改代码。

排查办法示例:top+jstack
1)用top、ps aux 等命令找出CPU占比最高的进程的PID
2)定位到具体线程:ps -mp PID -o THREAD,tid,time
3)将需要的线程TID转换为16进制格式:printf “%x\n”
4)jstack PID|grep TID -A60

4.4 如何对垃圾收集器的性能进行调优?

  • 主要关注以下两个指标:

停顿时间:垃圾收集器做垃圾回收中断应用执行的时间,-XX:MaxGCPauseMillis

吞吐量:吞吐量=运行用户代码的时间/(运行用户代码时间+垃圾收集时间。垃圾收集时间占: 1/(1+n)

最理想的情况下,吞吐量最大的时候,停顿时间最小。现实中,这两个指标是互斥的,GC调优时候,很大部分工作就是如何权衡这两个变量。

5 JVM监控工具的使用

5.1 jps

  • 输出pid和该进程的启动主类名:

-m 输出传入main方法的参数
-v 输出传入jvm的参数
-q 不输出class名、jar名和传入main方法的参数
-l 输出main类或jar的全称

5.2 jinfo

观察进程运行环境参数,包括Java System属性和JVM命令行参数。

  • jinfo pid:输出当前 jvm 进程的全部参数和系统属性
  • jinfo -flags pid:输出全部的参数

5.3 jstat

监视虚拟机各种运行状态信息,运行期间定位性能问题的首选工具。

可以显示本地或者远程虚拟机中的类装载、卸载数量、内存、垃圾收集、JIT编译状况等运行时数据。

  • jstat -compiler pid

Compiled:编译数量。
Failed:失败数量
Invalid:不可用数量
Time:时间
FailedType:失败类型
FailedMethod:失败的方法

  • jstat -gcutil pid <时间间隔> <次数> 500ms内收集10次数据

S0:幸存0区当前使用比例
S1:幸存1区当前使用比例
E:伊甸园区使用比例
O:老年代使用比例
M:元数据区使用比例
CCS:压缩使用比例
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间

  • jstat -gccause pid

LGCC:上一次GC触发的原因是内存分配失败
GCC:当前没有发生GC,所以是No GC

5.4 jmap

主要用于生成堆转储快照,生成dump文件或称为heapdump文件,同时可以查询java堆和永久代的详细信息,如空间使用率,是使用哪种收集器等)

  • jmap -heap pid
Attaching to process ID 35133, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.141-b15(虚拟机版本)
  
using thread-local object allocation.
Parallel GC with 4 thread(s)(使用何种收集器)
  
Heap Configuration:(Java堆的配置信息)
   MinHeapFreeRatio         = 0 (堆使用率小于n时进行收缩)
   MaxHeapFreeRatio         = 100 (堆使用率大于n时进行扩张,2个参数在Xmx==Xms 的情况下无效)
   MaxHeapSize              = 268435456 (256.0MB) (最大堆可用空间)
   NewSize                  = 67108864 (64.0MB)  (新生代空间大小)
   MaxNewSize               = 67108864 (64.0MB)   (新生代最大可用空间)
   OldSize                  = 67108864 (64.0MB)   (老年代可用空间)
   NewRatio                 = 2(设置老年代与新生代的比例,OLD/NEW=2)
   SurvivorRatio            = 8(设置Eden:S1:S2=8:1:1,这是jvm的默认设置)
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)
  
Heap Usage:(堆的内存监控信息)
PS Young Generation
Eden Space:
   capacity = 66060288 (63.0MB) (可用空间)
   used     = 22447528 (21.407630920410156MB) (已使用)
   free     = 43612760 (41.592369079589844MB) (空闲)
   33.980366540333584% used  (已使用所占的百分比)
From Space:
   capacity = 524288 (0.5MB)
   used     = 262144 (0.25MB)
   free     = 262144 (0.25MB)
   50.0% used
To Space:
   capacity = 524288 (0.5MB)
   used     = 0 (0.0MB)
   free     = 524288 (0.5MB)
   0.0% used
PS Old Generation
   capacity = 77594624 (74.0MB)
   used     = 17072000 (16.2811279296875MB)
   free     = 60522624 (57.7188720703125MB)
   22.00152422930743% used
  
17747 interned Strings occupying 2199536 bytes.
  • jmap -dump[:live,]format=b,file=

导出jvm的java堆所有存活对象dump文件,以二进制形式输出,拿到这个这个文件后就可以借助外部的可视化监控工具或者其他途径进行堆的分析。

5.5 jstack

生成虚拟机的线程快照threaddump或javacore,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。

可以用来观察jvm中当前所有线程的运行情况和线程当前状态。

  • jstack pid
  • jstack pid > /tmp/threadDump.log

有时候jstack pid输出的内容较多,所以保存到文件中再查看比较方便~

接下来的文章,详细写一下【prometheus+jmx】 对jvm进行监控的方案~

https://blog.csdn.net/qq_35550345/article/details/107043460
https://blog.csdn.net/qq_35550345/article/details/107044028

你可能感兴趣的:(自动化运维,jvm监控,jvm内存,GC垃圾回收,JVM内存回收)