美文网首页
深入拆解Java虚拟机

深入拆解Java虚拟机

作者: 卡斯特梅的雨伞 | 来源:发表于2020-12-15 04:28 被阅读0次

    深入拆解Java虚拟机

    参考诸葛老师深入拆解Java虚拟机

    JAVA虚拟机的组成模块

    JAVA虚拟机由类装载子系统、运行时数据区(内存模型)、字节码执行引擎三部分组成。一个class文件要执行首先要由类装载子系统加载到内存模型中,也就是运行时数据区,再有字节码执行引擎去执行解析成机器码执行。

    image

    Java内存模型详解

    虚拟机栈(线程栈)及栈帧结构

    线程执行时会在内存模型中开辟一块虚拟机栈空间去存放如局部变量等内容。虚拟机栈是线程私有的,栈帧是虚拟机栈的组成更小组成单位,一个方法对应一块栈帧内存区域,线程每进入一个方法就生成一个栈帧。栈帧内部由局部变量表、操作数栈、动态链接、方法出口4个部分组成。虚拟机栈存储在栈这种数据结构中,先进后出FILO.

    image 虚拟机栈内部组成: image

    虚拟机栈图:

    image

    方法区图:方法区存放的是常量+静态变量+类信息

    image

    本地方法栈和虚拟机栈查不到,区别只是本地方法栈是由本地方法使用的。

    image

    局部变量表就是存放方法参数和方法内部定义的局部变量的区域。局部变量表里的第0位存放的是this。

    操作数栈是线程执行方法过程中计算赋值等运行中用来存放操作数的时用的暂存空间,一块临时的内存空间。

    程序计数器是线程私有的,程序计数器存放的是每一个线程正在运行的那一行代码所在的位置,实际上存放的是当前运行的那一行代码在内存中的位置,就是代码加载到方法区中它所在的位置。

    程序计数器的作用是什么?JAVA多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,线程有可能被挂起,当CPU继续执行挂起的线程时,就是通过程序计数器来知道线程的目前执行到的当前位置。而程序计数器的值是字节码执行引擎去操作修改的。

    操作数栈局部变量表中存放的是堆中的引用地址。

    方法区在JDK1.8之前的实现是永久代,1.8及之后是用元空间实现。方法区存放的是常量+静态变量+类信息。方法区中的静态变量存放的还是堆对象中的内存地址,对象还是在堆中创建,只是方法区的静态变量引用存放了这个地址。

    本地方法栈是线程运行本地方法时创建的,本地方法就是被native修饰的方法,其底层是调用c或c++代码实现。当调用到本地方法时,比如运行在Windows系统下,就会去找该本地方法对应的c语言对应的.dll文件,dll文件相当于我们java的jar包文件。

    虚拟机栈、本地方法栈、程序计数器是线程私有的,而堆和方法区是线程共有的。

    minor gc、full gc都是由字节码执行引擎后台开启的垃圾回收线程,minor gc 对新生代进行垃圾回收。full gc是对整个堆进行垃圾回收。(包含新生代和老年代)

    image image

    堆由新生代和老年代组成,新生代和老年代的默认内存空间大小比例大概是1:2。而新生代又由Eden区和Survivor区组成,Survivor区有两个空间,分别为S0 和S1。Eden区和两个Survivor区的空间大小比例一般是8:1:1。我们创建是对象时会先在Eden区创建对象。当Eden区空间满了的时候,会触发minor gc,字节码执行引擎后台会开启的垃圾回收线程进行垃圾回收,垃圾回收是通过gc roots Tracing算法进行可达性分析,GC Roots对象主要包括虚拟机栈(栈桢中的局部变量表)中的引用的对象、本地方法栈中的引用的对象、方法区中的类静态属性引用的对象、方法区中的常量引用的对象这四种。从GC Roots对象出发向下搜索其引用的对象链,把这些对象都标记为非垃圾对象,在minor gc 结束后复制到空着的Survivor区,比如s0。然后对Eden区中的对象进行垃圾回收。对象在Survivor区中每熬过一次Minor GC,分代年龄就增加1岁。再进行一次minor gc 时,这时候就会对Eden区和非空的Survivor区s0进行垃圾回收,把非垃圾对象复制到Survivor区s1中,并对存活的对象分代年龄加1。就这样如此反复进行垃圾回收,直到存活的对象年龄达到15岁时,这些对象就会被转移到老年代中。而当老年代也放满的时候,虚拟机就会触发full gc,对整个堆进行垃圾回收(包含新生代和老年代)。当老年代回收不了足够的内存空间给新创建的对象来分配空间时,就会发生OOM内存溢出。这种情况下就要进行代码的分析梳理和虚拟机调优。

    虚拟机调优的目的是为了减少gc执行的时间和次数,其核心就是减少STW的时间和次数。我们的虚拟机只有进行gc,无论是minor gc 还是full gc ,都会触发STW机制,暂停我们的运用程序的用户线程的执行,造成让用户觉得 程序卡顿等问题。full gc 与 minor gc 相比STW的时间会更长,因此我们进行虚拟机调优时会优先优化减少full gc的次数,减少gc执行的时间。

    Q:web系统中哪些会被存放到老年代中?

    A: 代码里的缓存对象,静态变量引用的对象、常量引用的对象、线程池、数据库连接池、spring bean容器里面的bean对象等等一直存活的对象。

    Q: JAVA虚拟机为什么要设置STW的机制?

    A: 在gc的过程中,只要我们的用户线程在执行,我们的对象是否是垃圾对象的状态就会不断的变化。针对这种变化处理太多复杂,因此引入STW的机制,暂停用户线程执行,让gc专心的进行垃圾回收,不让对象的状态进行变化,使gc回收过程简单化且更加高效。反证法:我们假设如果没有STW的机制,那么在进行gc垃圾收集时应用程序的用户线程还在继续执行,这时候我们通过GC Roots 搜寻对象时找到的非垃圾对象可能在用户线程执行完后就没用了,这些对象便成了垃圾对象了,而也有可能原本是垃圾对象的对象被用户线程执行时重新引用到了,这时候它便不是垃圾对象了,对于这种情况虚拟机如果继续不断重新的标记便没完没了了,gc没有结束的判断标准了。

    完整的对象组成

    一个完整的对象由对象头,实例数据和对齐填充组成。对象头包括有Mark Word标记字段比如gc分代年龄、对象的hashcode、类的元数据指针、锁状态(无锁、偏向锁,轻量级锁、重量级锁)、如果是数组还有个数组长度等这些数据。

    对象头 image

    JDK自带的JVM调优工具——jvisualvm

    jvisualvm自带识别出系统正在运行的jdk进程。Visual GC可以实时查看进程正在进行的内存区域中各个部分的内存情况和GC发起时间。

    image 频繁full gc优化,survivor区动态年龄判断 image image image

    订单中心运维反馈项目出现下单高峰期,系统有压力会发生频繁的full gc,通过分析进行JVM调优发现是动态年龄判断导致,通过增大了survivor区空间大小解决了这个问题,提高了系统的可靠性。Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,那么年龄大于或等于该年龄的对象就直接进行老年代,无须等到MaxTenuringThreshold中要求的年龄简单版:经过分析发现老年代空间增长过快,分析堆dump和代码还有当时发现是老年代中存活的对象中理论上应该被minor gc 收集走了,现在确大量存活着,再经过进一步的分析判断是进行minor gc时survivor区的动态年龄判断导致了本应被回收的对象晋升到了老年代,说明survivor区分配空间太小了。于是把新生代的空间扩大了1.5倍。eden:s0:s1= 1230m:150m:150m解决了当时的问题。新生代和老年代比: 1.5g :2.5g 详细版:当时用jstat -gccause 看了下最近二次gc统计——主要关注已使用空间占总空间的百分比 ,会额外输出导致上一次GC产生的原因。使用jstat -gc 2764 250 20 每250毫秒查询一次进程2764垃圾收集的情况,一共查询20次 。jmap -histo:显示堆中对象统计信息 ,经过分析发现是老年代对象增长太快了,一般正常情况下老年代是很稳定的。

    数据导出导致OOM java.lang.OutOfMemoryError: Java heap space。问题及解决。一开始是前端同步等待导出结果,但后面因为数据量太大,接口有设置超时时间,在超时时间后文件还未导出便无法导出成功。后面改成前端发起数据导出请求沉淀数据库,后端tf进程扫描请求任务拉去异步下载订单Excel处理。但因为文件数据量大且并发高导致进程过载 ,加线程池,加判断避免用户不断重新发起下载。内存溢出java heap space 经判断是因为文件很大的时候都写在内存里导致内存溢出,通过边写边io输出到文档中,减少内存占用。

    垃圾收集器 image cms参数 image

    其他

    JDK体系
    image
    Java通过如何实现跨平台运行

    Java通过JVM实现跨平台运行。通过不同版本和操作系统平台类型的JVM,去把字节码文件解析成适合不同平台运行的机器码来实现跨平台运行。JVM从软件层面屏蔽不同操作系统在底层硬件与指令上的区别。

    image
    javap命令的作用和使用方法

    javap 是class文件分解器,可以反编译 ,生成更可读的jvm指令码文件,主要用于帮助开发者深入了解 Java 编译器的机制 。

    使用:

    <pre class="md-fences md-end-block" lang="" contenteditable="false" cid="n151" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">//先获取到class文件所在的路径,切换路径
    cd D:\self\hellospringboot\target\classes\com\self\jdknew
    //执行反编译并把执行结果输出为当前目录下的JavaTest.txt
    //-c:分解方法代码,即显示每个方法具体的字节码
    javap -c JavaTest.class > JavaTest.txt

    //添加 -d 选项除了可以指定存放编译生成的 .class 文件的路径外,最大的区别是可以将源文件首行的 package 关键字下的包名在当前路径下生成文件夹。
    //javac是用来编译.java文件的
    javac -encoding utf-8 -d . MyBeanBootApplication.java
    ​</pre>

    参考:命令行中 javac、java、javap 的使用详解

    相关文章

      网友评论

          本文标题:深入拆解Java虚拟机

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