Java虚拟机底层原理详细分析
作者:智由静生
Java虚拟机底层原理
Java虚拟机主要由以下三部分组成。
- 类装载子系统:java代码编译成class文件后,首先由类装载子系统加载到虚拟机内存中。
- 运行时数据区:就是俗称的虚拟机内存,主要包括我们熟悉的堆、栈、本地方法栈、方法区(元空间)、程序计数器
- 字节码执行引擎:最终java代码的真正执行是由字节码执行引擎来执行的。
虚拟机调优主要针对的是运行时数据区,也就是虚拟机内存。
一、堆
虚拟机调优主要针对的就是堆。当堆满了的时候,字节码执行引擎就会单独开启一个垃圾回收线程执行gc。
二、栈
当线程开始执行,java虚拟机立刻在栈中为该线程分配一块内存空间,这块内存是这个线程专属的,主要放线程自己的局部变量,每个线程都有自己的内存区域。
栈由一系列帧组成,一个方法对应一块栈帧内存区域。帧保存一个方法的局部变量、操作数栈、常量池指针。每一次方法调用创建一个帧,并压栈。
虚拟机内存中的栈的数据结构特性类似数据结构中的栈,满足先进后出(FILO)特性。当一个方法运行完,它对应的栈帧就被销毁。
虽然我们常说栈帧是用来放局部变量的,但实际上要复杂的多。栈帧内部主要包括:局部变量表、操作数栈、动态链接、方法出口四部分。下面以一段简单的代码为例,介绍一下这几部分的作用。
public int compute() { int a = 1; int b = 1; int c = (a + b) * 10; return c; }
java代码编译成的class文件打开是十六进制字符无法读,需要使用javap -c进行反编译。以上代码反编译的结果如下:
可以通过JVM指令手册查看每条命令的具体含义。
第0行iconst_1:将int类型常量1压入操作数栈。第1行istore_1:将int类型存入局部变量1。这一步就是先在局部变量表分配一块内存给变量a,从操作数栈中的常量1取出赋给变量a并存入局部变量表。这两行完成了源码中的第一行 int a = 1; 。同理,第2行和第3行完成了源码中的第二行Int b = 1; 。
第4行iload_1:从局部变量1装载int类型值。这里局部变量1就是a,这一步是把a中的值加载到操作数栈。第5行iload_2也是同样道理,从局部变量2也就是b装载int类型值到操作数栈。第6行iadd:就是进行加法运算,java虚拟机进行加减乘除运算时,会从操作数栈的栈顶弹出两个元素进行运算。这里也就是iload_1和iload_2装载的值。并且将运算完成的结果压回操作数栈。第7行bipush 10:将一个8位带符号整数压入操作数栈。这里就是后面的那个10。由于10也要占一个位置,所以这一条指令占了两个位置,也就没有了第8行。第9行imul:就是进行乘法运算,与第6行iadd类似。第10行istore_3:将前面运算表达式的结果从操作数栈中取出赋给变量c并存入局部变量表。从第4行到第10行完成了源码中Int c = (a + b) * 10;这一行的操作。
第11行iload_1:从局部变量3装载int类型值。页就是把局部变量c中的值取出加载到操作数栈。第12行ireturn:就很明显了,就是返回结果。
通过上面简单程序的例子,应该可以理解局部变量表和操作数栈的工作方式了吧。操作数栈简单的说就是程序在运行过程中,存放操作数的临时内存。局部变量如果是对象类型,那么对象值实际是存在堆里的,而栈的局部变量表实际上只是保存该对象在堆内存中的地址。
栈帧中除了局部变量表和操作数栈还有动态连接和方法出口。
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了方法调用过程中的动态连接。Class文件中的常量池中存有大量的符号引用,字节码中的方法调用指令就是以常量池里指向方法的符号引用作为参数的,这些符号的一部分会在类加载阶段或者第一次使用的时候被转化为直接引用,这种转化被称为静态解析。另一部分在每一次运行期间转化为直接引用,被称为动态连接。因为方法的执行对应着栈帧的入栈出栈,所以将所属方法的引用存放在栈帧中。
方法出口记录了上级调用方法的信息,比如compute()方法是main()方法调用的。当compute()方法执行完成返回时,需要返回到上级调用方法main()继续运行,此时需要知道一些调用方法main()的信息,比如返回到main()的哪个位置。方法出口就记录了相关信息。
三、程序计数器
程序计数器也是属于线程的,标识这个线程正在或者马上要执行的那行程序的行号。如上例中0-12就是行号,程序运行过程中,字节码执行引擎每执行一行代码,都会修改程序计数器的值。
为什么需要程序计数器呢?如果当前线程挂起了,让出了cpu去执行其它线程,当线程重新获得cpu时间片开始执行时,需要让线程知道应该执行到哪一行了,而程序计数器则记录了要执行程序的行号。
四、方法区(元空间)
class文件通过类装载子系统加载到虚拟机内存中,其实主要就是加载到方法区。方法区内主要是常量、静态变量、类元信息等。类元信息就是类的名称、修饰符、方法签名、方法中的指令码等。创建对象时,在对象头中会保存指向类元信息的地址指针。
JDK1.8之后改叫元空间,也不再使用虚拟机内存,而是使用直接内存。可以使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置元空间初始大小以及最大可分配大小。
1.如果不指定元空间的大小,默认情况下,元空间最大的大小是系统内存的大小,如果元空间一直扩大,虚拟机可能会消耗完所有的可用系统内存。
2.如果元空间内存不够用,就会报OOM。
3.默认情况下,对应一个64位的服务端JVM来说,其默认的-XX:MetaspaceSize值为21MB,这就是初始的高水位线,一旦元空间的大小触及这个高水位线,就会触发Full GC并会卸载没有用的类,然后高水位线的值将会被重置。
4.从第3点可以知道,如果初始化的高水位线设置过低,会频繁的触发Full GC,高水位线会被多次调整。所以为了避免频繁GC以及调整高水位线,建议将-XX:MetaspaceSize设置为较高的值,而-XX:MaxMetaspaceSize不进行设置。
到此这篇关于Java虚拟机底层原理详细分析的文章就介绍到这了,更多相关Java虚拟机底层原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!