恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

深入理解JVM虚拟机

  • 首页
  • 资讯中心
  • /
  • 深入理解JVM虚拟机

相关资讯

中文文本分类的分层建模实战:CNN+RNN+GCN+BERT协同设计 2026/9/8 20:52:41
5分钟用amis搭起后台管理系统:JSON配置生成页面 2026/9/8 20:52:41
在 Visual Studio 里接入 Ace Data Cloud:几分钟配置 AI 编程助手 2026/9/8 20:52:41

最新资讯

7款爆火的AI写小说工具测评!新手写小说软件就看它!
AI Skill解析:从提示词到可复用技能包的工作原理
用WorkBuddy搭建半自动化周报流水线,从3天压缩到4小时
MCP到MHS:大模型控制物理设备的安全语义契约
零基础深度学习实战指南:从数学基础到训练第一个神经网络
FastAPI 直接返回 Response:JSONResponse、jsonable_encoder 与自定义响应实践指南

今日推荐

Redis缓存与离线预计算在大数据处理中的实战应用
Android 12热启动闪屏排查:从冷热启动差异到官方SplashScreen避坑指南
加密资产价值投资:原理、方法与实战策略

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

深入理解JVM虚拟机

发布时间:2026/9/8 20:57:42
深入理解JVM虚拟机 深入理解JVM虚拟机JVMJava虚拟机是运行Java字节码的虚拟计算机环境它提供了一个平台无关的运行环境使得Java程序能够在任何平台上运行而无需重新编写。JVM的核心功能包括加载代码、验证代码、执行代码、以及提供运行时环境。本文将详细介绍JVM相关结构主要包括JVM内存模型、类加载器、双亲委派机制、JMM、垃圾回收算法等JVM内存模型Java代码运行时new出来的对象存在哪呢方法执行的时候方法存在哪方法里面的参数、局部变量存在哪一个类的元数据信息也就是接口、父类、类的属性、函数等存在哪代码种各种分支循环函数调用跳来跳去JVM如何知道代码执行到哪一行要理解这些东西你就得了解JVM内存结构。JVM内存结构主要划分为以下几个部分堆、方法区、虚拟机栈、本地方法栈、程序计数器等虚拟机栈首先来说一下虚拟机栈虚拟机栈本质上就是一个栈代码执行需要调用方法调用方法就需要将方法的信息封装成一个栈帧然后将栈帧放入虚拟机栈方法执行完后就出栈这样对应的方法执行顺序就是后进先出。所以我们就可以知道虚拟机栈的本质就是用来管理方法执行的那么问题来了方法里面有很多东西啊比如局部变量、入参、出参、参数传递这些东西放在哪里呢这里就可以详细说一下栈帧的结构了栈帧里面有局部变量表保存局部变量、操作数栈算数运算和方法调用的一个参数传递栈帧的其他东西不需要鸟姐太多浅尝辄止即可。那么问题又来了虚拟机栈需要进行GC吗显然不需要因为虚拟机栈的结构是一个栈方法执行完后就出栈了纯自动的不需要GC来进行垃圾回收。本地方法栈要理解本地方法栈我们就要知道什么是本地方法所谓本地方法就是底层C或C语言写的方法Java代码执行的时候要调用底层C或C的方法那这些方法执行的时候就需要放到本地方法栈。程序计数器因为代码中有分支循环、方法调用执行的时候从这个类跳到那个类从这一行跳到哪一行;跳来跳去所以就需要用程序计数器来进行保存即将执行的字节码指令的位置保存位置之后我们就知道下一步要执行到什么代码了。以上介绍的虚拟机栈、本地方法栈、程序计数器这些都是线程私有的那么问题来了这些东西为什么要线程私有不同线程是可以并发执行的A线程调用A方法B线程调用B方法肯定需要不同的虚拟机栈来保存正在执行的方法本地方法栈也是同理的那么程序计数器也可以参照以上来理解不同线程执行的位置肯定是不一样的方法执行到哪一行也是不一样的所以需要不同的程序计数器去记录这个线程他执行到哪里了。堆堆是用来存放对象的几乎所有new出来的对象都存放到堆上。对象放在堆里那么基本类型放在栈帧的操作数栈或局部变量表里面。问题来了为什么要把对象放在堆里基本类型放在栈里首先基本类型是比较小的放在栈里方便创建和销毁放在堆里会增加GC的一个压力而且基本类型不太有可能会被线程共享共享的话复制一份开销也不会太大对象它比较大需要去跨方法、跨线程甚至是跨GC周期去存活对象放到堆里只需要给一个引用地址就可以进行访问假如对象放到栈里就会失去共享能力如果要共享就需要深拷贝一份所以说对象放堆里基本类型放栈里问题又来了所有的对象都会放到堆里吗JVM会对对象进行一个栈上分配简单来说就是做逃逸分析分析这个对象是不是只在方法内部使用没有作为返回值或参数传递给其他对象使用如果这个对象完全没有给到外边也就是完全没有逃逸JVM就会将这个对象给拆散根据对象内部属性拆成一个个基本类型然后在栈上进行分配。方法区方法区里面放的不是方法而是类的元数据信息也就是类的全类名父类字段信息、类的方法信息、类的静态变量等等。方法区是JVM的一个规范不同JVM对方法区的实现是不同的JDK1.8之前方法区这个实现叫做永久代永久代是JVM内存里面的是运行时数据区的一部分JDK1.8之后方法区就变成了元空间放在本地内存里面也就是操作系统内存为什么要把永久代换成元空间放到本地内存里面因为永久代占用JVM内存JVM内存比较小加载得过多就容易OOM元空间依赖操作系统内存加载多少类的元数据信息就由实际可用的空间来配置能加载的类就更多了不容易OOM堆是存对象的不同的线程需要去使用所以堆肯定是线程共享的。方法区放的是类的元数据信息类的元数据信息就是用来new对象的一个元数据信息多个地方要用来new对象那么肯定是线程共享的。类加载器类加载器是JVM用来把.class字节码文件加载到内存、转换成Clss对象的组件。它最核心的两个职责一是动态加载程序运行时按需加载类不用编译期就把所有类都塞进来二是隔离命名空间不同类加载器加载的同名类互不干扰Tomcat能同时跑多个版本相同依赖的Web应用就靠这个。JDK8的三种类加载器JDK8有三层类加载器启动类加载器(Bootstrap ClassLoader)它是属于虚拟机自身的一部分主要负责加载lib目录中或被-Xbootclasspath指定的路径中的并且文件名是被虚拟机识别的文件它是所有类加载器的父亲。扩展类加载器(Extension ClassLoader)),它是Java实现的独立于虚拟机主要负责加载lib\ext目录中或被java.ext.dirs系统变量所指定的路径的类库。应用程序类加载器(Application ClassLoader),它是Java实现的独立于虚拟机。主要负责加载用户类路径(classPath)上的类库如果我们没有实现自定义的类加载器那这个加载器就是我们程序中的默认加载器。JDK9模块化后的变化JDK9引入了模块化系统Jigsaw,原来的rt,jar、tool,jar被拆成了几十个jmod文件。既然已经满足可扩展需求就没必要保留JAVA_HOME/1ib/ext这个目录了所以扩展类加载器被重命名为平台类加载器(PlatformClassLoader),主要加载被module-info,java中定义的类。Java的类加载过程类加载就是把.class文件的二进制数据读进内存经过校验、转换最终变成VM能用的Class对象。二进制流不一定非得来自.class文件也可以是字节码工具动态生成的、或者从网络传过来的只要格式对JVM都认。整个类加载流程分为三大阶段加载、连接、初始化。连接又能拆成验证、准备、解析三步所以细分下来是5个阶段加载把二进制流读进内存在方法区生成类的运行时数据结构同时在堆里创建一个Clss对象作为访问入口。验证校验二进制流是否符合Clss文件规范包括魔数检查、版本号校验、元数据验证、字节码验证、符号引用验证。这一步是为了防止恶意代码搞崩JVM。准备给类变量(static修饰的变量)分配内存并设置初始零值。注意这里只是零值比如static int a-123在准备阶段a的值是0不是123。但如果是static final inta 123,编译期就确定了准备阶段直接赋值123。解析把常量池里的符号引l用替换成直接引l用。符号引用就是一个字符串形式的标识比如java/lang/Object;直接引用是真正的内存地址或偏移量能直接定位到目标。初始化:执行类构造器()方法这时候才真正执行static int a123这种赋值操作静态代码块也是在这个阶段跑的。双亲委派机制前面说了Java类加载器那么现在就能详细聊一聊双亲委派机制。双亲委派其实可以理解为类逐级加载的一个规则就是先看看上层的类加载器能不能加载简单来说儿子在做类加载的时候先去看看它爹能不能加载它爹再去看它爷爷能不能加载它爷爷能加载就让爷爷加载它爷爷不能加载就让它父亲进行加载它父亲如果不能加载就自己加载这个过程就叫做委派也就是逐级加载。这样做有什么好处呢避免重复加载逐级加载能避免两个类加载器加载同一个类。避免Java的核心API被篡改在逐级加载的模式下核心API肯定能被顶层的类加载器所加载。打破双亲委派模型那么设想一个场景如果我有一个目录需要这个目录下的类有先被加载不想去走逐级加载应该怎么做呢这就是所谓的打破双亲委派模型打破双亲委派的核心就是重写loadClass()方法在里面区优先加载某个目录的类OverrideprotectedClass?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){// 首先检查该类是否已经被加载过Class?cfindLoadedClass(name);if(cnull){// 对指定包优先自己加载其他仍走双亲委派if(name.startsWith(com.example.plugin)){try{// 自定义加载逻辑查找并定义类cfindClass(name);}catch(ClassNotFoundExceptionignore){// 兜底给父加载器捕获异常不做处理}}// 如果自定义加载未成功委托父类加载器双亲委派if(cnull){csuper.loadClass(name,false);// 委托父加载器}}// 如果需要解析对类进行解析处理if(resolve){resolveClass(c);}returnc;}}为什么需要打破双亲委派模型呢肯定不是为了打破而打破实际项目中可能会出现父类的类加载器需要调用子类的加载器才能加载实现类这个需求。举个例子在JDBC场景中DriverManager由Bootstrap ClassLoader加载上层驱动实现在classpath,由Application ClassLoader可见下层若严格按双亲委派上层代码无法“向下看到下层类JDBC的解决方式在DriverManager初始化时通过ServiceLoader.load(Driver…class)使用当前线程的上下文类加载器(Thread Context ClassLoader)去加载实现类Java内存模型JMMJava内存模型是JVM定义的一套规范规定了多线程程序中变量如何在内存中存储和传递约定了线程何时从主内存读取数据、何时把数据写回主内存。JMM的核心目标是确保多线程环境下的可见性、有序性和原子性屏蔽掉硬件和编译器优化带来的不一致问题可见性一个线程对变量的修改能及时被其他线程看到。volatile关键字就是用来保证可见性的强制线程每次读写都直接跟主内存交互有序性线程执行操作的顺序。JMM允许指令重排序来提高性能但通过happens-before关系保证跨线程的有序性原子性操作不可分割执行过程中不会被打断。synchronized关键字能保证代码块的原子性JMM的抽象内存模型主内存存放共享变量所有线程都能访问每个线程有自己的本地内存存放共享变量的副本线程对变量的操作必须在本地内存中进行不能直接操作主内存线程间变量传递必须通过主内存完成主内存和工作内存主内存主内存是java堆内存的一部分所有的实例变量、静态变量和数组元素都存储在主内存中。工作内存每个线程都有自己的工作内存。工作内存存储了主内存中变量的副本线程对变量的所有操作都在工作内存中进行而不是直接在主内存中。线程之间不能直接访问对方的工作内存中的变量线程间变量的传递必须通过主内存来完成。内存间的交互操作(8种操作必须原子性)Java内存模型定义了八种操作用于控制主内存和工作内存之间的交互这些操作都是原子的lock(锁定)把一个变量标识为一条线程独占的状态。unlock(解锁)把一个变量从独占状态中释放出来释放后的变量才能被其他线程锁read(读取)从主内存中读取一个变量到工作内存中。load(载入)把read操作从主内存中得到的变量值放入工作内存的变量副本中。use(使用)把工作内存中的一个变量值传递给执行引擎。assign(赋值)把一个从执行引擎接收到的值赋给工作内存中的变量。store(存储)把工作内存中的一个变量的值传送到主内存中。write(写入)把store操作从工作内存中得到的变量值放入主内存的变量中。对于volatile型变量的特殊规则可见性对一个volatile变量的写操作会立即刷新到主内存中任何线程对这个volati1e变量的读操作都能立即看到最新的值。禁止指令重排序在对volatile变量进行读/写操作时会插入内存屏障禁止指令重排序。具体来说对volatile变量的写操作不能与之前的读/写操作重排序。对volatile变量的读操作不能与之后的读/写操作重排序。针对long和double型变量的特殊规则(long和double的非原子协定)非原子性在一些平台上对64位的log和double类型的变量的读/写操作是非原子的。这意味着读取一个64位变量时可能只读取了其中的32位数据从而导致读取到的值是不完整的。解决方案可以通过使用volatile关键字或者使用锁来保证对long和double类型变量的操作是原子的。原子性、可见性、有序性原子性指一个操作是不可分割的即使在多线程环境下一个操作一旦开始就不会被其他线程中断。可见性指一个线程对共享变量的修改能够及时地被其他线程看到。volatile关键字、锁和内存屏障可以保证可见性。有序性指程序按照代码顺序执行。ava内存模型允许编译器和处理器对指令进行重排序但通过volatile和锁可以保证必要的有序性。Happens-Before原则Happens-Before原则是JMM中定义的操作间的顺序规侧确保操作的有序性和可见性。具体包括以下八个规侧程序次序规则一个线程中的每个操作按照程序代码的顺序发生。监视器锁规侧一个解锁操作发生在同一个锁的随后的加锁操作之前。volatile变量规侧对一个volatile变量的写操作发生在对该变量的随后的读操作之前。线程启动规则在一个线程中对另一个线程的Thread.start()调用发生在这个新线程的每一个操作之前。线程终止规侧一个线程中的所有操作都发生在另一个线程检测到这个线程已经终止通过Thread.join()返回)之前。线程中断规则对线程的中断操作(Thread.interrupt()发生在被中断线程检测到中断事件通过Thread,interrupted()或Thread.isInterrupted())之前。对象终结规则一个对象的构造函数执行结束发生在这个对象的finalize()方法之前。传递性如果操作A Happens-Before操作B,操作B Happens-Before操作C,那么操作A Happens-Before操作C。垃圾回收你要回收一个垃圾就得知道怎么看一个对象是不是垃圾主要有两种算法引用计数法如果对象被引用了计数器1计数器为0代表是垃圾但是这种方法没有办法处理循环引用。可达性分析对象之间的引用关系可以构成一个有向图可达性分析就是从根对象触发去遍历这个有向图遍历不到就是不可达对象也就是垃圾。可达性分析的具体过程三色标记法三色标记是一种增量标记算法让GC可以和应用线程并发执行不用一口气停下来把所有对象扫完。CMS和G1都用了这套算法。核心思路是给对象打三种颜色的标签白色还没被GC访问过可能是垃圾灰色已经被访问但它引用的对象还没处理完黑色自己和它引用的对象都改处理完了肯定不是垃圾标记过程从GC Roots开始先把根对象染成灰色。然后不断从灰色集合里拿对象出来把它引用的白色对象染成灰色自己染成黑色。重复这个过程直到没有灰色对象为止。最后剩下的白色对象就是垃圾。但是这会出现两个问题漏标和多标####漏标假设GC刚扫完A,A变成黑色B还是灰色等着被扫。这时候mutator把A到C的引用加上了又把B到C的引用删了。等GC去扫B的时候B没有引用了GC认为C是白色的垃圾对象。但实际上C还被A引用着不该被回收。这就是漏标把活着的对象当垃圾清了这是致命错误GC绝对不能容忍。宁可放过不能杀错。解决方案漏标发生需要同时满足两个条件mutator给黑色对象加了一条到白色对象的引用mutator把灰色对象到用那个白色对象的引用删了打破任意一个条件就不会漏标。两种经典方案增量更新用写屏障拦截第一个条件。黑色对象要引用白色对象时把白色对象变成灰色或者把黑色对象退回灰色重新扫一遍。CMS用的就是这个方案。STB:用写屏障拦截第二个条件。灰色对象删除对白色对象的引用时把这条旧引用记下来相当于保存了标记开始时刻的引用快照。G1用的是这个方案SATB全称是Snapshot At The Beginning.两种方案各有优劣。增量更新更精确但需要在标记结束后再扫一遍被修改的引用。SATB可能会多标一些对象但不需要重新扫描整体停顿时间更短。多标还有多标的情况A变黑之后mutator把根到A的引用删了A其实已经是垃圾了但已经被标成黑色不会被回收只能等下次GC。这个问题不严重最多浪费一点内存下次GC就清理掉了。垃圾回收算法垃圾回收算法本质上就是处理内存碎片的几种不同策略。主要有三种标记-清除算法、复制算法、标记-整理算法1. 标记-清除算法先遍历一遍把有用的对象打个标记然后把没标记的垃圾直接清掉。问题是清完之后空出来的地方东一块西一块的像蜂窝煤一样。下次想分配个大对象明明总空间够但就是找不到一块连续的地儿放。2. 复制算法把内存一分为二平时只用一半。回收的时候把活着的对象全部复制到另一半整整齐齐排好然后把原来那一半直接清空。好处是快绝对没有碎片。坏处是得空着一半地盘不能用太浪费。3. 标记整理算法老年代常用。老年代对象活得久用复制算法得复制一大堆太慢用标记-清除又有碎片。标记-整理的做法是先标记然后把所有活着的对象往一端推像整理书架一样排紧凑最后把剩下的空间清空。既没碎片又不用浪费一半空间代价是移动对象比较耗时。新生代与老年代依据以上垃圾回收算法的特点JVM将堆区划分为两块区域一个是新生代一个是老年代。新生代新生代区域基本上是一些新出生的对象大多数对象就是朝生夕死的例如从数据库查一批数据放到对象里面返回给前端对象立马就不用了就应该被回收。老年代项目里面各个Service类、Controller类的、Spring IOC容器等这些对象活周期长项目启动就存在项目停止才进行回收这样的对象就放到老年代里面。新生代因为朝生夕死垃圾对象多存活对象少就适合使用复制算法老年代因为对象存活周期长就适合使用标记-清除或标记整理算法。针对不同的内存区域使用不同的回收算法这个就叫分代收集算法垃圾回收过程新生代分为三个区域分别是EDEN、Survivor0、Survivor1这两个Survivor大多被称为from survivor 和 to survivor。一般来说对象会在EDEN区出生当EDEN区空间不足的时候就会触发一次young gc这个时候就会使用复制算法把EDEN区存活对象和from区对象复制到to survivor区然后呢存活对象寿命1EDEN清除垃圾from和to进行交换from 存放存活对象。如果to区空间满了就会把to区对象移动到老年代。存活对象寿命达到阈值15的时候这种情况下也会把对象从新生代移动到老年代如果一次晋升的对象多了老年代装不下了怎么办这个时候会尝试触发young gc去清理新生代对象减少晋升的数量。如果还是不够那么就会触发full gc把新生代和老年代都清理一遍还会清理一下元空间老年代垃圾回收过程实际上是使用标记清除或标记整理的一个过程具体使用什么算法那得看使用了什么样的垃圾回收器。垃圾回收器HotSpot虚拟机的垃圾收集器按作用区域分成两类新生代收集器和老年代收集器它们需要搭配使用。新生代收集器Seial:单线程用标记-复制算法。GC时所有应用线程停下来等着简单粗暴。在客户端模式下是默认收集器几十MB的新生代几毫秒就能收完。ParNew:Serial的多线程版本除了能并行收集别的都一样。它存在的意义是能跟CMS配合JDK9之后跟CMS绑定了单独用不了了。Parallel Scavenge:也叫吞吐量收集器多线程并行收集。它的目标不是缩短单次停顿而是最大化CPU用在业务代码上的时间占比。适合后台跑批、大数据计算这种不在乎偶尔卡一下的场景。老年代收集器Serial Old:Serial的老年代版本单线程用标记-整理算法。Parallel Old:Parallel Scavenge的老年代搭档多线程并行标记-整理。要发挥吞吐量优先的效果新生代老年代得配套用。CMS全称Concurrent Mark Sweep,追求低停顿。大部分工作跟应用线程并发执行只有初始标记和重新标记需要短暂停顿。缺点是用标记-清除算法会产生碎片还有并发失败的风险。JDK9标记为废弃JDK14正式移除。G1:JDK9之后的默认收集器把堆切成2048个左右的Region,不再严格区分新生代老年代。能设定目标停顿时间让GC变得可预测。ZGC:DK11引入的低延迟收集器停顿时间控制在10ms以内跟堆大小无关。支持TB级别的堆内存。JDK15转正。ial Old:Serial的老年代版本单线程用标记-整理算法。2. Parallel Old:Parallel Scavenge的老年代搭档多线程并行标记-整理。要发挥吞吐量优先的效果新生代老年代得配套用。3. CMS全称Concurrent Mark Sweep,追求低停顿。大部分工作跟应用线程并发执行只有初始标记和重新标记需要短暂停顿。缺点是用标记-清除算法会产生碎片还有并发失败的风险。JDK9标记为废弃JDK14正式移除。4. G1:JDK9之后的默认收集器把堆切成2048个左右的Region,不再严格区分新生代老年代。能设定目标停顿时间让GC变得可预测。5. ZGC:DK11引入的低延迟收集器停顿时间控制在10ms以内跟堆大小无关。支持TB级别的堆内存。JDK15转正。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号