JVM 由哪些部分组成,运行流程是什么?
JVM 是什么
Java Virtual Machine Java程序的运行环境(java二进制字节码的运行环境)
好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收机制
JVM的组成
我们首先看一下一个程序的完整执行流程:
所以可以看出JVM主要是由四部分组成:
- ClassLoader(类加载器)
- Runtime Data Area(运行时数据区,内存分区)
- Execution Engine(执行引擎)
- Native Method Library(本地库接口)
JVM运行流程
由此我们说一下JVM的大体的运行流程:
- 类加载器(ClassLoader)把Java代码转换为字节码
- 运行时数据区(Runtime Data Area)把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行能力的执行引擎运行
- 执行引擎(Execution Engine)将字节码翻译为底层系统指令,再交由CPU 执行去执行,此时需要调用其他语言的本地库接口(Native Method Library)来 实现整个程序的功能。
其中在运行时数据区中有很多组件组成,这个也是JVM的重中之重
运行数据区的组成
运行时数据区主要是由以下几部分组成:
- 程序计数器
- 堆
- 虚拟机栈
- 本地方法栈
- 方法区/元空间
下面来介绍一下程序计数器
程序计数器
在Java中,任务的执行时多线程并发执行的,但是这个并发指的是多个线程交替执行。在任何的一个时间点上,一个处理器只会执行一个线程,如果当前线程它所分配的执行时间用完了,就会被'挂起'。处理器会切换到另外一个线程上来进行执行并且这个线程执行时间用完了,接着处理器就会用来执行被挂起的线程
那么问题就会出现了,当前处理器如何知道上一次线程执行到什么位置呢?程序计数器,可以解决这个问题,程序计数器会回到当前这个线程上一次执行的行号,案后继续向下执行
程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址
javap -verbose xx.class 打印堆栈大小,局部变量的数量和方法的参数
程序计数器是JVM规范中唯一一个没有规定出现OOM的区域,所以这个空间也不会进行GC
堆
线程共享的区域:主要是用来保存对象实例、数组等,党对中没有内存空间可以分配给实例,也无法再扩展时就会抛出OOM(OutOfMemoryError)异常
其中堆主要是分为两个部分:年轻代、老年代和永久代(Java8将其移动到本地内存中)
- **年轻代:**年轻代主要是被划分为三部分,Eden区和两个大小严格相同的Survivor区,根据JVM的策略,在经过几次垃圾收集后,让人然存活在Survivor会移动到老年代
- **老年代:**老年代主要是保存生命周期比较长的对象
- **永久代:**永久代保存的时类信息、静态变量、常量、编译后的代码
永久代的大小是固定的,并且在运行时不容易进行调整。这意味着如果应用加载的类过多,或者类的信息占用空间过大,很容易就会填满永久代的固定空间,从而引发 OOM 错误,而在 Java 8 中,将永久代移动到本地内存并重新开辟为元空间,在一定程度上缓解了 OOM 问题。这是因为元空间的使用不再受限于 JVM 堆的大小。元空间使用的是本地内存,其空间相对较大,并且可以根据实际需要动态扩展
虚拟机栈和本地方法栈
Java Virtual machine Stacks (java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈,先进后出
- 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
栈解决的是程序运行的问题,栈里面存的是栈帧,栈帧里面存的是局部变量 表、操作数栈、动态链接、方法出口等信息
本地方法栈与栈功能相同,本地方法栈执行的是本地方法,一个Java调用非 Java代码的接口。
垃圾回收是否涉及栈内存?
垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放
栈内存分配越大越好吗?
未必,默认的栈内存通常为1024k 栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程 数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半
方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈内存溢出情况:栈帧过多导致栈内存溢出,典型问题:递归调用
元空间
元空间是各个线程共享的内存区域,主要是存储类的信息、运行时常量池。在虚拟机启动时创建,关闭时销毁。如果方法区域中的内存无法满足分配请求,则会抛出OOM
常量池可以看成一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型等信息
查看字节码结构(类的基本信息、常量池、方法定义) javap -v xx.class
比如下面是一个Application类的main方法执行,源码如下:
java
public class Application {
public static void main(String[] args) {
System.out.println("hello world");
}
}
找到类对应的class文件存放目录,执行命令: javap -v Application.class 查看字节码结构
java
D:\code\jvm-demo\target\classes\com\heima\jvm>javap -v
Application.class
Classfile /D:/code/jvm
demo/target/classes/com/heima/jvm/Application.class
Last modified 2023-05-07; size 564 bytes
//最后修改的时间
MD5 checksum c1b64ed6491b9a16c2baab5061c64f88 //签名
Compiled from "Application.java" //从哪个源码编译
public class com.heima.jvm.Application //包名,类名
minor version: 0
major version: 52
//jdk版本
flags: ACC_PUBLIC, ACC_SUPER //修饰符
Constant pool: //常量池
#1 = Methodref
#6.#20
<init>":()V
#2 = Fieldref
#21.#22
// java/lang/Object."
//
java/lang/System.out:Ljava/io/PrintStream;
#3 = String
#23
#4 = Methodref
#24.#25
// hello world
//
java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class
#26
com/heima/jvm/Application
#6 = Class
#7 = Utf8
#8 = Utf8
#9 = Utf8
#10 = Utf8
#11 = Utf8
#27
<init>
()V
Code
LineNumberTable
//
// java/lang/Object
LocalVariableTable
#12 = Utf8
#13 = Utf8
#14 = Utf8
#15 = Utf8
#16 = Utf8
#17 = Utf8
#18 = Utf8
#19 = Utf8
#20 = NameAndType
#21 = Class
#22 = NameAndType
out:Ljava/io/PrintStream;
#23 = Utf8
#24 = Class
#25 = NameAndType
(Ljava/lang/String;)V
#26 = Utf8
#27 = Utf8
#28 = Utf8
#29 = Utf8
this
Lcom/heima/jvm/Application;
main
([Ljava/lang/String;)V
args
[Ljava/lang/String;
SourceFile
Application.java
#7:#8
#28
#29:#30
hello world
#31
#32:#33
// "<init>":()V
// java/lang/System
//
// java/io/PrintStream
// println:
com/heima/jvm/Application
java/lang/Object
java/lang/System
out
下图,左侧是main方法的指令信息,右侧constant pool 是常量池
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.heima.jvm.Application(); //构造方法
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method
java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/heima/jvm/Application;
public static void main(java.lang.String[]); //main方法
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field
java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method
java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 7: 0
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "Application.java"
下图,左侧是main方法的指令信息,右侧constant pool 是常量池 main方法按照指令执行的时候,需要到常量池中查表翻译找到具体的类和方法地址去执行
运行时常量池:常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址