Jvm结构
主要构成部分
- 类装载器子系统;
- 运行时数据区;
- 执行引擎;
- 本地方法接口;
- 垃圾收集模块;
jvm结构图
运行时数据区
上面为jvm的结构图,而今天主要分析的是运行时数据区,也就是运行内存,运行时数据区由堆、元空间、栈、本地方法栈、程序计数器构成,而由堆、元空间可线程共享,栈、本地方法栈、程序计数器则为线程独享。
栈
定义:
栈为线程私有,每个线程对应一个栈,栈是由栈帧组成,每个方法为一个栈帧,当线程结束时,栈被释放,不存在垃圾回收问题,栈的生命周期和线程一样。
组成:
- 局部变量
- 一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量;
- 由若干个Slot(32位)组成,一个Slot可以存放boolean、int、char、byte,若需要存放long和double,则需要两个Slot;
- 操作数栈
- 操作数栈遵循FILO(先进后出)原则;
- java虚拟机栈中的一个用于计算的临时数据存储区;
- 动态链接
- Class文件的常量池中存在大量的符号引用,字节码的调用指令就是使用这种符号作为参数,这些符号在程序运行时会转化为直接引用,称为动态链接;
- 在程序运行时,子译码在内存中的位置称为动态链接;
- 方法出口
- 方法的出口有两种情况:一种是正常返回,执行引擎遇到返回方法的字节码指令,此为正常完成出口;另一种是执行过程中遇到了异常,这种称为异常完成出口;
演示:
下面使用一段代码演示一下数据在栈中的过程:
代码如下:
package com.xiaolinzi;
/**
* @Author xiaolinzi
* @Date 2020/2/14 2:24 下午
* @Email xiaolinzi95_27@163.com
* @Description
*/
public class JvmStackDemo {
private int add() {
int a = 5;
int b = 6;
int c = a + b;
return c;
}
public static void main(String[] args) {
JvmStackDemo demo = new JvmStackDemo();
int add = demo.add();
}
}
使用 javap -c 命令 对java字节码进行反汇编,生成文件如下:
Compiled from "JvmStackDemo.java"
public class com.xiaolinzi.JvmStackDemo {
public com.xiaolinzi.JvmStackDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int add();
Code:
0: iconst_5 --将int类型常量5压入栈
1: istore_1 --将int类型值存入局部变量1
2: bipush 6 --常数6到操作数
4: istore_2 --将int类型值存入局部变量2
5: iload_1 --从局部变量1中装载int类型值
6: iload_2 --从局部变量2中装载int类型值
7: iadd --执行int类型的加法
8: istore_3 --将int类型值存入局部变量3
9: iload_3 --从局部变量3中装载int类型值
10: ireturn --从方法中返回int类型的数据
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/xiaolinzi/JvmStackDemo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method add:()I
12: istore_2
13: return
}
从反汇编的文件中我们可以看出,这个线程有两个栈帧(每个方法一个栈帧),栈帧main和栈帧add,我们可以先分析一下栈帧add来分析一下数据在栈帧的的过程,通过查询jvm指令集可以将栈帧add翻译成以下描述:
- 程序计数器为0的那一行表示:将int类型的5压入操作数栈;
- 程序计数器为1的那一行表示:将int类型值(也就是5)从操作数栈取出来,放入局部变量1中(也就是局部变量a);
- 程序计数器为2的那一行表示:将常数6压入操作数栈;
- 程序计数器为4的那一行表示:将int类型值(也就是6)从操作数栈取出来,赋值给局部变量2中(也就是局部变量b);
- 程序计数器为5的那一行表示:从局部变量1(局部变量a)中将int类型值(5)压入操作数栈;
- 程序计数器为6的那一行表示:从局部变量2(局部变量b)中将int类型值(6)压入操作数栈;
- 程序计数器为7的那一行表示:在操作数栈中执行int类型的加法(使用栈顶两个数进行计算);
- 程序计数器为8的那一行表示:将上一步计算结果存入局部变量3(局部变量c);
- 程序计数器为9的那一行表示:从局部变量3(c)中取出计算结果压入操作数栈;
- 程序计数器为10的那一行表示:从方法中返回int类型的数据。
本地方法栈
用native修饰如: System.currentTimeMillis();调用的是本地方法库。
程序计数器
一个指针,指向方法区中的字节码。指向正在执行或即将执行的指令代码。如下图标出的数字即为程序计数器:
元空间
用于存放类所有的字段和方法字节码,包括一些特殊的构造函数,接口定义等。简单来说就是所有定义的方法信息都储存在该区域,包括:静态变量、常量、类信息(构造方法/接口定义)、运行时常量池。元空间称为Non-Heap(非堆),与java堆分开。
堆
虚拟机启动时创建,用户存放实例对象,几乎所有的对象(包括常量池)都分配在堆上,当对象无法获取到空间时会抛出OutOfMemoryError异常。同时此为垃圾收集器管理的主要区域。可通过Xmx Xms 分别设置最大、最小堆。
堆分为两个部分:
- 新生代:
新生区分为Eden区(伊甸园区)和survivor区(幸存者区),所有类都是在Eden区被new出来。Survivor区分为0区(From区)和1区(To区)。当Eden区内存满了之后,又需要继续创建新的对象,就会发生MinorGc,Eden区就会把不用的对象进行销毁,剩余的对象移动到survivor区中的0区,如果0区满了,将会移动到1区,如果1区满了,这时候0区和1区发生角色互换,1区将对象移动到0区,每次互换分代年龄都会+1,当分代年龄达到15时候,就会移动到老年代。
- 老年代:
新生区的对象经过多次回收到大老年代,若老年代内存满了之后,就会触发FullGc,这时候就会Stop THe World,虚拟机除了垃圾收集的线程都会被挂起,进行垃圾回收。也就是我们平常遇到的程序卡顿。