一、JVM的基础介绍
JVM 是 Java Virtual Machine 的缩写
- 定义:JVM是java程序的核心执行引擎,负责将Java源代码编译成可执行的字节码,并在运行是负责解释执行字节码或将其编译成本地机器代码。
- 作用:JVM使得Java程序具有跨平台性,即“一次编写,到处运行”。Java源代码被编译成字节码后,JVM可以在任何平台上执行这些字节码。
1.1 Java文件是如何被运行起来的
比如现在我们写好了一个HelloWorld.java文件,那我们的JVM他是不认识这个文件的,他需要编译,把这个文件成为他会读的二进制文件的HelloWorld.class。
其中JVM主要由以下几个部分组成:
- 类加载器(Class Loader):
- 负责加载Java类的字节码到JVM中。
- 主要的类加载器包括:根类加载器(加载Java核心类)、扩展类加载器(加载JRE扩展目录中的类)和系统类加载器(加载用户类路径上的类)。
- 运行时数据区:
- 方法区(Method Area):线程共享的内存区域,存储每个类的结构信息,如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容等。
- 堆(Heap):所有线程共享的一块内存区域,用于存放对象实例和数组。JVM的垃圾回收器主要管理这部分内存。
- Java虚拟机栈(Java Virtual Machine Stack):线程私有的内存区域,每个方法执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈(Native Method Stack):为虚拟机使用到的Native方法服务,与Java虚拟机栈类似,但主要是为Native方法服务。
- 程序计数器(Program Counter Register):线程私有的内存区域,指示当前线程所执行的字节码的行号指示器。
- 执行引擎(Execution Engine):
- 负责执行字节码,可以通过解释器执行或JIT(Just In Time)编译器编译为本地代码执行。
1.2 JVM的工作原理总结
- 代码编译:Java源代码(.java文件)被编译成Java字节码(.class文件)。
- 类加载:JVM将Java字节码文件加载到内存,并对其进行解析和验证。
- 执行:JVM对Java字节码进行解释执行或编译为本地代码执行。
- 内存管理:JVM负责管理Java程序执行过程中所使用的内存,包括堆、栈、方法区等。
- 垃圾回收:JVM提供自动垃圾回收机制,定时回收不再使用的对象并释放内存。
1.3 JVM的结构图
1.4 简单的小案例
一个动物类
public class Animal {
public String type;
public Animal(String type) {
this.type = type;
}
public void isType(){
System.out.println("Animal's type is " + type);
}
}
一个mian方法
public class MethodDemo {
public static void main(String[] args) {
Animal animal = new Animal("dog");
animal.isType();
}
}
执行 main 方法的步骤如下:
- 编译好 MethodDemo .java 后得到 MethodDemo .class 后,执行 MethodDemo .class,系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 MethodDemo .class 的二进制文件,将 MethodDemo 的类信息加载到运行时数据区的方法区内,这个过程叫做 MethodDemo 类的加载
- JVM 找到 MethodDemo 的主程序入口,执行 main 方法
- 这个 main 中的第一条语句为Animal animal = new Animal("dog"),就是让 JVM 创建一个 Animal 对象,但是这个时候方法区中是没有 Animal 类的信息的,所以 JVM 马上加载 Animal 类,把 Animal 类的信息放到方法区中
- 加载完 Animal 类后,JVM 在堆中为一个新的 Animal 实例分配内存,然后调用构造函数初始化 Animal 实例,这个 Animal 实例持有 指向方法区中的 Animal 类的类型信息 的引用
- 执行animal.isType();时,JVM 根据 Animal 的引用找到 Animal 对象,然后根据 Animal 对象持有的引用定位到方法区中 studAnimal nt 类的类型信息的方法表,获得 isType() 的字节码地址。
- 执行 isType()
二、 类加载器的介绍
类加载器负责.class 文件的,它们在文件开头会有特定的文件标示,将 class 文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,并且 ClassLoader 只负责 class 文件的加载,而是否能够运行则由 Execution Engine 来决定
2.1 类加载器的流程
从类被加载到虚拟机内存中开始,到释放内存总共有 7 个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析三个部分统称为连接
2.1.1 加载
- 将 class 文件加载到内存
- 将静态数据结构转化成方法区中运行时的数据结构
- 在堆中生成一个代表这个类的 java.lang.Class 对象作为数据访问的入口
2.1.2 连接
- 验证:确保加载的类符合 JVM 规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件,其实就是一个安全检查
- 准备:为 static 变量在方法区中分配内存空间,设置变量的初始值,例如 static int a = 3 (注意:准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是对象初始化时赋值的)
- 解析:虚拟机将常量池内的符号引用替换为直接引用的过程(符号引用比如我现在 import java.util.ArrayList 这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)
2.1.3 初始化
类的静态变量赋予初始值,并执行静态代码块的过程。
在初始化阶段,JVM会按照以下步骤执行:
- 为静态变量赋值:根据类定义中的声明,为静态变量赋予初始值。如果静态变量在声明时就已经被赋予了初始值,那么JVM会使用这个值;如果没有,则使用数据类型的默认值(如
int
类型的默认值为0,boolean
类型的默认值为false
等)。 - 执行静态代码块:按照静态代码块在类定义中出现的顺序,依次执行每个静态代码块。静态代码块只能访问定义在静态代码块之前的静态变量,对于定义在静态代码块之后的静态变量,虽然可以赋值,但不能在静态代码块中访问其值。
2.1.4 卸载
GC将无用对象从内存中卸载
3. 类加载器的工作机制
类加载器采用双亲委派模型(Parent Delegation Model)来加载类:
- 当一个类加载器需要加载一个类时,它会首先将这个请求委派给它的父类加载器去加载。
- 如果父类加载器无法加载这个类,子类加载器才会尝试自己去加载。
这种机制的好处包括:
- 避免类的重复加载:确保一个类只被加载一次,节省内存空间。
- 确保类的安全性:通过控制类的加载过程,防止恶意类的加载和执行。
- 提高代码的稳定性和可靠性:保证核心类库的一致性和稳定性。
三、运行时数据区的介绍
运行时数据区(Runtime Data Area)是Java虚拟机(JVM)在执行Java程序时所使用的内存区域,用于存储程序运行过程中的数据。这些区域包括方法区(Method Area)、堆(Heap)、虚拟机栈(Java Virtual Machine Stack)、本地方法栈(Native Method Stack)和程序计数器(Program Counter Register)。
1. 方法区(Method Area)
- 作用:方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。在JDK 8之前,方法区被称为永久代(PermGen),而从JDK 8开始,逐步替换为元空间(Metaspace)。
- 特点:方法区是线程共享的内存区域,不会进行垃圾回收,但元空间可以进行自动调整大小。
- 实现:方法区的具体实现依赖于虚拟机实现,例如HotSpot VM使用元空间来替代永久代。
2. 堆(Heap)
- 作用:堆是Java虚拟机中最大的一块内存区域,用于存放Java对象实例和数组等数据结构。
- 特点:堆是线程共享的内存区域,可以动态地扩展和缩减。堆的大小可以通过命令行参数(如-Xms和-Xmx)进行控制。
- 分代收集:堆通常被分为新生代和老年代,新生代又分为Eden区、From Survivor区和To Survivor区。对象首先在Eden区分配内存,经过多次GC后仍存活的对象会被移到老年代。
3. 虚拟机栈(Java Virtual Machine Stack)
- 作用:虚拟机栈是线程私有的内存区域,用于存储局部变量、方法参数、返回值和操作数等信息。
- 特点:每个方法被调用时,都会创建一个栈帧(Stack Frame)并入栈,方法执行完毕后出栈。栈帧中包含了方法的局部变量表、操作数栈、动态链接、方法出口等信息。
- 异常:如果线程请求的栈深度超过虚拟机栈允许的最大深度,会抛出StackOverflowError异常;如果栈空间无法继续分配,会抛出OutOfMemoryError异常。
4. 本地方法栈(Native Method Stack)
- 作用:本地方法栈与虚拟机栈类似,但它用于执行本地方法(Native Method),即使用C或C++等语言编写的方法。
- 特点:本地方法栈也是线程私有的,其实现依赖于虚拟机实现。
5. 程序计数器(Program Counter Register)
- 作用:程序计数器是一块较小的内存空间,用于记录当前线程执行的字节码指令的地址或下一条需要执行的指令地址。
- 特点:程序计数器是线程私有的,每个线程都有自己独立的程序计数器,它们之间互不影响。程序计数器不会进行垃圾回收,也不会发生内存溢出的情况。
四、 GC垃圾回收机制
GC(Garbage Collection,垃圾回收)是Java虚拟机(JVM)中的一项重要功能,用于自动管理堆内存中不再使用的对象,释放其占用的内存空间。
GC通过标记和回收无效对象来实现内存的回收和释放,以避免内存泄漏和溢出。在JVM中,GC操作是一个自动化过程,由JVM自动执行,开发者无需手动干预。
了解堆内存:
类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:
- 新生区 (Young GC)
- 伊甸区(Eden Space):
- 伊甸区是堆内存中的一个主要区域,用于存放新生成的对象。
- 大多数新创建的对象首先会被分配到伊甸区。
- 当伊甸区空间不足以存放新对象时,会触发一次Minor GC(也称为Young GC),以清理不再被使用的对象,并可能将存活的对象移动到幸存区。
- 幸存区(Survivor Spaces):
- 图片中提到的“幸存0区”和“幸存1区”都属于幸存区,但通常这些区域被标记为Survivor Space 0(或From Space)和Survivor Space 1(或To Space),它们是轮流使用的。
- 在Minor GC之后,伊甸区中存活的对象会被移动到其中一个幸存区(如果幸存区已满,则先清空)。
- 在下一次Minor GC时,当前存活的对象会被移动到另一个空的幸存区,同时清空刚刚使用过的幸存区。
- 如果对象在多次Minor GC之后仍然存活,它们最终会被移动到老年代(养老区)。
- 伊甸区(Eden Space):
- 养老区(Tenured Generation 或 Old Space):
- 养老区用于存放长时间存活的对象。
- 随着Minor GC的进行,对象逐渐从伊甸区和幸存区转移到养老区。
- 当养老区空间不足时,会触发Full GC(也称为Major GC),以清理整个堆空间中的无用对象。
- 永久存储区(Permanent Space 或 Metaspace in JDK 8+):
- 在JDK 8之前,永久存储区用于存放类的元数据(如类的结构、方法信息等)以及常量池。
- 从JDK 8开始,永久存储区被元空间(Metaspace)所取代,元空间使用的是本地内存,因此其大小只受物理内存的限制。
- 永久存储区/元空间也可能需要进行垃圾收集,但其触发条件与堆内存的GC不同。
4.1 GC的算法
- 标记-清除算法(Mark-Sweep):
- 最基本的垃圾回收算法之一。
- 分为标记阶段和清除阶段。
- 标记阶段:遍历所有对象,并标记所有需要回收的对象。
- 清除阶段:将所有被标记的对象进行删除。
- 缺点:效率低下,空间利用率低,容易产生内存碎片。
- 复制算法(Copying):
- 将可用内存空间分为两块,每次只使用其中一块。
- 当这一块内存用完后,GC将其中存活的对象复制到另一块未使用的内存中,然后重新启动程序。
- 优点:效率高,不会产生内存碎片。
- 缺点:浪费了一半的内存空间。
- 标记-整理算法(Mark-Compact):
- 类似于标记-清除算法,但在标记阶段完成后,将所有需要回收的对象移到内存的一端,再将其余未被标记的对象移到另一端。
- 优点:避免了内存碎片问题。
- 缺点:效率相对较低。
- 分代算法(Generational):
- 将内存分为几个不同的区域(通常是新生代、老年代和持久代),并根据对象的生命周期分配到相应的区域。
- 对于新生代,采用复制算法进行回收;对于老年代,采用标记-清除算法或标记-整理算法进行回收。
4.2 GC的触发条件
GC的触发条件通常包括以下几种:
- 新生代满:当新生代中的Eden区满时,会触发Young GC。
- 老年代满:当老年代空间不足时,会触发Full GC。
- 显式调用:通过System.gc()或Runtime.getRuntime().gc()显式触发GC,但这种方式并不保证JVM会立即执行GC。
- 其他条件:如大对象分配、元数据区满等也可能触发GC。
4.3 GC的优化
为了优化GC性能,可以采取以下措施:
- 合理配置堆大小:根据应用程序的需求和特点,合理配置堆大小以减少GC的频率和时间。
- 选择合适的GC算法:根据应用程序的特点选择合适的GC算法以提高GC效率。
- 减少对象创建:通过减少对象的创建和销毁来降低GC的压力。
- 使用对象池:对于频繁创建和销毁的对象,可以使用对象池来复用对象以减少GC的开销。