JVM是什么
JVM全称为Java Virtual Machine(Java虚拟机)。它是运行Java字节码(.class文件)的虚拟计算机。它屏蔽了底层操作系统和硬件的差异,实现了"一次编写,到处运行"(Write once,Run Anywhere)
通俗理解:想象JVM是一个万能翻译官+执行指挥官。我们写的Java代码(.java文件)先被编译成一种中间语言(字节码, .class文件)。
JVM的工作就是:
1.加载:找到这些.class文件(就像指挥官拿到作战计划书)
2.翻译&执行:把字节码指令一条条翻译成我们电脑CPU能懂的语言(本地机器码),并指挥CPU执行(就像翻译官把作战计划翻译成士兵能听懂的指令并指挥行动)
3.管理资源:给程序运行过程中需要的各种"东西"(对象、数据)分配内存空间(堆、栈等),并在不需要时清理掉(垃圾回收--后续详细讲解)
JVM运行流程&核心部件
JVM的执行流程
程序在执行之前先要把java代码转换成字节码(class文件),JVM首先需要把字节码通过一定的方式--类加载器 (ClassLoader)--把文件加载到内存中 运行时数据区 (RunTime Data Area),而字节码文件是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器--执行引擎 (Execution Engine)--将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口--本地库接口(Native Interface)--来实现整个程序的功能,这就是这4个主要组成部分的职责与功能

总结
核心流程: .java -> (编译) -> .class -> (类加载器加载) -> (存入运行时数据区) -> (执行引擎翻译执行) -> (可能需要本地库接口调用系统功能) -> CPU执行
四大核心部件:
1.类加载器(ClassLoader):图书管理员。负责把 .class文件从磁盘(或网络等)找到,并"搬进"内存里JVM的特定区域(方法区)
2.运行数据区(Runtime Data Area):JVM的工作内存。程序运行时的所有数据都存放在这里。它又细分为几个重要的"房间",下面第三点详细讲
3.执行引擎(Execution Engine):真正的翻译官+指挥官。它读取加载到内存中的字节码(.class文件),把它翻译成CPU指令,让CPU干活。核心包含:
·解释器(Interpreter):逐条解释执行字节码(启动快,执行慢)
·即时编译器(JIT Compiler):把热点代码编译成本地机器码缓存起来,下次直接执行机器码(编译慢,执行超快)
·垃圾回收器(Garbage Collector,GC):清洁工。负责自动回收不再使用的对象占用的内存(后面重点讲)
4.本地库接口(Native Interface):外交官。当Java代码需要调用操作系统底层功能(比如读写文件、网络操作)或者用C/C++写的库时,就通过它来沟通
JVM运行时数据区(内存布局--重点)

想象JVM运行程序需要一块大内存,这块内存被划分成几个功能区:
在讲解功能区之前,我们先讲解一下何为线程私有?
线程私有区域是JVM为每个线程独立分配的内存空间,其生命周期与线程完全同步。这类区域的数据仅对所属线程可见,其他线程无法直接访问或修改。线程私有区域的存在,本质是为了解决多线程环境下执行状态隔离的问题
执行状态隔离的基础
多线程通过时间片轮转执行,CPU在任意时刻仅能运行一个线程。若所有线程共享一个程序计数器,切换时将无法区分各线程的执行位置,导致逻辑混乱。因此:
·独立计数器:每个线程拥有独立的程序计数器,记录自身执行位置
·互不干扰:线程A的计数器修改不会影响线程B的执行流程
1.程序计数器(Program Counter Register)--线程私有
程序计数器是线程私有区域的核心组件
·作用:记住当前线程执行到哪一行代码了。就像书签,告诉你现在读到书的哪一页哪一行
·特点:每个线程独享一个,速度最快,永远不会内存溢出(OOM)
2.Java虚拟机栈(Java Virtual Machine Stack)--线程私有
·作用:存放方法执行时的临时数据。每当调用一个方法时,就为这个方法创建一个栈帧压入栈顶。方法执行完,栈帧就弹出。栈帧中包含了如下图所示四部件:

局部变量表:存放方法的参数和方法内部定义的局部变量(基本类型值、对象引用)
操作数栈:方法执行过程中进行计算的临时工作区(类似CPU的寄存器)
动态链接:指向方法区中该方法的"身份信息"
方法返回地址:方法执行完后,该回到哪里继续执行
通俗理解:就像每个线程专属的"工作台"。你在工作台上处理当前任务(方法)需要的材料和工具(局部变量、操作数栈...)。任务做完,工作台清空。
异常:StackOverFlow(栈深度太深,比如无限递归)、OutOfMemoryError(线程太多,栈空间总大小不够)
3.本地方法栈(Native Method Stack)--线程私有
作用:和Java虚拟机栈非常相似,但它是为执行本地方法(用其他语言如C/C++写的方法)服务的
通俗理解:专门给那些 "外语(非Java)方法"准备的工作台
4.Java堆(Java Heap)--线程共享
作用:存放几乎所有你new出来的对象实例和数组。是JVM管理的内存中最大的一块

结构(分代):为了更高效地管理对象和进行垃圾回收,堆被划分为:
新生代(Young Generation):新创建的对象大部分在这里。又分为:
·Eden区(伊甸园):对象"出生"的地方。新对象先放这里
·Survivor区(幸存者区):两个相同大小的区(S0,S1)。经过一次GC(Eden区满触发Minor GC)还存活的对象,会被移到Survivor区。在Survivor区熬过多次GC(默认15次)的对象,晋升到老年代
老年代(Old Generation/Tenured):存放存活时间较长或较大的对象。新生代中熬过多次GC的对象会晋升到这里。老年代触发Major GC/Full GC
参数:-Xms(堆初始大小),-Xmx(堆最大大小)
通俗理解:对象的大本营和养老院。新对象在"出生地"(Eden),活下来的去"大本营"(Survivor),长寿的进"养老院"(Old)
异常:OutOfMemoryError:Java heap space(堆内存不够用了)
5.方法区(Method Area)--线程共享
作用:存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等。是各个线程共享的逻辑区域
实现:
·JDK7及之前:叫永久代(PermGen),是堆的一部分
·JDK8及之后:叫元空间(Metaspace),移到本地内存(非JVM堆内存)管理,大小只受本地内存限制,避免了PermGen的内存溢出(OOM)问题。同时,字符串常量池被移到了堆中
包含:运行时常量池(是方法区的一部分),存放编译期生成的字面量(字符串字面量在JDK8后移到了堆里、final常量、基本类型值)和符号引用(类名、字段名、方法名等描述信息)
通俗理解:JVM的"知识库"和"蓝图库"。存放所有类的定义(类名、有哪些方法字段)、常量(如final int Max=100)、静态变量(如static int count)等元数据信息。JDK8后,这个"知识库"搬到了更大的"外部仓库"(本地内存)
异常:OutOfMemoryError:Metaspace(元空间大小不足,通常是类加载太多)
垃圾回收(Garbage Collection,GC)--重点中的重点

核心思想:自动回收 程序中 不再使用的对象所占用的内存。程序员不用手动free/delete
为什么需要GC?
堆内存有限,不断创建对象会耗尽内存。GC就是"清洁工",及时清理"垃圾"(死亡对象)
如何判断对象"已死"(可回收)?
·引用计数法(简单但不用):给对象加计数器,有引用+1,引用失效-1,为0即死。
问题:无法解决循环引用(A引用B,B引用A,但两者都没用了)
·可达性分析法(JVM采用):从一些称为"GC Roots"的根对象出发(如:栈中局部变量引用的对象、方法区静态变量引用的对象、方法区常量引用的对象、本地方法栈JNI引用的对象),看哪些对象通过这些引用链是可达的。不可达的对象就是可回收的"垃圾"。
因此,JVM的垃圾回收(GC)通过可达性分析算法判断对象是否存活,其核心思想是从一组称为"GC Roots"的根对象出发,通过引用关系遍历对象图,无法被访问到的对象视为垃圾。
示例图如下:

引用类型:影响GC行为
·强引用(Strong Reference):Object obj=new Object();最常见。只要强引用在,对象绝不会被回收
·软引用(Soft Reference):描述有用但非必需的对象(如缓存)。内存不足时,在内存溢出(OOM)发生前,会被回收。回收后还不够才会导致内存溢出(OOM)
·虚引用(Phantom Reference):最弱。无法通过它获取对象。唯一作用:对象被回收时收到一个系统通知。用于跟踪对象回收
垃圾回收算法(GC的思想指导)
标记-清除(Mark-Sweep)

步骤:1.标记 所有可达对象 2.清除所有未标记对象
缺点:效率不高(标记和清除都耗时);产生内存碎片(清除后内存不连续)
比喻:在房间里标记所有有用的东西,然后把没标记的都当垃圾扔掉。结果:房间空了但东西散落各处(碎片)
复制(Copying)

步骤:把内存分成大小相等的A、B两块。只用A块。GC时,把A块存活的对象复制到B块,然后清空整个A块。下次用B块
优点:简单高效,无碎片
缺点:浪费一半内存;对象存活率高时,复制代价大(因此,不适用于老年代)
应用:新生代(Young Gen)! HotSpot优化:不按1:1分,而是Eden(80%)+S0(10%)+S1(10%)。Minor GC时,把Eden +一个Survivor(如S0)存活的对象复制到另一个空的Survivor(如S1),然后清空Eden和S0。下次Minor GC时,S1变成From区,S0变成To区。对象在Survivor区来回"蹦跶"(复制)熬过一定次数(默认15)后进入老年代。如果Survivor放不下,直接进入老年代(分配担保)

比喻:你有两个一模一样的房间A和B。平时只在A房间活动。大扫除时,把A房间有用的东西都搬到空房间B,然后把A房间彻底清空。以后就在B房间活动。下次大扫除再搬回A
标记-整理(Mark-Compact)

步骤:1.标记所有可达对象 2.让所有存活对象向一端移动 3.清理掉边界外的内存
优点:无碎片;适合老年代(对象存活率高)
缺点:移动对象成本较高
比喻:标记有用的东西,然后把它们都推到房间的一边堆整齐,最后把另一边空出来的区域彻底清理干净
分代收集(Generational Collection):JVM实际采用的策略

思想:根据对象存活时间的不同,将堆内存划分为新生代和老年代
·新生代:对象"朝生夕死",死亡率高。采用复制算法(效率高,配合Eden/Survivor设计内存利用率也高)
·老年代:对象存活时间长、存活率高,采用标记-清除 或标记-整理算法
Minor GC vs Full GC(Major GC)
·Minor GC:发生在新生代的垃圾收集。非常频繁,速度相对快。触发条件:Eden区满
·Full GC/Major GC:通常指发生在老年代的垃圾收集(有时也会回收整个堆,包括新生代、老年代、方法区)。速度慢很多(可能慢10倍+),应尽量避免。触发条件:老年代空间不足、方法区(元空间)不足、调用System.gc()(建议而非强制)、某些GC策略触发(如分配担保失败)
垃圾收集器(GC算法的具体实现)
就像不同品牌的"清洁机器人",实现不同的清扫策略
·Serial(串行):最古老。单线程工作。GC时,暂停所有用户线程(STW-Stop The World)。适合Client模式或者单核小内存
·ParNew(并行) :Serial的多线程版本。主要用于新生代。GC时多线程并行收集,但仍有STW(暂停部分用户线程)。是唯一能与CMS配合工作的新生代收集器
·Parallel Scavenge (吞吐量优先):新生代收集器。目标:达到可控制的吞吐量(CPU运行用户代码时间/(CPU运行用户代码时间+GC时间))。
可自适应调整参数(-XX:+UseAdaptiveSizePolicy)
Serial Old:Serial的老年代版本。单线程,标记-整理算法
Parallel Old:Parallel Scavenge的老年代版本。多线程,标记-整理算法。JDK6后出现,让"吞吐量优先"组合(Parallel Scavenge+Parallel Old)名副其实
CMS(Concurrent Mark Sweep)-并发低停顿:老年代收集器。目标:最短回收停顿时间。过程分4步:
1.初始标记(Initial Mark):STW,标记GC Roots直接关联的对象(很快)
2.并发标记(Concurrent Mark):并发(与用户线程一起),标记所有可达对象(耗时长)
3.重新标记(Remark):STW,修正并发标记期间因用户线程运行导致变动的那部分标记(比初始标记稍长)
4.并发清除(Concurrent Sweep):并发(与用户线程一起),清除死亡对象
优点:停顿时间短(主要在初始标记和重新标记)缺点:
·对CPU资源敏感(并发阶段占用线程)
·无法处理"浮动垃圾"(并发清除阶段新产生的垃圾)
·使用标记-清除算法,会产生内存碎片
G1(Garbage-First)-全能选手:JDK9+默认!面向服务端。目标:在可预测的停顿时间模型下实现高吞吐量
核心思想:将堆划分为多个大小相等的独立区域(Region)。优先回收垃圾最多(Garbage-First)的Region
特点:
·同时管理新生代和老年代(逻辑分代,物理不分)
·Mixed GC模式:可以同时回收新生代和部分老年代Region
·整体基于标记-整理,局部(Survivor复制)基于复制,避免碎片
·建立可预测停顿模型:允许用户指定在M毫秒的时间片段内,GC时间不超过N毫秒
步骤(类似CMS但更复杂):初始标记(STW)-> 并发标记 -> 最终标记(STW) ->筛选回收(部分STW)
一个对象的一生:"我出生在Eden区,和很多小伙伴玩耍。Eden满了,我们经历第一次"大筛选"(Minor GC),活下来的被送到Survivor区(S0/S1)。在Survivor区,我们来回搬家(S0< - >S1),每次搬家都是一次筛选(GC)。熬过15次(默认)筛选后,我们晋升到老年代安家。在老年代,我们生活了很久,直到整个养老院(老年代)快满时,经历一次更彻底的"大扫除"(Full GC)。如果这次扫除后还活着的伙伴太多导致空间不够,JVM就会报错(OOM:Java heap space),宣告我们中一些长寿者的终结

类加载机制
核心:JVM如何把 .class文件加载到内存,变成可用的Java类
生命周期:加载(Loading)->链接(Linking:验证Verification->准备Preparation->解析Resolution)->初始化(Initialization)->使用(Using)->卸载(Unloading)
·详细步骤:
1.加载:"图书管理员"的工作
·通过类全限定名获取 .class的二进制字节流
·将字节流转化为方法区的运行时数据结构
·在堆中生成一个代表该类的java.lang.Class对象,作为访问方法区数据的入口
2.验证:检查 .class文件是否符合规范,是否安全(如文件格式、字节码、符号引用验证)。确保不会危害JVM
3.准备:在方法区为类变量(static变量)分配内存并设置默认初始值。如pubilc static int value=123;此阶段value是0,不是123。final static 常量在此阶段会直接赋值为指定值
4.解析:将常量池内的符号引用(如类名、方法名等描述符)替换为直接引用(内存地址指针或句柄)。可以理解为把"名字"解析成具体的"地址"
5.初始化:真正开始执行类中定义的Java代码!主要是执行<clinit>()方法(由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并而成)。父类初始化优先于子类。这是触发类加载的最后一步
双亲委派模型(Parents Delegation Model)
双亲委派模型是类加载器的层级关系和工作原则
站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader
站在Java开发人员的角度来看,类加载器就应当划分得更细致一些。自JDK 1.2以来,Java一直保持着三层类加载器、双亲委派的类加载架构器
类加载器的层级结构(自底向上)
1.启动类加载器(Bootstrap ClassLoader)
·实现:C++编写,是JVM原生代码的一部分
·职责:加载Java核心类库(如rt.jar、charsets.jar等),路径为JAVA_HOME/lib
·特点:无父类加载器,是类加载体系的根节点
2.平台类加载器(Platform ClassLoader,JDK9+)
·前身:JDK8及之前称为扩展类加载器(Extension ClassLoader)
·实现:Java编写,继承自ClassLoader
·职责:加载JAVA_HOME/lib/ext目录或java.ext.dirs系统变量指定的JAR包
3.应用程序类加载器(Application ClassLoader)
·别名:系统类加载器(System ClassLoader)
·职责:加载用户类路径(ClassPath)下的类,是Java应用的默认类加载器
4.自定义类加载器(Custom ClassLoader)
·实现:用户通过继承ClassLoader类实现
·用途:用于隔离类加载环境(如OSGi)、热部署、加密解密等场景

工作原则:
·一个类加载器收到加载请求,先不自己加载
·它把这个请求委派给父类加载器去完成
·每一层都如此,最终请求传送到顶层的启动类加载器
·只有当父加载器反馈自己无法完成(在自己的搜索范围没找到)时,子加载器才会尝试自己加载
优点 :
·避免重复加载:保证一个类在JVM中全局唯一。父加载器加载过了,子加载器就不用再加载
·安全性:防止核心API被篡改。核心类(如java.lang.Object)由启动类加载器加载,用户无法通过自定义类加载器覆盖核心类
破坏双亲委派模型:有些场景需要打破这个规则
经典案例 :SPI -> JDBC

·问题:
JDBC的Driver接口(第三方实现接口)定义在rt.jar(理应由Bootstrap加载器加载)。
而数据库驱动实现(如mysql-connector.jar)在ClassPath下(应由Application ClassLoader 加载)。根据双亲委派,Application ClassLoader会委派给父加载器(Platform/Ext -> Bootstrap),但Bootstrap不认识ClassPath下的驱动实现类!导致驱动加载失败
·解决:JDBC使用线程上下文类加载器(Thread Context ClassLoader)。
DriverManager(在rt.jar,Bootstrap加载)在加载驱动时,获取当前线程的上下文类加载器(默认是Application ClassLoader),然后用这个加载器去加载数据库驱动实现类。
相当于"老子(Bootstrap)让儿子(Application ClassLoader)去干一件老子干不了(不认识)但儿子能干的事情"。这违反了"向上委派"的原则,是典型的破坏双亲委派
Java内存模型(JMM)
核心目标:定义Java程序中多线程访问共享变量的规则,屏蔽不同硬件/OS内存访问差异,保证Java程序在并发环境下的可见性、原子性、有序性
主内存 vs 工作内存
主内存 :所有共享变量都存储在主内存中
工作内存 :每个线程都有自己的工作内存。工作内存保存了该线程使用到的变量的主内存副本拷贝
规则:
·线程对变量的使用操作(读、写)都必须在工作内存中进行,不能直接读写主内存
·不同线程之间不能直接访问对方工作内存中的变量
·线程间变量值的传递必须通过主内存来完成
通俗理解:想象主内存是公司的共享数据库。每个线程(员工)有自己本地的记事本(工作内存),里面记录了TA需要用到的数据库数据的副本。员工只在自己的记事本上写写画画(操作工作内存)。当需要和别人同步数据时,必须先把本地修改写回共享数据库(store/write),然后别人再从数据库读取最新数据到自己的记事本(read/load)
内存间交互操作:JMM定义了8种原子操作来完成工作内存和主内存的交互(lock、unlock、read、load、use、assign、store、write)。理解交互流程即可
JMM三大特性
**1.原子性(Atomicity):**一个或多个操作不可分割,要么全部执行成功,要么都不执行
·JMM保证:read、load、assign、use、store、write这些基本操作是原子的。long/double的非原子性协定(64位读写可能分两次32位,但现代JVM通常实现为原子)
·更大范围原子性:需要synchronized或Lock
**2.可见性(Visibility):**一个线程修改了共享变量的值,其他线程能立即看到这个修改
·如何保证:volatile、synchronized,final(特殊规则)
3.有序性(Ordering) :程序执行的顺序。单线程内观察是有序的("as-if-serial"语义)。多线程并发时,由于指令重排序 和工作内存与主内存同步延迟,观察可能是无序的
·如何保证:volatile(禁止特定重排序)、synchronized(临界区内有序、如同单线程)
happens-before(先行发生)原则
JMM定义的天然有序性规则。如果操作A happens-before操作B,那么A的结果对B可见,且A的执行顺序排在B之前。重要规则:
·程序次序规则(单线程顺序)
·管程锁定规则(unlock先于后续lock)
·volatile变量规则(写 先于后续读)
·线程启动规则(start()先于线程内任何操作)
·线程终止规则(线程内所有操作先于终止检测)
·传递性(A先于B,B先于C =>A先于C)
volatile关键字
轻量级同步机制,保证可见性和禁止指令重排序
**可见性:**写volatile变量会立即刷新到主内存。读volatile变量会从主内存重新加载最新值。保证一个线程修改后,其他线程立即可见
·误区:volatile不保证原子性!count++这种复合操作在多线程下仍需加锁
**·禁止指令重排序:**通过内存屏障实现
·写volatile时:确保写之前的操作不会被重排序到写之后
·读volatile时:确保读之后的操作不会被重排序到读之前
适用场景:
·状态标志位(如 volatile boolean shutdownRequested)
·DCL单例模式(Double Check Lock)中的instance变量,防止new操作的重排序导致其他线程拿到未初始化完全的对象
·一次性安全发布(利用volatile写happens-before读)