一、JVM运行流程
如图:
JVM由四个部分构成:
- 1.类加载器
加载类文件到内存 - 2.运行时数据区
写的程序需要加载到这里才能运行 - 3.执行引擎
负责解释命令,提交操作系统执行 - 4.本地接口
融合不同编程语言为java所用,如Java程序驱动打印机
二、JVM内存区域
其中, 方法区 是各个线程共享的一块逻辑内存区域,它用于存储已经被虚拟机加载的类型信息、常量、静态变量、即使编译器编译后的代码缓存等数据。方法区的实现在 Java8后的HotSpot虚拟机采用的是元空间(元空间在本地内存)。
在Java8之前,HotSpot虚拟机采用的是永久代(永久代在堆内存)来实现的方法区,这样HotSpot的垃圾收集器能够像管理Java堆内存一样管理这部分内存,省去了为方法区编写内存管理代码的工作。但是其他虚拟机实现方法区时不存在永久代的概念。
三、类加载机制
Java程序不是一次性加载所有类到内存中的。JVM在运行时按需加之类。类加载机制负责将.class文件从磁盘、网络等地方的资源加载到JVM的方法区,并最终在堆内存中创建对应Class对象,作为访问该类型元数据的入口。
1.类加载的生命周期
- 1.加载
- 任务:查找并加载类的.class文件
- 结果:在方法区(jdk1.7)/元空间(jdk1.8)创建类运行时数据结构,并在堆内创建一个代表该类的一个java.lang.Class对象作为访问这些数据的入口。
java.lang.Class 对象是 Java 类的运行时表示。它包含了类的元数据(如类名、字段、方法、构造函数等信息),并且提供了访问这些元数据的方法。通过 Class 对象,程序可以在运行时动态地获取类的信息、创建类的实例、调用方法、访问字段等。
- 2.验证:
- 目的:确保被加载类的字节码文件是安全、合法的,符合JVM规范的,不会危害JVM安全的。
- 检查内容:
- 文件格式验证
- 元数据验证(语义分析:是否有父类、是否继承 final 类、方法覆盖是否合法等)
- 字节码验证(最复杂:检查方法体中的指令逻辑是否合法、类型转换是否安全、跳转指令是否指向合理位置等)
- 符号引用验证(发生在解析阶段,检查符号引用能否被正确解析)
- 3.准备:
- 目的:为类的静态变量分配内存(在方法区/元空间)并设置初始零值。
- 4.解析:
- 目的:将常量池的符号引用改成直接引用
- 符号引用:一组符号描述引用的目标
- 直接引用:可以是直接指向目标的指针、相对偏移量或能间接定义到目标的句柄。
- 解析目标:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符。
- 5.初始化:
- 目标:执行类的初始化代码(() 方法)。
- () 方法:由编译器自动收集类中所有类变量的赋值动作和静态语句块 (static{} 块) 中的语句合并生成。
- 触发时机:首次主动创建一个类时
- 父类优先: JVM 保证一个类的 () 方法执行前,其父类的 () 方法必须已执行完毕。
- 线程安全:JVM 会确保 () 方法在多线程环境下被正确地加锁同步执行(只有一个线程能执行)。
- 6.使用
- 7.卸载
2.类加载器
负责实现“加载”阶段。JVM 内置三类重要的类加载器,它们之间存在层次关系(双亲委派模型的基础):
- 1.启动类加载器(Bootstrap ClassLoader):
- 是JVM自身的一部分
- 加载 JAVA_HOME/lib 目录下的核心类库(如 rt.jar, resources.jar, charsets.jar)或 -Xbootclasspath 参数指定的路径下的类。
- 2.扩展类加载器(Extension ClassLoader / Platform ClassLoader - JDK9+):
- 加载 JAVA_HOME/lib/ext 目录或 java.ext.dirs 系统变量指定路径下的类库。
- 3.应用程序加载器(Application ClassLoader / System ClassLoader):
- 加载用户类路径 (ClassPath) 下的类库。开发者编写的类通常由此加载器加载。
- 是程序中 ClassLoader.getSystemClassLoader() 的默认返回值。
四、双亲委派
当类加载器收到类加载请求时:
- 1.委派父加载器:不会立刻加载职工类,而是先委派给父加载器来加载。
- 2.递归委派:直到订层的启动类加载器
- 3.父加载器尝试加载:
- 如果父加载器可以完成加载任务(在其负责的范围内找到了该类),则成功返回该类。
- 如果父加载器无法完成加载任务(在其负责范围内找不到该类),则子加载器才会尝试自己去加载。
- 如果子加载器也找不到,则抛出 ClassNotFoundException。
优势:
- 1.避免重复加载
- 2.保证核心类库安全:例如,即使你在 ClassPath 下写了一个 java.lang.Object 类,由于双亲委派,请求会最终委派给 Bootstrap Loader,它加载了真正的核心 Object 类,你的自定义 Object 不会被加载。
- 3.保证基础类的统一性:确保核心类库(如 java.lang.String)对所有子加载器可见且一致。
打破双亲委派的情况:
- SPI (Service Provider Interface): 如 JDBC。核心接口在 rt.jar 由 Bootstrap Loader 加载,但数据库厂商的实现类在 ClassPath 下需要由 AppClassLoader 加载。为了解决这个矛盾,引入了线程上下文类加载器 (Thread Context ClassLoader, TCCL),它通常默认是 AppClassLoader。核心代码(如 DriverManager)在需要加载 SPI 实现时,使用 TCCL 来加载。
- 热部署/热替换: 如 Tomcat, OSGi。需要同一个类的不同版本共存。自定义类加载器需要能独立加载同一类名的不同版本,不遵循父优先原则,而是先自己尝试加载。
- 代码热替换 (HotSwap): 在调试时替换修改过的类,需要类加载器能重新加载类。
五、垃圾回收机制
1.判断对象是否存活
1.引用计数法:
- 原理:为每个对象维护一个计数器,当对象被引用时计数器+1,引用失效时计数器-1,计数器为0时对象可回收。
- 缺点:无法解决循环引用问题(A 引用 B,B 引用 A,但 A 和 B 都不再被外部引用,计数器不为0)。
2.可达性分析算法
- 原理: 通过一系列称为 “GC Roots” 的根对象作为起始点集,从这些节点开始向下搜索,搜索过程走过的路径称为 “引用链 (Reference Chain)”。如果一个对象到 GC Roots 没有任何引用链相连(即不可达),则证明此对象是不可用的,可以被回收。
- GC Roots 对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 本地方法栈中 JNI(即 Native 方法)引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象(如字符串常量池里的引用)。
- Java 虚拟机内部的引用(如基本数据类型对应的 Class 对象,常驻的异常对象 NullPointerException、OutOfMemoryError 等,系统类加载器)。
- 所有被同步锁(synchronized 关键字)持有的对象。
- 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
- 主流 JVM 使用此方法。
3.引用类型:
- 1.强引用:强引用不能被回收。
- 2.软引用:当内存不足时才会被回收。
- 3.弱引用:下一次GC就被回收。
- 4.虚引用:最弱的引用。无法通过虚引用获取对象实例。唯一目的是对象被回收时收到一个系统通知。用于跟踪对象被垃圾回收的活动。
4.垃圾回收算法
- 1.标记清除法:
- 步骤:遍历GC Roots标记所有可达对象,清除所有未被标记的对象。
- 缺点:慢,空间碎片化。
- 2.复制算法:
- 步骤:将可用内存按容量划分为大小相等的两块(A 和 B)。每次只使用其中一块(如 A)。当 A 用完了,就将 A 中还存活的对象复制到 B 上,然后一次性清理掉 A 上的所有空间。交换角色(现在使用 B)。
- 优点:块,无大量不连续碎片内存。
- 缺点:内存缩小为原来的一半,代价高昂。适用于对象存活率低的场景(如新生代的 Eden 和 Survivor 区)。
- 3.标记整理法:
- 步骤:遍历GC Roots标记所有可达对象,清除所有未被标记的对象,并且将存活对象都移动到空间的一端。
- 优点:没有内存碎片。
- 缺点:移动存活对象并更新引用地址需要 STW (Stop-The-World) 时间较长。适用于对象存活率高的场景(如老年代)。
- 4.分代收集算法:
- 核心思想:根据对象存活周期的不同将堆划分为新生代 (Young Generation) 和老年代 (Old Generation)。
- 新生代特点: 对象创建频繁,存活率低。
- 回收算法: 主要采用复制算法(Minor GC / Young GC)。
- 老年代特点: 对象存活周期长,存活率高。
- 回收算法: 主要采用标记-清除或标记-整理算法(Major GC / Full GC)。