摘要:JVM是一种用于计算设备的规范,是一个虚构出来的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。
本文分享自华为云社区《[云驻共创]JVM内存模型的探知之旅》,作者:多米诺的古牌。
1. JVM介绍
1.1 什么是JVM?
JVM是JavaVirtual Machine(Java虚拟机)的简称,是一种用于计算设备的规范,是一个虚构出来的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。
1.2 JVM的优点
1.2.1 一次编写,到处运行
JVM可以让java程序,一次编写,导出运行。让底层代码和运行环境分离开,编写好一份代码后,不用再次修改内容,只用通过安装不同的JVM环境自动进行转换即可运行,在各种系统中无缝连接。
1.2.2 自动内存管理,垃圾回收机制
在Java诞生之时,C和C++称霸天下,但是这两种语言中没有内存管理机制,是通过手动操作来进行的管理,非常麻烦和繁琐。
此时Java应运而生,为了处理内存管理这个方面,专门设计了垃圾回收机制,来自动进行内存的管理。极大的优化了操作,让程序员们不用正在噼里啪啦在码代的海洋中遨游时,还要操心内存会不会溢出这些“影响我方输出”的问题,顿时获得了成吨的好评。
1.2.3 数组下标越界检查
在Java诞生之时,还有个让当时C和C++大佬头疼的问题是,数组下标越界是没有检查机制的,这还了得,又是一个影响“我方暴力输出”的罪魁祸首,因此JVM继续抱着暖男的思想,又来了个爱的抱抱。
JVM又一次看见了大佬们的烦恼,果断提供了数组下标越界的自动检查机制,在检测到数组下标出现越界后,会在运行时自动抛出“java.lang.ArrayIndexOutOfBoundsException”这个异常,在当时可是感动了很多业界大佬(我猜的)。
1.2.4 多态
JVM还有一个多态功能,是通过相同接口,不同的实例进行实现,完成不同的业务操作,比如:定义了一个动物接口(里面有一个吃的方法),我们就可以通过这个动物创造小猫(吃鱼),再创造一个狗狗(吃肉),再创造一个小助手(吃零食,O(∩_∩)O哈哈~)。
仔细想想,对我们有啥影响呢,那好处老多了,比如:
(1)消除类型之间的耦合关系;
(2)可替换性;
(3)可扩充性;
(4)接口性;
(5)灵活性;
(6)简化性;
1.3 JVM、JRE、JDK之间的关系
1.3.1 JVM的简介
JVM是JavaVirtual Machine的简称,是Java虚拟机,是一种模拟出来的虚拟计算机,它通过在不同的计算机环境当中模拟实现计算功能来实现的。
引入Java虚拟机后,Java语言在不同平台上运行时就不需要重新编译。在其中,Java虚拟机屏蔽了与具体平台的相关信息,使得Java源程序在编译完成之后即可在不同的平台运行,达到“一次编译,到处运行”的目的,Java语言重要的特点之一跨平台,也即与平台的无关性,其关键点就是JVM。
1.3.2 JRE的简介
JRE是JavaRuntime Environment的简称,是Java运行环境,是让操作系统运行Java应用程序的环境,其内部包含JVM,也就是说JRE只负责对已经存在的Java源程序进行运行的操作,它不包含开发工具JDK,对JDK内部的编译器、调试器和其它工具均不包含。
1.3.3 JDK的简介
JDK是JavaDevelopment Kit的简称,是Java开发工具包,是整个Java程序开发的核心。其主要包含了JRE、Java的系统类库以及对Java程序进行编译以及运行的工具,例如:javac.exe和java.exe命令工具等。
1.4 JVM的常见实现
Oracle(Hotspot、Jrockit)、BEA(LiquidVM)、IBM(J9)、taobaoVM(淘宝专用,对Hotspot进行了深度定制)、zing(垃圾回收机制非常快,到达1毫秒左右)。
1.5 JVM的内存结构图
当Java程序编译完成为.class文件==》类加载器(ClassLoader)==》将字节码文件加载进JVM中;
1.5.1 方法区、堆
方法区中保存的主要是类的信息(类的属性、成员变量、构造函数等)、堆(创建的对象)。
1.5.2 虚拟机栈、程序计数器、本地方法栈
堆中的对象调用方法时,方法会运行在虚拟机栈、程序计数器、本地方法栈中。
1.5.3 执行引擎
执行方法中代码时,代码通过执行引擎执行中的“解释器”执行;方法中经常调用的代码,即热点代码,通过“即时编译器”执行,其执行速度非常快。
1.5.4 GC(垃圾回收机制)
GC是针对堆内存中没有引用的对象进行回收,可以手动也可以自动。
1.5.5 本地方法接口
因为JVM不能直接调用操作系统的功能,只能通过本地方法接口来调用操作系统的功能。
2. JVM内存结构-程序计数器
2.1 程序计数器的定义
Program Counter Register即程序计数器(寄存器),用于记录下一条Jvm指令的执行地址。
2.2 操作步骤
javap主要用于操作JVM,javap -c 是对java代码进行反汇编操作。下图为通过先编译demo.java后,再执行javap -c demo的输出结果:
其中第一列为二进制字节码,即JVM指令,第二列为java源代码。第一列中的序号为JVM指令的执行地址。
JVM会通过程序计数器记录下一条需要执行的JVM指令的地址(比如第一行的0),然后交给解释器解析为机器码,最后交给cpu(只能识别机器码),完成一行的执行。
想要执行下一行,继续让JVM的程序计数器记录下一条地址(比如第二行的3),再交给解释器解析后给cpu,以此类推执行结束。
2.3 特点
2.3.1 线程私有的
2.3.2 不会存在内存溢出
3. JVM内存结构-虚拟机栈
3.1 定义
虚拟机栈是每个线程运行所需要的内存空间,每个栈中由多个栈帧组成,每个线程中只能有一个活动栈帧(对应当前正在执行的方法),所有栈帧都遵循后进先出,先进后出的原则。
栈帧是每次调用方法时所占用的内存,在栈帧中保存的内容参数、局部变量、返回地址。
注1:垃圾回收不涉及栈内存,因为栈内存是由方法调用产生的,当方法调用结束后会弹出栈。
注2:栈内存不是分配的越大越好,因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数会越来越少。
注3:方法的局部变量,当其没有逃离方法的作用范围时,是线程安全的;如果其引用了对象(比如静态变量,即共享变量,用对象作为参数的方法,返回值为对象的方法),并且逃离出了方法的作用范围,就需要考虑线程安全的问题了。
3.2 栈内存溢出
3.2.1 发生原因
(1)虚拟机栈中,栈帧过多(无限递归),如图1栈帧过多;
(2)每个栈帧所占用过大,如图2 栈帧过大。
3.2.2 栈内存溢出小实验
3.2.2.1 栈帧过多的小实验
无限递归调用(栈帧过多)的小实验,method1()方法在主方法中无限调用自己,那么会发生什么情况呢?
答案很明显,程序崩溃了,产生了栈内存溢出错误,如下图所示:
-Xss:该参数规定了每个线程虚拟机栈的大小;
接着我们通过设置一个虚拟机栈的大小是256k试试会发生什么?
我们发现当我们调整了虚拟机栈的大小后执行了4315次方法后内存就溢出了,而调整虚拟机栈之前,我们是23268次,很明显我们可以通过-Xss参数调整虚拟机栈的大小来控制内存的溢出情况。
3.2.2.2 线程运行诊断小实验
想象中的场景,大佬在疯狂输出,突然CPU爆表了,显示CPU占用过多,如何去定位哪行代码的问题,是的是哪行(大佬都很忙的好吗,当然要精确了,一分钟几千万上下的,O(∩_∩)O哈哈~)?
Linux环境下:
在后台运行Stack_6这个java字节码(.class)文件:
注:无论是否将nohup命令的输出重定向到终端,输出都将附加到当前目录的 nohup.out 文件中。
(1)通过top命令,查看进程(相当于任务管理器),发现了一个占用CPU达到100%的可疑家伙,这还了得,赶紧瞅瞅具体发生了什么,还有没有王法,这让其他小伙伴还怎么愉快的玩耍,秒速纠错ING。。。
注:top命令,查看哪个进程占用CPU过高,返回进程号。
(2) 通过ps H -eo pid,tid ,%cpu | grep 命令过滤任务管理器中的内容。
注:ps H -eopid,tid,%cpu |grep,是通过ps命令查看哪个线程占用CPU过高,返回进程id,其中pid为进程id,tid为线程id,%cpu为CPU占用情况;
发现了罪魁祸首,这一串串心惊肉跳的红色。。。
(3) 通过jstack 进程id查看,20389这个有问题的进程中具体的情况。
注:jstack 进程id,是通过jstack 命令定位具体哪段代码出现占用CPU过高,注意jstack命令查找的线程id是16进制的,需要转换;
发现里面有一堆执行的代码,那么我们怎么找到具体是哪个家伙搞事情的呢?上图我们可以发现搞事情的线程是20441,那么我们通过计算器将20441转换为16进制的4FD9再去试试,真相只有一个。
通过对比nid(线程id)4fd9,我们发现这个叫thread1的线程一直在运行(RUNNABLE状态),并且查看到位置是位于Stack_6.java文件的第11行出现的问题。。。
现在我们回到源码中,在Stack_6文件的第11行,我们发现原来这里一直在执行死循环,终于找到你,还好我没放弃,奈斯~
4. JVM内存结构-本地方法栈
4.1 定义
由于Java本身有时候是无法直接和操作系统底层交互的,但有时候需要Java调用本地的C或C++方法,所以此时本地方法栈应运而生,它们是一类带有native关键字的方法。
5. JVM内存结构-堆
5.1 定义
Heap堆:是通过new关键字创建的对象存放在堆中
5.2 特点
5.2.1线程共享
堆中存放的对象都是线程共享的,因此都是需要考虑线程安全问题的。
5.2.2有垃圾回收机制
因为堆中存放的对象存放了大量的对象,因此给他配了个小助手——垃圾回收机制(可以调自动挡和手动挡哦~)。
5.3 堆内存溢出小实验
5.3.1 修改堆内存大小参数的小实验
继续幻想一个场景,当一个大佬开发完一个段代码的时候(当然一般大佬都是很自信的,我写的代码怎么可能有问题,不存在的。。。),但是测试可跑不了,稳妥起见咱们还是默默得搞测试试试嘛,安全第一。但是机器的内存就这么大,大佬肯定跑了很多次了,都没出现问题的,这不是找茬嘛。。。还是默默改下机子参数再试试吧(想去怼大佬,一定要拿出证据嘛~)。
-Xmx:JVM调优参数之一,该参数表示java堆可以扩展到的最大值。下面上案例:
在执行了26次之后,果断的后台报了堆内存溢出错误。
下面通过-Xmx JVM调优参数将堆内存调小至8m,再试试会发生什么呢?
操作基本和栈内存溢出的时候的案例一样,次数明显变小了,只调用了17次就出现了堆内存溢出错误了。
5.3.2 堆内存诊断的小实验
jps工具:查看当前系统中有哪些java进程
jmap工具:查看堆内存的占用情况jmap -heap 进程id
jconsole工具:图形化的工具,拥有多功能的监测功能,可以连续监测。
下面我们通过运行代码后通过jconsole可视化图形工具,来查看堆内存的使用情况。
上图我们可以看到,在我们创建10mb的数组对象时,内存使用有一定上升;然后在我们手动调用垃圾回收机制后,内存又得到了很大的释放。
6. JVM内存结构-方法区
6.1 定义
Java虚拟机中有一个被所有jvm线程共享的方法区。方法区有点类似于传统编程语言中的编译代码块或者操作系统层面的代码段。它存储着每个类的构造信息,譬如运行时的常量池,字段,方法数据,以及方法和构造方法的代码,包括一些在类和实例初始化和接口初始化时候使用的特殊方法。
方法区有个别称non-heap(非堆),可以看作是一块独立于堆的内存空间,是JVM规范中定义的一个概念,用于存储类信息、常量池、静态变量,JIT编译后的代码等数据,具体放在哪里,不同的实现可以放在不同的地方。
6.2 特点
(1)方法区与java堆一样,是各个线程共享的内存区域;
(2)方法区在JVM启动的时候被创建;
(3)方法区的大小,跟堆空间一样,可以选择固定大小或扩展;
(4)方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出溢出错误OutOfMemoryError;
(5)关闭JVM就会释放这个区域的内存。
6.3 JVM内存结构示意图
在JVM内存结构1.6的时候,方法区保存在内存结构中,叫做永久代,里面存储了运行时的常量池(包含串池StringTable)、类的信息、类加载器;
在JVM内存结构1.8的时候,方法区做为一个概念,保存在本地内存中,叫做元空间,里面存储了运行时的常量池、类的信息、类加载器,此时串池(StringTable)储存在堆之中。
原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/158018.html