前言
最近开始补jvm知识。作为一名Java开发,JVM是我们必须要学习了解的基础,也是通向高级及更高层次的必修课。但JVM的体系非常庞大,且术语非常多,所以初学者对此非常的头疼。
JVM 基础知识
其实一个java程序,首先会经过javac编译成.class文件,然后jvm会将其加载到方法区,执行引擎会执行这些字节码。执行时,会翻译成操作系统相 关的函数。JVM 作为 .class 文件的翻译存在,输入字节码,调用操作系统函数。
过程:Java 文件->java编译器>字节码->JVM->机器码
事实上JVM就是java虚拟机,它能识别 .class 后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。
JVM 知识模块
JVM所涉及的知识体系非常庞大,大致分为以下模块:内存结构、GC垃圾回收、类的加载、调优、类文件结构、执行引擎、监控等。但所有的知识体系都离不开内存。JVM其实就是一个虚拟化的操作系统,所以跟我们的操作系统有很大一部分相似。编译后的代码也是会运行再jvm虚拟中,并且这块也是由虚拟机管理,我们不需要关系内存的释放,它实现了自动垃圾回收机制。所以内存结构处于 JVM 中核心位置。也是属于我们入门 JVM 学习的最好的选择。
JVM 的运行时数据区域
运行时数据区的定义:其实JVM在运行java程序的时候会将它所管理的内存划分为若干个不同的数据区域。在JVM中,JVM内存主要分为:堆、程序计数器、方法区、虚拟机栈和本地方法栈等。同时按照线程的关系也可以分为:线程私有区域(一个线程拥有单独的一份内存区域)、线程共享区域(被所有线程共享,且只有一份)、直接内存(这个不是运行时数据去的一部分,但是会被频繁的使用。指的数没有被JVM虚拟化的部分)。
JAVA 方法的运行与虚拟机栈
虚拟机栈是线程运行 java 方法所需的数据,指令、返回地址。其实在我们实际的代码中,一个线程是可以运行多个方法的。 比如:
public class TestStack {
public static void main(String[] args) {
A();
}
private static void A(){
System.out.println("A被调用执行");
B();
}
private static void B(){
System.out.println("B被调用执行");
C();
}
private static void C(){
System.out.println("C被调用执行");
}
}
这段代码很简单,就是起一个 main 方法,在 main 方法运行中调用 A 方法,A 方法中调用 B 方法,B 方法中运行 C 方法。 在执行每个方法的时候都会打包成一个栈帧。
最后 main 方法运行完了,main 方法这个栈帧就 出栈了。 这个就是 Java 方法运行对虚拟机栈的一个影响。虚拟机栈就是用来存储线程运行方法中的数据的。而每一个方法对应一个栈帧。
虚拟机栈
栈的数据结构是先进后出,虚拟机栈的作用是在 JVM 运行过程中存储当前线程运行方法所需的数据,指令、返回地址。虚拟机栈是基于线程的,哪怕只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生 命周期是和线程一样的。 虚拟机栈的大小缺省为 1M,可用参数 –Xss 调整大小,例如-Xss250k。
官方文档地址:爪哇官方文档
栈帧:java程序的方法被调用的时候,都会创建一个栈帧(俗称的压 栈),当方法执行完后再出栈。栈帧大体包含四个区域(局部变量表、操作数栈、动态连接、返回地址)。
-
局部变量表:顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量)。首先它是一个 32 位的长度,主要存放我们的 Java 的八大基础数据 类型,一般 32 位就可以存放下,如果是 64 位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的 Object 对象,只需要存放它的一个引用地址即可。
-
操作数据栈:存放 java 方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的 java 数据类型,所以一个方法刚刚开始的时候,这个方法的操作数栈就是空的。操作数栈本质上是 JVM 执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。
-
动态连接:Java 语言特性多态。
-
返回地址:正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)同时,虚拟机栈这个内存也不是无限大,它有大小限制,默认情况下是 1M。 如果我们不断的往虚拟机栈中入栈帧,但是就是不出栈的话,那么这个虚拟机栈就会爆掉。
最后说了那么多也是云里雾里就看看代码里面是怎么一回事。
下面是一个普通的代码:
package com.li.work01;
public class DemoStack {
public static void main(String[] args) {
new DemoStack().work();
}
public int work(){
int a=3;
int b=4;
int sum=10*(a+b);
return sum;
}
}
然后我们用javap -c DemoStack .class反汇编命令就能得到如下代码
public com.li.work01.DemoStack();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/li/work01/DemoStack
3: dup
4: invokespecial #3 // Method "<init>":()V
7: invokevirtual #4 // Method work:()I
10: pop
11: return
public int work();
Code:
0: iconst_3
1: istore_1
2: iconst_4
3: istore_2
4: bipush 10
6: iload_1
7: iload_2
8: iadd
9: imul
10: istore_3
11: iload_3
12: ireturn
}
通过以上反汇编后(字节码指令)的就能清楚看出work方法里面:首先执行引擎会通过iconst_3字节码指令将常量3加载到操作数栈,接着通过istore_1从操作数栈取出来存储到局部变量表里面的1号位置。同样iconst_4与istore_2进行同样的操作。然后bipush指令将常量值10推送至栈顶。应为这里有括号所以会先执行括号里面的内容,通过iload_1和iload_2将局部变量表中的值取出放入操作数栈中,然后执行iadd指令进行加法指令(所有二元算数指令会从操作数栈中取出顶部的两个变量进行计算,计算结果自动加入到栈中)接着将常量10压入到栈中进行imul乘法运算。完成后需要通过istore_3指令将数值从操作数栈存储到局部变量表中,接着通过iload_3将一个局部变量加载到操作数栈中,最后通过ireturn返回(不管我们方法是否定义了返回值都会调用该指令,只是当我们定义了返回值时,首先会通过iload指令加载局部变量表的值并返回给调用者)。以上就是栈帧的运行原理了。
后续也会跟大家分享后面的章节(加油搬砖,少年们!)。