摘要:JVM是一种计算设备的规范,是一种虚构的计算机,通过在实际计算机上模拟各种计算机功能来实现。本文分享自华为云社区《[[云驻共创]JVM内存模型的探知之旅](https://bbs.huaweicloud.com/b...)》,作者:达美乐古牌。一、JVM简介1.1JVM是什么?JVM是Java虚拟机(JavaVirtualMachine)的缩写。它是计算设备的规范。它是在实际计算机上模拟各种计算机功能而实现的虚拟计算机。1.2JVM的优点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(∩_∩)哦哈哈~)。仔细想想,对我们会有什么影响?带来的好处就老多了,比如:(1)消除了类型之间的耦合关系;(2)可更换性;(3)可扩展性;(4)界面;(5)灵活性;(6)简化;1.3JVM、JRE、JDK的关系1.3.1JVM简介JVM是JavaVirtualMachine的缩写,是Java虚拟机,模拟虚拟计算机。它通过在不同的计算机环境中模拟计算功能来实现这一点。引入Java虚拟机后,Java语言在不同平台上运行时不需要重新编译。其中,Java虚拟机屏蔽了与特定平台相关的信息,使得Java源程序编译完成后可以在不同平台上运行,达到“一次编译,到处运行”的目的。Platform,即平台无关,关键点是JVM。1.3.2JRE简介JRE是JavaRuntimeEnvironment的缩写。就是Java运行环境,也就是操作系统运行Java应用程序的环境。它内部包含JVM,这意味着JRE只负责运行现有的Java源程序。它不包括开发工具JDK,也不包括JDK内部的编译器、调试器等工具。1.3.3JDK简介JDK是JavaDevelopmentKit的缩写,是Java开发工具包,是整个Java程序开发的核心。主要包括JRE,Java系统类库,以及编译运行Java程序的工具,如javac.exe、java.exe命令工具等。1.4JVM常用实现Oracle(Hotspot、Jrockit)、BEA(LiquidVM)、IBM(J9)、taobaoVM(淘宝专用,为Hotspot深度定制)、zing(垃圾回收机制非常快,达到1毫秒左右)。1.5JVM的内存结构图当Java程序编译成.class文件时==”类加载器(ClassLoader)==”将字节码文件加载到JVM中;1.5.1方法区和堆方法中存放的主要方法是类信息(类属性、成员变量、构造函数等),堆(创建的对象)。1.5.2当虚拟机栈、程序计数器、本地方法栈中的对象调用一个方法时,该方法会在虚拟机栈、程序计数器、本地方法栈上运行。1.5.3当执行引擎执行方法中的代码时,代码由执行引擎中的“解释器”执行;方法中被频繁调用的代码,即热点代码,由“即时编译器”执行,其执行速度非常快。1.5.4GC(GarbageCollectionMechanism)GC就是回收堆内存中没有被引用的对象,可以手动也可以自动回收。1.5.5本地方法接口由于JVM不能直接调用操作系统的函数,只能通过本地方法接口调用操作系统的函数。2.JVM内存结构——程序计数器2.1程序计数器的定义ProgramCounterRegister就是程序计数器(register),用来记录下一条Jvm指令的执行地址。2.2运行步骤javap主要用于运行JVM,javap-c用于反汇编java代码。下图是先编译demo.java,再执行javap-cdemo的输出结果:第一列是二进制字节码,即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所示。Frameistoolarge。3.2.2栈内存溢出小实验3.2.2.1栈帧太多的小实验无限递归调用(栈帧太多)的小实验,在main方法中method1()方法无限调用自己,那么会发生什么?答案很明显,程序崩溃了,出现了栈内存溢出错误,如下图:-Xss:该参数指定了每个线程的虚拟机栈的大小;那么我们尝试将虚拟机堆栈的大小设置为256k会发生什么?我们发现,当我们调整虚拟机栈的大小时,执行了4315次方法后,内存溢出了。在调整虚拟机堆栈之前,我们有23268次。显然我们可以通过-Xss参数来调整虚拟机栈的大小。控制内存溢出。3.2.2.2线程运行诊断小实验的想象场景。老板在疯狂输出。忙吗,当然要精??确,一分钟几千万,O(∩_∩)O哈哈~)?Linux环境下:后台运行Java字节码(.class)文件Stack_6:**注意:不管nohup命令的输出是否重定向到终端,输出都会追加到nohup.out文件中当前目录。**(1)通过top命令,查看进程(相当于任务管理器),发现有一个可疑的家伙占用了100%的CPU。这样可以让其他小伙伴玩的开心,秒改错。..注意:top命令,查看哪个进程占用CPU过多,返回进程号。(2)使用psH-eopid,tid,%cpu|grep命令过滤任务管理器中的内容。注:psH-eopid,tid,%cpu|grep是用ps命令查看哪个线程占用CPU过多,并返回进程id,其中pid为进程id,tid为线程id,%cpu是CPU使用率;found罪魁祸首找到了,这一串吓人的红色。..(3)通过jstack进程id查看20389问题进程中的具体情况。注:jstack进程id是通过jstack命令定位具体占用CPU过多的代码。注意jstack命令查找到的threadid是16进制的,需要进行转换;里面有一堆执行的代码,那我们怎么找出具体是哪个人做的呢?在上图中,我们可以发现正在做事的线程是20441,那么我们用计算器将20441转换成16进制的4FD9再试。只有一个真理。通过对比nid(threadid)4fd9,我们发现名为thread1的线程一直在运行(RUNNABLE状态),我们发现问题出在Stack_6.java文件的第11行。..现在我们回到源码,在Stack_6文件的第11行,我们发现这里一直在执行死循环,终于找到你了,幸好我没有放弃,内斯~4.JVM内存结构-本地方法栈4.1定义由于有时候Java本身不能直接与操作系统底层交互,而有时候Java又需要调用本地的C或C++方法,所以此时本地方法栈应运而生,它们是一类带有native关键字的方法。5.JVM内存结构-堆5.1Heap的定义堆:new关键字创建的对象存放在堆中5.2特点5.2.1线程共享堆存放的对象是线程共享的,所以需要考虑线程安全问题的。5.2.2有垃圾回收机制。因为堆中存储的对象存储了大量的对象,所以配备了一个小助手——垃圾收集机制(自动和手动传输都可以调~)。5.3堆内存溢出小实验5.3.1修改堆内存大小参数小实验继续想象一个场景,某大佬开发一段代码时(当然大佬很有信心,我写的代码怎么可能可能吗?有问题,不存在...),但是测试跑不起来。为了安全起见,还是默默地做一下测试吧,安全第一。但是机器内存这么大,老板肯定跑过很多次了,没问题。这不是找茬。..还是默默的改一下机器的参数再试(想上boss的,一定要拿出证据~)。-Xmx:JVM调优参数之一,代表java堆可以扩展到的最大值。情况如下:执行26次后,后台果断报堆内存溢出错误。接下来通过-XmxJVM调优参数将堆内存减少到8m,再试一次,会发生什么?操作与栈内存溢出的情况基本相同,次数明显少了,只调用了17次就出现了堆内存溢出错误。5.3.2堆内存诊断小实验jps工具:查看当前系统存在哪些java进程jmap工具:查看堆内存占用情况jmap-heapprocessidjconsole工具:图形化工具,多功能监控。接下来我们运行代码,使用jconsole可视化工具查看堆内存的使用情况。从上图可以看出,当我们创建一个10mb的数组对象时,内存占用有一定程度的增加;然后我们手动调用垃圾回收机制后,内存被释放了很多。6.JVM内存结构——方法区6.1定义Java虚拟机中有一个所有jvm线程共享的方法区。方法区有点类似于传统编程语言中的编译代码块或者操作系统级别的代码段。它存储了每个类的构造信息,比如运行时常量池、字段、方法数据以及方法和构造方法代码,包括类和实例初始化以及接口初始化时用到的一些特殊方法。方法区也称为非堆(non-heap),可以看做是独立于堆的内存空间。是JVM规范中定义的一个概念,用于存放类信息、常量池、静态变量、JIT编译代码等数据放在哪里,不同的实现可以放在不同的地方。6.2特点(1)方法区和java堆一样,是各个线程共享的内存区域;(2)方法区是在JVM启动时创建的;(3)方法区的大小和堆空间一样,是可以固定的。或扩张;(4)方法区的大小决定了系统可以保存多少个类。如果系统定义了过多的类,导致方法区溢出,虚拟机也会抛出溢出错误OutOfMemoryError;(5)关闭JVM会释放这个区域的内存。6.3JVM内存结构示意图在JVM内存结构1.6中,方法区存放在内存结构中,称为永久代,存放运行时常量池(包括字符串池StringTable)、类信息、类加载器;在JVM内存结构为1.8的时候,方法区是一个存放在本地内存中的概念,叫做元空间,存放运行时常量池、类信息、类加载器。这时,字符串池(StringTable)就存放在堆当中。点击关注,第一时间了解华为云的新鲜技术~
