目录
前言:首先需要理解的是JVM是一种规范,在不同的平台上,如果有对应平台的虚拟机,那么我们编写的Java程序都是可以在对应平台的虚拟机上边运行的,这也就是Java语言具有的跨平台能力。
代码分析
请看这段代码:
public class Person {
public int work()throws Exception{//一个方法对应一个栈帧
int x =1;// iconst_1 、 istore_1
int y =2;// iconst_2 、 istore_2
int z =(x+y)*10;
return z;
}
public static void main(String[] args) throws Exception{
Person person = new Person();
person.work(); //这个 3 字节码的行号(针对 本方法偏移)
person.hashCode();//方法属于本地方法 ---本地方法栈 4
}
}
编译完成后的字节码指令:
C:\Users\ASUS>javap -c A:\Android\SeniorEngineer\01\ref-jvm3\out\production\ref-jvm3\ex1\Person.class
Compiled from "Person.java"
public class ex1.Person {
public ex1.Person();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int work() throws java.lang.Exception;
Code:
0: iconst_1 // 将值为1的变量放入操作数栈
1: istore_1 // 存储命令,将操作数栈顶的数据放入到局部变量表的下表为1的位置
2: iconst_2 // 将值为1的变量放入操作数栈
3: istore_2 // 存储命令,将操作数栈顶的数据放入到局部变量表的下表为2的位置
4: iload_1 // 加载命令,将局部变量表中的下标为1的位置加载到操作数栈
5: iload_2 // 加载命令,将局部变量表中的下标为2的位置加载到操作数栈,此时操作数栈有两个数,栈顶为2;
6: iadd // 首先将操作数栈里边的数据拿出来,算完之后再保存在操作数栈;
7: bipush 10 // 这个数太大,不能用const命令,此时栈顶为10;int型该方式只能把-1,0,1,2,3,4,5(分别采用iconst_m1,iconst_0, iconst_1, iconst_2, iconst_3, iconst_4, iconst_5)送到栈顶。 对于int型,其他的数值请使用push系列命令(比如bipush)
9: imul // 将操作数栈的数取出来,做乘法,然后重新放入操作数栈。
10: istore_3 // 将上边的结果存到局部变量表的第三个位置
11: iload_3 // 方法与方法之间,返回的数据必须经过操作数栈,因为这是属于操作的数据;将局部变量表中的下标为3的位置加载到操作数栈
12: ireturn // 在程序执行期间,栈桢与栈桢之间,能返回的数据都是在操作数栈中。
public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
0: new #2 // class ex1/Person
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method work:()I
12: pop
13: aload_1
14: invokevirtual #5 // Method java/lang/Object.hashCode:()I
17: pop
18: return
}
为什么少了第八行指令?0,1,2,3,4,5,6这些都是本方法的字节码的偏移量,这是因为bipush指令比较大,占据了两个位置,所以导致少了第八行指令。
完成出口:这个就是work方法的出口,main方法的地址也是从0开始的,work方法也是从0开始,work在main方法里边有自己的地址。所以这就是完成出口。
程序计数器:可能会重复,这个没有问题,因为虚拟机执行的时候,只会执行最顶上的栈帧。
动态链接: 与多态有关,指向运行时常量池的方法引用。
注意:如果是静态方法,那么局部变量表就没有this,因为对象对唯一的,就是这个类对象,this指的是当前对象,用来确定是哪个对象。
Java中无法直接操作一个线程。在HotSpot中,本地方法栈和虚拟机栈用的是同一块内存,没有做区分。程序计数器只能记录虚拟机栈里边的运行的方法,本地方法栈里边的他无法记录,因为这个已经和Java没有关系了。因为里边的表现形式不再是字节码了。
运行时数据区中的其他区域:
请看下边这段代码:
public class ObjectAndClass {
static int age=18;//todo 静态变量(基本数据类型)放入方法区,准备阶段赋值0,初始化赋值18
final static int sex=1;//todo 常量(基本数据类型)放入方法区,准备阶段赋值就是1
final static ObjectAndClass object = new ObjectAndClass();//todo 成员变量指向(对象)在类加载的时候不会执行
//构造方法 -》ObjectAndClass object = new ObjectAndClass();
private boolean isKing;//todo 成员变量 放在哪里???
public static void main(String[] args) {//启动一个线程,创建一个虚拟机栈,数据结构,单个,压入一个栈帧
int x=18;//todo 局部变量(基本数据类型)放入栈帧
long y=1;//todo 局部变量(基本数据类型)放入栈帧
ObjectAndClass lobject = new ObjectAndClass();//todo 局部变量 引用放入局部变量表 对象放入堆。注意,这个成员变量也需要完成构造方法,所以还会去创建一个对象。
lobject.isKing=true;// isKing跟随对象,堆空间
lobject.hashCode();//方法中调用方法 本地方法(C++语言写 JNI)
ByteBuffer bb = ByteBuffer.allocateDirect(128*1024*1024);//todo 直接分配128M的直接内存
//这个地方 分配在哪里 128M
}
}
整体的内存分配情况:存在对象指向对象的情况。
一个类的生命周期:
直接内存
底层与unsafe这个类有关,这个类已经跨越了虚拟机的规范,JVM已经将内存虚拟化,将内存区域进行划分,但是unsafe却绕过来这个限定。如果使用unsafe来申请内存,直接操作内存。该方式绕过了JVM垃圾回收。需要自己手动进行回收。优势是可能能够提升效率。
如果想用直接内存,又担心回收的问题,可以使用这种方式,直接内存使用这种方式申请没有问题:
ByteBuffer bb = ByteBuffer.allocateDirect(128*1024*1024);//todo 直接分配128M的直接内存
里边使用了一个幽灵引用进行垃圾回收(待确定)。
JVM中的各种参数是由JVM根据操作系统的情况来进行评估,然后来确定一个合适的值。
JVM整体的情况
一般来说,一个对象经历过15次的回收,就会被放入老年代。
JHSDB是一个可视化的,可以来直接看对象的大小的工具。
参考文章:[三] java虚拟机 JVM字节码 指令集 bytecode 操作码 指令分类用法 助记符 - 腾讯云开发者社区-腾讯云