java虚拟机工作原理图_超“强”的图文详解-JVM虚拟机底层原理与调优实战

今天我和大家分享一篇文章,文章上半部分为JVM底层原理 下半部分为调优实战

文章有点长,需要点耐心哦!

如果觉得看文章太难理解,就点击下面我投稿B站的jvm视频讲解。

还配有视频讲解:解密BATJ一线大厂JVM虚拟机性能调优实战 如果看文章过程中有什么疑问,就再看一遍视频。看完记得三连哦!文中所有代码为截图,也是方便阅读!a53eb0eb12ec18339cad1453550f2019.png

5cfc9483540a7810a461292c22a76259.png

8ace35a20761a2fb49c00951af1df9f6.png

【上】什么是JVM

相信很多小伙伴都非常熟悉了,JVM不就是虚拟机吗?那虚拟机又是什么了?不是JVM嘛!这不废话嘛。

JVM可以说离我们既熟悉又陌生,很多朋友可能在工作中接触不到这块技术,但是在面试往往被问到(概率还蛮大),被问到了自认倒霉,死记硬背是没用的,到头来还是的忘,不过没有关系,今天你们遇到了我,我这免费给大家说道说道JVM知识点,我要没让你明白算我输,你可以留言喷我,如果要是可以,你们也给我点个赞,谢谢。来。

初识JVM:f4233580e0fed53b5b2a29e2c6749ab1.png

相信这张图大家都不陌生,这是整个Java体系,其中包括JDK.JRE.JVM三者的关系。

图中可以看得出来JRE包含了JVM,JDK包含了JRE。

从包含的角度就是: JDK是爷爷 JRE是父亲 JVM是儿子(如果觉得列子不太恰当)来看图8f07bf887b26956fbbc481f59cd4aba0.png

我们来看代码:

c29076a209550064879c50873fa7c14f.png

642e16f18eb98fe6811b0e1652e44864.png

我们运行上述代码输出结果是300,虽然这个代码非常简单,这个时候已经涉及到JVM相关的知识了,在我们学Java基础的时候老师就告诉我们,Java是跨平台的,一次编写到处运行。

那Java是怎么做到跨平台的?继续看下图:bf49b4195a8a839a08e1a874d5c13c2a.png

通过此图大家就不难发现,我们编译的App.class文件可以在Windows操作系统运行也可以在Linux系统运行,但是两个系统底层的操作指令是不一样的,为了屏蔽底层指令的细节,起到一个跨平台的作用,JVM功不可没,我们常说Java是跨平台还不如说是Jvm跨平台(JRE运行时跨平台)。那Jvm虚拟机是怎么跨平台的?

JVM底层原理:

JVM底层由三个系统构成分别是:类加载、运行时数据区、执行引擎。09d29205cc14437882ad7063ca3a9ea0.png我们今天重点讲解JVM运行时数据区(栈),其他两块可以关注我后续文章。

我们App.class文件通过类加载子系统从硬盘中读取文件加载到内存中(运行时数据区)。

加载完成之后怎么处理了?(打个比喻 人吃饭 》吃到肚子里》各各器官负责自己工作吸收)

Stack栈:

先讲一下其中的一块内存区域虚拟机栈,大家都知道栈是数据结构,也是线程独有的区域,也就是每一个线程都会有自己独立的栈区域。我们运行App.java输出300就靠线程执行得来的结果。是哪个线程执行的?获取线程快照:"main线程"栈》数据结构》存储内容》先进后出FILO4f4da703160a2417ef41639d2f591dea.png

大家都知道每个方法都有自己的局部变量,比如上图中main方法中的result,add方法中的a b c,那么java虚拟机为了区分不同方法中局部变量作用域范围的内存区域,每个方法在运行的时候都会分配一块独立的栈帧内存区域,我们试着按上图中的程序来简单画一下代码执行的内存活动。

14b4603da1b69171381711cd967e2c32.png

执行main方法中的第一行代码是,栈中会分配main()方法的栈帧,并存储math局部变量,,接着执行add()方法,那么栈又会分配add()的栈帧区域。

这里的栈存储数据的方式和数据结构中学习的栈是一样的,先进后出。当add()方法执行完之后,就会出栈被释放,也就符合先进后出的特点,后调用的方法先出栈。

栈帧:

栈帧内部“数据结构”主要由这几个部分组成:局部变量表、操作数栈、方法出口等信息。

94a9760c71bba82fe25400107689bfa5.png

说了半天,栈帧到底干嘛用的呀?别急讲这个就会涉及到更底层的原理--字节码。我们先看下我们上面代码的字节码文件。

9dfb77277b27282060c87e1e9cb05e99.png

APP.class文件看着像乱码,其实每个都是有对应的含义的,oracle官方是有专门的jvm字节码指令手册来查询每组指令对应的含义的。那我们研究的,当然不是这个。

jdk有自带一个javap的命令,可以将上述class文件生成一种更可读的字节码文件。dcfa8c582822fbfd3375c2d7cbb20296.png

986411c402e1bf1a518bcd101d11c0fc.png

dddff998c6d64517501b507d20bb8b43.png

此时的jvm指令码就清晰很多了,大体结构是可以看懂的,类、静态变量、构造方法、add()方法、main()方法。其中方法中的指令还是有点懵,我们举add()方法来看一下:

c8d26c35efd368e64c60118a85d2f62d.png

这几行代码就是对应的我们代码中add()方法中的四行代码。大家都知道越底层的代码,代码实现的行数越多,因为他会包含一些java代码在运行时底层隐藏的一些细节原理。那么一样的,这个jvm指令官方也是有手册可以查阅的,网上也有很多翻译版本,大家如果想了解可自行百度。

执行流程:

设计代码中的部分指令含义:

第一步:压栈:

将int类型常量1压入操作数栈0: iconst_1

就是将1压入操作数栈19ad79b009c9c16f03a2998a5287f748.png

更正:执行流程过程中灰色背景“操作数栈”应改为“局部变量表”(!!!!!!!!!!)。

c991d81b658e97a420016f54c1a94e7a.png

第二步:存储:

将int类型值存入局部变量11: istore_1局部变量1,在我们代码中也就是第一个局部变量a,先给a在局部变量表中分配内存,然后将int类型的值,也就是目前唯一的一个1存入局部变量a056b76f524c2a7ea17c15d39c58b64c8.png

第三步:赋值

这两行代码就和前两行类似了。2: iconst_2 3: istore_297b38631fa41c2c5f21d66423f672c4b.png

第四步:装载:

从局部变量2中装载int类型值4: iload_15: iload_2这两个代码是将局部变量1和2,也就是a和b的值装载到操作数栈中3a0164905bf93f9e2ccfc18588a86ccc.png

第五步:加法

执行int类型的加法6: iadd iadd指令一执行,会将操作数栈中的1和2依次从栈底弹出并相加,然后把运算结果3在压入操作数栈底。bfafe330b1c0e39e8ee07c98ec4315ca.png

第六步:压栈:

将一个8位带符号整数压入栈7: bipush        100 这个指令就是将100压入栈bea62c05683b5676027bfae842b18a43.png

第七步:乘法:

执行int类型的乘法9: imul 这里就类似上面的加法了,将3和100弹出栈,把结果300压入栈3d4a815219e56b7b3b38b5889a59b892.png

第八步:压栈:

将将int类型值存入局部变量310: istore_3这里大家就不陌生了吧,和第二步第三步是一样的,将300存入局部变量3,也就是ca2dff339f86fecbbcd4f3dc3329cf39b.png

第九步:装载:

从局部变量3中装载int类型值11: iload_3从局表变量3加载到操作数栈d2c10024e5014519860aaf6241747365.png

第十步:返回:

返回int类型值12: ireturn

我们add方法是被main方法中调用的,所以通过方法出口返回到mian方法中result变量存储方法出口说白了不就是方法执行完了之后要出到哪里,那么我们知道上面add()方法执行完之后应该回到main()方法第三行那么当main()方法调用add()的时候,add()栈帧中的方法出口就存储了当前要回到的位置,那么当add()方法执行完之后,会根据方法出口中存储的相关信息回到main()方法的相应位置。看我图中的红线d7bd642357c00c9f96e5f29bb529a24d.png

栈堆关系:

main方法中除了result变量还有一个app变量,app变量指向的是一个对象。那对象是怎么存储的?这儿要在说下局表变量表结构:基本类型和引用类型(Java叫引用C C++叫指针)cec00ab127ec8219c4b08c69e809fafa.png

关系就是:0798366f0cf475a40c80fbc60db78a1d.png

通过引用在栈中的app变量引用堆中的App对象

写到这里的话上半部分完。

【下】CMS收集器实战:

实战开始,准备好了没

af8a666db7392f404e4f04758c7d684e.png

实战开始,准备好了没

构建Spring Boot项目:b480bbfdfdad1043dd09ec01ee348f3e.png

11e6eccbd81f4490377c09079bac245e.png

模拟业务场景代码:536b20ea589d82653693366e2929d69b.png

c75683a42a9ff0c8f8fdb07cdbd03076.png

输出收集器信息:bda8f29459739f6e92d40251fdbf0263.png

生成jar包部署到服务器438c72545426195b5d909976ad657dfa.png

启动参数:9fa3df60b951530f8a73c4f1ba60f782.png

这儿要插播下JVM参数意义.

JVM参数详解:81ab92374fd271804a8fe640f42142be.png

JVM工具参数:448f3fa96127a50e316395a2da2b95b2.png

a10fbd563bf97488aa639650bea8c9d8.png

启动效果:1685ca34414031b19d0718b0d96c019a.png

访问:

我们不难发现新生代用的是parNew 老年代用的cms00019e6cabc11588ef3f241547a1b115.png

请求put:

我们通过http访问put方法之后看看效果:

效果如下:63a67e5970366f23c0d8e1db27af20c8.png

cc953834ebae379ffc9f8bbf2d3a0e4d.png

5e230743e7e83cd0cc4dfc8e0d3f32c0.png

9eef44025068790609b7aa660ad31f22.png

在运行的过程中我们发现有大量的对象进入老年代,触发了full gc,cms一直在收集。

使用率达到99%,cms也一刻没停下:09c8799d84d1d4db048686966ad5e427.png

日志分析:7e0a99c5410283447e82588b773dd3a8.png

日志分析1.0版本:

我们抽取一条日志来分析下

[GC (Allocation Failure) 0K->63K(64K), 0.0047147 secs] 10258K->6780K(46144K), [Metaspace: 3434K->3434K(1056768K)], 0.0047613 secs][Times: user=0.02 sys=0.00, real=0.00 secs]

该日志为四个部分:

Full GC:

表明进行了一次垃圾回收,前面没有Full修饰,表明这是一次Minor GC ,注意它不表示只GC新生代,并且现有的不管是新生代还是老年代都会STW。

Allocation Failure:

表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。

10258K->6780K(46144K),:单位是KB

三个参数分别为:GC前该内存区域(这里是年轻代)使用容量,GC后该内存区域使用容量,该内存区域总容量。

0.0047613 secs:

该内存区域GC耗时,单位是秒

[Times: user=0.04 sys=0.00, real=0.01 secs]:

分别表示用户态耗时,内核态耗时和总耗时4e6eb2aac50d59cc30332eeb4de0f4a0.png

日志分析2.0版本:

采用在线gceasy来进行分析,我们打开网址,然后上传我们生产的gc日志,如图所示:d433b35efabcf10926b024b9ca151cb3.png

优化问题:

列出了可以优化的4个问题新生代和老年代元空间内存占用情况232e5f71df770e9a298119984fc819d4.png

吞吐量统计:97.39%241031567e4538e1432b828efedea4f9.png

各各分代的内存变化:b2045754fe913c7c66bd338857e63710.png

CMS垃圾收集器不同时期发生的耗时63e8e1d087c38b62ffd208df0a6b133d.png

b22b10dcd97b7c111551bfb855a11a1f.png

GC发生次数的分类和耗时情况121a401645d96ff5a1469a27298dbc5f.png

定位问题:

我们通过生产的快照文件来定位问题:4a7c452a46aa6ac4add92c713c4fb42e.png

c72d88154f74aec8fe138f6dbefab05e.png

JProfiler:

下载到本地通过JProfiler打开查看ca9b15025e8b8768b8654455ce93261b.png

查看大对象:

我们不难发现是ArrayList集合占用了96%的内存,那我们来看看哪块代码大量用到了我们ArrayList集合了?

找到对应代码

通过此代码我们就发现put方法大量用到了ArrayList集合造成的内存溢出OOM

总结:

上述实战相信大家都明白了,大致流程就是:

1、够将SpringBoot项目 模拟真实大批量用户场景

2、配置JVM参数然后部署运行监控数据生成日志文件

3、通过分析日志文件确认问题。

4、有收获记得给我三连哦a53eb0eb12ec18339cad1453550f2019.png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值