JVM监控篇(一)- 先了解一下JVM

1- JVM是什么

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

1.1 JVM由以下几个部分构成

在这里插入图片描述

类加载器(Class Loader)

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

运行时数据区(Runtime Data Area)

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

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

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

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

1)永久存储区:
又叫永久代。永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。Java8以前叫永久代,java8以后改名为元空间。元空间与永久代之间最大的区别在于:永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。

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

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

在这里插入图片描述

  • 方法区

方法区是被所有线程共享的,该区域保存所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。

  • 程序计数器

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

  • 本地方法栈

与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垃圾回收

如何判断哪些是垃圾

  • 引用计数法

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

  • 可达性分析

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

在这里插入图片描述

垃圾回收算法

  • 标记-清除算法

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

在这里插入图片描述

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

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

在这里插入图片描述

  • 复制算法

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

在这里插入图片描述

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

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

在这里插入图片描述

  • 标记-整理算法

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

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

在这里插入图片描述

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

在这里插入图片描述

  • 分代收集算法

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

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

垃圾回收机制

在这里插入图片描述

不设置垃圾回收机制时,默认使用的一般是+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对历史监控数据进行展示。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章