文章目录
- [JVM------Java虚拟机(Java Virtual Machine)](#JVM——Java虚拟机(Java Virtual Machine))
-
- 类的生命周期
- [JMM------JVM内存模型(Java Memory Modle)](#JMM——JVM内存模型(Java Memory Modle))
- JVM运行时的内存区域
JVM------Java虚拟机(Java Virtual Machine)
| 名称 | 全称 | 作用 |
|---|---|---|
| JDK | Java Development Kit | Java 开发工具包,包含编译器(javac)、打包工具 (jar)、调试工具(jdb)、JRE 等。是开发和运行 Java 程序的完整环境。 |
| JRE | Java Runtime Environment | Java 运行环境,包含 JVM + Java 核心类库,让程序可以 运行。 |
| JVM | Java Virtual Machine | Java 虚拟机,真正负责执行 .class 字节码,并将它翻译为对 应操作系统的机器指令。它在不同系统上有不同实现,从而屏蔽了底层操作系统的差异。 |
Java 的 一次编写,到处运行 特性:不同操作系统如 Windows、mac、Linux 对应不同的 JDK 版 本,以支持同一份代码编译文件(.class文件)在各种系统中都能运行。实际上,这种跨平台能力主要依靠的是 JDK 中的 JVM(Java 虚拟机) ------它负责在各自的操作系统上,把与平台无关的字节码文件(.class) 翻译成该系统能理解的机器指令。
类的生命周期
加载------>连接------>初始化------>使用------>卸载
- 加载:查找并加载二进制类的二进制数据(.class文件),即将硬盘上的二进制文件加载到jvm内存中。
- 连接:确定类与类之间的关系。
- 验证:.class 正确性校验。
- 准备:static静态变量分配内存并设置默认初始值(零值变量类型的默认值),不执行显式初始。化。这一阶段,jvm中只有类,没有对象。
- 解析:将符号引用转换为直接引用,例如把类名、方法名解析为内存地址。如将符号引用
com:yanqun.poio:student映射成实际的内存地址,内存地址的引用就是直接引用。
- 初始化:给静态变量赋真实的初始值。
- 使用:对象的初始化、对象的垃圾回收、对象的销毁。
- 卸载:当 类对应的 ClassLoader 被垃圾回收且 该类的所有实例已经被回收,类卸载是 JVM 的可选行为,标准 JVM 并不强制卸载类。
jvm结束生命周期的时机:
- 正常结束
- 异常结束/错误
- System.exit()
- 操作系统异常
JMM------JVM内存模型(Java Memory Modle)
用于定义变量(所有线程的共享变量,不能是局部变量)的访问规则。
JMM将内存分为两个区:
- 主内存区:存放真实的变量。
- 工作内存区:主内存中变量的副本,供各个线程使用。
注:
- 各个线程只能访问自己私有的工作内存(不能访问其他线程的工作内存,也不能访问主内存)。
- 不同线程之间,可以通过主内存间接的访问其他线程的工作内存。
- save:工作内存------>主内存
- load:主内存------>工作内存
线程之间交互数据的步骤:
- Lock(锁定):将主内存中的变量标记为某个线程独占,防止其他线程同时修改。
- Read(读取):从主内存读取变量值到线程的工作内存(Working Memory)。
- Load(载入):将 Read 读取到的值放入工作内存中的变量副本(local copy)。
- Use(使用):将工作内存中的变量副本传递给执行引擎(CPU)使用。
- Assign(赋值):将执行引擎得到的值赋给工作内存中的变量副本。
- Store(存储):将工作内存中的变量副本写回到主内存,但可能还没生效到最终主内存位置。
- Write(写入):将 Store 的值正式写入主内存,使其他线程可见。
- Unlock(解锁):解除 Lock,释放主内存变量的独占状态。
JVM要求以上8个动作必须是原子性的
(早期java 1.5之前对于64位的数据类型(long double)有些是非原子性协议,这样就可能出现 半读/半写 的情况。避免方式:可以通过关键字volatile解决。)
volatile和线程安全
【在早期的 Java 版本(1.5 之前),volatile 被用于防止 long 和 double 等 64 位数据类型的非原子性读写问题(即"半读/半写"现象)】
volatile 是 JVM 提供的一种轻量级同步机制,用于
- 保证多线程环境下变量的可见性
- 对该变量的写操作会立即刷新到主内存;
- 对该变量的读操作会从主内存中重新读取最新值;
- 禁止指令重排序:通过内存屏障来实现的
-
重排序:编译器或 CPU 为了提高执行效率,在不改变单线程程序最终结果的前提下,调整原子性指令执行顺序的行为。
-
volatile 保证在读写该变量时,程序指令的执行顺序不会被编译器或 CPU 重新排列,
从而避免多线程下由于指令乱序导致的可见性和安全性问题。 -
简单示例
javaint a = 0; boolean flag = false; // 线程1 a = 1; // ① flag = true; // ② // 线程2 if (flag) { // ③ System.out.println(a); // ④ } 结果:线程2打印结果为'0'与期望的'1'不同 原因:指令重排序了,先执行了'flag = true;',线程2识别到后立刻打印a的值'0',然后再执行'a = 1; ' 解决方法:'volatile boolean flag = false;',禁止指令重排序:保证 a = 1 一定发生在 flag = true 之前。 -
双重检查式的懒汉式单例模式
javaclass Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance == null) { // 1. 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 2. 第二次检查 instance = new Singleton(); // 3. 创建对象 } } } return instance; } } instance = new Singleton();不是一个原子操作,它会被编译为三步指令: 1分配内存地址 2调用构造方法初始化对象 3将引用赋值给 instance 而由于 指令重排序,可能会变成: 1分配内存地址 3将引用赋值给 instance 2调用构造方法初始化对象 这就意味着另一个线程可能在步骤2还没执行完时看到 instance 已经不为 null,从而拿到了一个"未初始化完成的对象"。 解决方案:禁止重排序--'private static volatile Singleton instance;'
-
volatile无法保证原子性和线程安全。
要想保证原子性/线程安全,可以使用原子包java.util.concurrent.atomic中的类(如 AtomicInteger, AtomicLong 等),该类能够保证原子性的核心是因为提供了compareAndSet()方法,该方法提供了cas算法(无锁算法,svn/git都使用的这个算法)
JVM运行时的内存区域

将JVM在运行时的内存,划分为了五个部分:
- 程序计数器(PC Register):存储当前线程正在执行的字节码地址,即下一条要执行的指令位置;如果是 native 方法(调用操作系统的底层 API),则值为空;是唯一不会产生内存溢出的区域。
- Java 虚拟机栈(JVM Stack):每个方法被调用时,JVM 会创建一个栈帧(Frame),用于存储:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)
- 方法返回地址
- 本地方法栈(Native Method Stack):为 JVM 调用本地(native)方法提供栈空间。
- 堆(Heap):
- 存放对象实例和数组。
- 是虚拟机区域中最大的一块。
- 是JVM 的垃圾回收器(GC)主要管理的区域。
- 随着 JVM 启动而创建,JVM 关闭而销毁。
- 堆的逻辑划分:新生代:老年代 = 1:2。
- 年轻代(Young Generation)
- 存放新创建的对象。
- 年轻代包含 eden,s0,s1,eden:s0:s1=8:1:1。
- 年轻代的使用率一般在90%,在使用时,只能使用一个eden和一个s区间。
- 当对象"熬过"几次 GC 后,会被晋升到老年代。
- 老年代(Old Generation)
- 存放生命周期较长的对象(比如单例、缓存对象)。
- 垃圾回收频率较低,但每次回收代价较高。
- 年轻代(Young Generation)
- 方法区(Method Area):存放
- 类的元数据(类名、父类、接口信息等)
- 字段、方法数据
- 字节码(Method 字节码)
- 运行时常量池(Runtime Constant Pool)
- 静态变量(static)
- 即时编译器(JIT)编译后的代码缓存等