JVM(Java Virtual Machine,Java 虚拟机)
JVM(Java Virtual Machine,Java 虚拟机)是 Java 程序运行的虚拟计算机,它是一个抽象计算机的概念,为 Java 程序提供了一个与平台无关的执行环境。JVM 负责加载 Java 类文件,执行字节码,并提供运行时环境,包括内存管理、垃圾回收等。
JVM 主要由四大部分组成:
- ClassLoader(类加载器):负责加载字节码文件(即class 文件)到内存
- Execution Engine(执行引擎):负责解释命令,交由操作系统执行。
- Native Interface(本地库接口):负责调用本地接口的。它的主要作用是调用不同语言的接口给JAVA用。
- Runtime Data Area(运行时数据区,内存分区):分为五部分:Stack(虚拟机栈),Heap(堆),Method Area(方法区),PC Register(程序计数器),Native Method Stack(本地方法栈)。
执行流程:
首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区。执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
类加载过程
- 类的生命周期:
类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。
-
类加载过程:
类加载过程大致如下:
1.加载:查找类的字节码文件,并将其加载到 JVM 中。
2.链接:包括验证、准备和解析三个阶段。- 验证:确保加载的类信息符合 JVM 规范,没有安全问题。
- 准备:为类的静态变量分配内存,并设置默认值。
- 解析:将类的符号引用转换为直接引用。
3.初始化:执行类的构造方法(
<clinit>()
方法)。 -
类加载器
类加载器负责在运行时(而非编译时)动态加载类,当一个类初次被引用的时候,它将被加载、链接、初始化。
JVM内置了三个类加载器,包括启动类加载器(BootStrap ClassLoader), 扩展加载器(Extension ClassLoader),应用加载器(Application ClassLoader)。除此之外,用户也可以自定义类加载器。(java9有改变)
-
启动类加载器 (BootStrap ClassLoader) –是虚拟机自身的一部分,C++实现,负责从 bootstrap classpath 中加载类(<JAVA_HOME>\jre\lib\目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库),有且只有一个 rt.jar 文件,该加载器具有最高优先级;
-
扩展加载器(Extension ClassLoader) – 它是Java实现的,独立于虚拟机,主要负责加载<JAVA_HOME>\jre\lib\ext\目录中或被java.ext.dirs系统变量所指定的路径的类库;
-
应用加载器(Application ClassLoader)– 它是Java实现的,独立于虚拟机,负责从用户定义的 classpath 中加载类,用户可以通过指定环境变量的方式定义该目录,如: java -classpath。一般情况下,如果没有自定义类加载器默认就是用这个加载器。
-
自定义类加载器:继承 ClassLoader抽象类,重写loadClass或findClass方法。(重写loadClass实现的,会打破双亲委派模型)
除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。
-
-
双亲委派模型
如果一个类加载器需要加载类,那么首先它会把这个类加载请求委派给父类加载器去完成,如果父类还有父类则接着委托,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。
所以,一般情况类加载会从应用程序类加载器委托给扩展类再委托给启动类,启动类找不到然后扩展类找,扩展类加载器找不到再应用程序类加载器找。(若有自定义类加载器,则从其开始)
运行时数据区(JVM内存)
JVM内存结构分为5大区域,程序计数器、虚拟机栈、本地方法栈、堆、方法区。
-
程序计数器(Program Counter Register):
- 每个线程都有一个程序计数器,用于存储当前线程执行的字节码指令的地址。它是线程私有的。
- 程序计数器用于记录当前线程执行的位置,以便在线程切换时能够恢复执行。
-
虚拟机栈(VM Stack):
- 每个线程都有自己的虚拟机栈,用于存储局部变量表、操作数栈、方法调用和返回地址。它是线程私有的。
- 虚拟机栈是java方法执行的内存结构,虚拟机会在每个java方法执行时创建一个“栈桢”,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。当方法执行完毕时,该栈桢会从虚拟机栈中出栈。其中局部变量表包含基本数据类型和对象引用;
-
本地方法栈(Native Method Stack):
- 每个线程都有自己的本地方法栈。它是线程私有的。
- 与虚拟机栈类似,但是用于存储本地方法(Native方法,如用C或C++编写的方法)的调用和返回地址。
-
方法区(Method Area)
- 方法区是所有线程共享的内存区域。
- 它用于存储类信息、常量、即时编译器编译后的代码等。运行时常量池是方法区的一部分。
- 方法区在JDK 1.8之前被称为永久代(PermGen),在JDK 1.8及以后版本中被元空间(Metaspace)所取代。
-
堆(Heap):
- 堆是JVM内存中最大的一块区域,被所有线程共享。
- 堆用于存储对象实例和对象数组。
- 堆是垃圾回收器(Garbage Collector)管理的主要区域,因此也被称为GC堆。
- JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
注意,不同版本的JDK,结构可能会有些差异。