十年之重修JVM原理(经长一寸寿延十年原理)
yuyutoo 2025-03-26 18:54 7 浏览 0 评论
弱小和无知并不是生存的障碍,傲慢才是。
---- ---- 面试者
总结
关于JVM相关的内容,只摘选一些常用需要理解以及面试常考的内容,其中相关内容完全来自《深入理解Java虚拟机》,这里只做内容提炼以及相关补充,包括Java运行时数据区、类的加载过程与双亲委派、对象的创建与访问、内存模型和垃圾回收等。
Java运行时数据区
当代码在JVM中运行时,JVM会把它所管理的内存分为若干区域进行管理,部分区域是线程共享的,部分区域是线程独占的,期间同样涉及线程内存与主存之间的数据同步,以及相关垃圾回收逻辑。
程序计数器
线程私有的一小块内存空间,用于存储当前线程所执行字节码指令的地址,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器。在多线程场景下,为了保证线程切换之后能恢复到正确的执行位置,每一个线程都需要有一个独立的程序计数器,独立存储互不影响。
虚拟机栈
线程私有内存空间,虚拟机在执行方法时,会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息,每一个方法被调用直至完毕的过程,就对应着一个栈帧在虚拟机栈入栈出站的过程。其中局部变量表和操作数栈在编译阶段,就已经确定并存储在方法的相关属性中。当执行的方法链路太长,申请的栈深度超过虚拟机所允许的深度时,就会抛出StackOverflowError 异常。
本地方法栈
本地方法栈为线程私有内存空间,其作用与虚拟机栈相似,其中的区别只是虚拟机栈为执行Java方法服务,而本地方法栈为本地(Native)方法服务。
堆
虚拟机管理的最大的一块内存区域,用于存储对象实例,由各线程共享。堆是垃圾收集器管理的内存区域,当JVM无法完成对实例内存分配时,将会抛出OutOfMemoryError 异 常。
方法区
用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,各线程共享的区域。在实现上,JDK8以前是基于永久代的逻辑设计实现,希望通过垃圾收集器对方法区进行回收,由于永久代存在内存设置上线,更容易出现内存溢出的问题,故在JDK8之后,采用元空间的逻辑来实现方法区,存储在方法区的类信息直接存入主存,不再受堆内存的限制以及垃圾收集器的管理,但是也是受物理机内存大小的限制的。
注意:常量池、静态变量、Class对象在JDK1.7时,就已经移入堆存储了,剩余的类信息以及字节码在JDK1.8以后存入了元空间(直接内存),方法区只是一个JVM规范,具体实现由JVM自定义。
类加载
类加载过程
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期会经历加载、验证、准备、解析、初始化、使用和卸载;
有且只有以下六种场景必须立刻对类进行加载与初始化:
- 遇到new(使用new关键字实例化对象)、getstatic(读取静态字段)、putstatic(设置一个静态字段)、invokestatic(调用一个静态方法)这四条字节码指令时,其中读取或设置被 final 修饰、已在编译期把结果放入常量池的 静态字段除外;
- 使用 java.lang.reflect 包的方法对类型进行反射调用的时候,如果类型没有进行 过初始化,则需要先触发其初始化;
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父 类的初始化;
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的那个 类),虚拟机会先初始化这个主类;
- 当使用 JDK 7 新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
- 当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流;(这里没有限定必须从class文件获取,故后来衍生从JAR、WAR、从网络中获取、运行时生成字节码(动态代理)等)
- 将这个字节流转换为JVM定义的存储格式存入方法区;
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
验证
- 文件格式验证,比如二进制文件是否以魔数 0xCAFEBABE 开头(头4个字节),二进制文件中主次版本号,是否在当前JVM虚拟机接受(JDK向下兼容class文件,当时向上不支持,第5和第6字节是次版本号,第7第8字节是主版本号),以及相关class文件格式的验证。
- 元数据验证,这里主要是对语法进行分析与验证;
- 字节码验证,主要是通过数据流分析和控制流分析,确保语义执行是合法符合逻辑的。
- 符号引用验证,主要是校验代码中通过符号引用的类、方法是否存在或可访问等。
准备
准备阶段正式为类中定义的变量(即静态变量,而非实例变量)分配内存,并设置初始值(默认值),具体赋值得到初始化阶段;
解析
JVM将常量池内的符号引用替换成直接引用,其中符号引用支持一组符号用来唯一标记目标,引用目标并不是一定加载到虚拟机的内容,替换成直接引用后,则是可以指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
初始化
初始化阶段会将准备阶段针对类变量初始化后的值,再根据程序编码实际赋值逻辑进行赋值,同时会基于类构造器
类构造器
类加载器
类与类加载器
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在JVM中的唯一性。即如果一个类,在同一个JVM中,使用两个不一样的类加载器加载,则这两个类不相等。这里的相等包括:equals()方法、isAssignableFrom()方 法、isInstance()方法的返回结果等。
双亲委派模型
- 启动类加载器(Bootstrap Class Loader):这个类加载器负责加载存放在JAVA_HOME/lib目录的类,或者被-Xbootclasspath 参数所指定的路径中存放的, 而且是 Java 虚拟机能够识别的(按照文件名识别,如 rt.jar、tools.jar,名字不符合的类 库即使放在 lib 目录中也不会被加载);
- 扩展类加载器(Extension Class Loader):这个类加载器是在类 sun.misc.Launcher$ExtClassLoader中以Java 代码的形式实现的。它负责加载 \lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中所有的类库。
- 应用程序类加载器(Application Class Loader):这个类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它 负责加载用户类路径(ClassPath)上所有的类库。
双亲委派模型,其中加载器之间并不是继承关系,只是组合引用,如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,每一个层次的类加载都是如此,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中,没有找到所需的类)时,子加载器才会尝试自己去完成加载。
这样的好处就是保证基础类型例如Object这样的基础类,在JVM运行时只被加载一次,而不至于出现多个相同同名类导致系统混乱。
破坏双亲委派模型
双亲委派模型的出现,是为了解决基础类只被加载一次以及类加载流程规范问题,但是破坏双亲委派模型,也是为了解决实际问题,并不存在破坏就是不好的意思。
- 用户基于继承ClassLoader覆写loadClass()方法进行自定义加载类过程,导致并不遵守双亲委派原则;
- 在SPI技术上,由于启动类无法加载提供SPI接口的类,故需要通过Thread类的ClassLoader进行加载;
- OSGi技术,支持热部署
对象的创建&内存布局&访问
new对象流程
- 当JVM遇到new指令时,首先会检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、 解析和初始化过,如果没有,那必须先执行相应的类加载过程;
- 类加载检查通过之后,虚拟机将会为新生的对象分配内存。
- 对象头信息进行初始化。(Mark Word 、类指针);
- 执行类的构造函数
()方法,进行静态属性初始化、静态代码块执行以及构造函数执行
对象内存布局
在堆内存中,对象的存储布局可以分为三个部分,对象头、实例数据和对其填充;对象头包括Mark Word 和 类指针(指向该实例所属类);实例数据存储的字段内容,其中包括从父类继承的属性;对齐填充这个因为JVM要求对象起始地址必须是8字节的整数倍,不足则进行补充。
这个Mark Word涉及synchronized实现原理:synchronized实现原理
对象访问
对象的访问有两种方式,一是句柄访问,二是直接指针访问(JVM使用)。句柄访问就需要在堆中开辟一个内存空间存储句柄,定义的引用变量指向句柄,句柄存储两个指针,一个指向堆类的对象实例数据,一个指向方法区的对象数据类型,这样当对象进行GC被迁移之后,只需要调整句柄中的指针,引用变量不需要调整,但是这样就需要多走一次指针。直接指针访问,就是引用变量直接指向堆中对象实例数据,对象实例中也保存了对象类型的指针,这样访问速度更快。
Java内存模型
Java内存模型规定了所有实例字段、静态字段和构成数组对象的元素,都存储在主内存中,每一条线程有自己的工作内存,其中工作内存中保存了该线程使用的变量的主内存副本,线程对变量的所有操作(读写)都必须在工作内存中进行,而不能直接读写主内存,不同线程之间也无法直接访问对方的工作内存中的变量,线程之间的数据传递都需要通过主内存来完成。
主内存与工作内存数据交互:
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
- load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
- write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
涉及工作内存与主存间数据同步经典问题:Volatile原理
垃圾回收
可达性分析
Java的垃圾回收是通过可达性分析,通过定义一系列的“GC Roots”的根对象,从这些节点开始基于引用关系往下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链,则表示此对象不可达,需要被回收,一般经历至少两次不可达标记之后,对象才会被回收。
GC Roots定义:
- 虚拟机栈中引用的对象,即线程栈帧中还存在对象的引用;
- 方法区中静态属性引用的对象,常量引用的对象,本地方法中native方法引用的对象;
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些异常对象如NullPointException,还有系统的类加载器等;
- 所有被同步锁synchronized持有的对象;
- 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
垃圾回收算法
- 标记-整理算法:是标记-清除算法的升级,即对要回收的内存进行标记,然后进行清除,清除之后进行整理,保证被使用的内存是连续的,以及空余的内存也是连续的。这里如果不整理,则需要维护一个内存空间链表来记录那个内存可用哪个不可用,这样会导致整体吞吐量下降,如果选择整理,会导致在回收期间需要stop the world,但是能接受。
- 标记-复制算法:内存会被分为两部分,一部分使用,一部分预留,当使用的那部分发生回收之后,直接把标记存活的对象复制到预留内存中,被使用的那部分直接全部回收掉,这样两个内存区域反复使用;
- 分代回收算法:把堆内存分为新生代和老年代,新生代通过多次回收还存活的对象会存入老年代,其中新生代由按照8:1:1分为一个Eden区和两个Survivor区,每次使用一个Survivor区和Eden区,进行回收时,把存活的对象存入另外一个Survivor区。当Survivor存储不下时,就对存入老年代区。
垃圾回收器
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代),jdk23 使用的G1收集器。Parallel Scavenge基于标记-复制算法实现,支持多线程收集,Parallel Old支持多线程并发收集,基于标记-整理算法实现。
G1 开创的基于 Region 的堆内存布局是它能够实现这个目标的关键。虽然 G1 也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。收集器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
坐观垂钓者,徒有羡鱼情!
相关推荐
- Python操作Word文档神器:python-docx库从入门到精通
-
Python操作Word文档神器:python-docx库从入门到精通动动小手,点击关注...
- Python 函数调用从入门到精通:超详细定义解析与实战指南 附案例
-
一、函数基础:定义与调用的核心逻辑定义:函数是将重复或相关的代码块封装成可复用的单元,通过函数名和参数实现特定功能。它是Python模块化编程的基础,能提高代码复用性和可读性。定义语法:...
- 等这么长时间Python背记手册终于来了,入门到精通(视频400集)
-
本文毫无套路!真诚分享!前言:无论是学习任何一门语言,基础知识一定要扎实,基础功非常的重要,找一个有丰富编程经验的老师或者师兄带着你会少走很多弯路,你的进步速度也会快很多,无论我们学习的目的是什么,...
- 图解Python编程:从入门到精通系列教程(附全套速查表)
-
引言本系列教程展开讲解Python编程语言,Python是一门开源免费、通用型的脚本编程语言,它上手简单,功能强大,它也是互联网最热门的编程语言之一。Python生态丰富,库(模块)极其丰富,这使...
- Python入门教程(非常详细)从零基础入门到精通,看完这一篇就够
-
本书是Python经典实例解析,采用基于实例的方法编写,每个实例都会解决具体的问题和难题。主要内容有:数字、字符串和元组,语句与语法,函数定义,列表、集、字典,用户输入和输出等内置数据结构,类和对象,...
- Python函数全解析:从入门到精通,一文搞定!
-
1.为什么要用函数?函数的作用:封装代码,提高复用性,减少重复,提高可读性。...
- Python中的单例模式:从入门到精通
-
Python中的单例模式:从入门到精通引言单例模式是一种常用的软件设计模式,它保证了一个类只有一个实例,并提供一个全局访问点。这种模式通常用于那些需要频繁创建和销毁的对象,比如日志对象、线程池、缓存等...
- 【Python王者归来】手把手教你,Python从入门到精通!
-
用800个程序实例、5万行代码手把手教你,Python从入门到精通!...
- Python从零基础入门到精通:一个月就够了
-
如果想从零基础到入门,能够全职学习(自学),那么一个月足够了。...
- Python 从入门到精通:一个月就够了
-
要知道,一个月是一段很长的时间。如果每天坚持用6-7小时来做一件事,你会有意想不到的收获。作为初学者,第一个月的月目标应该是这样的:熟悉基本概念(变量,条件,列表,循环,函数)练习超过30个编...
- Python零基础到精通,这8个入门技巧让你少走弯路,7天速通编程!
-
Python学习就像玩积木,从最基础的块开始,一步步搭建出复杂的作品。我记得刚开始学Python时也是一头雾水,走了不少弯路。现在回头看,其实掌握几个核心概念,就能快速入门这门编程语言。来聊聊怎么用最...
- 神仙级python入门教程(非常详细),从0到精通,从看这篇开始!
-
python入门虽然简单,很多新手依然卡在基础安装阶段,大部分教程对一些基础内容都是一带而过,好多新手朋友,对一些基础知识常常一知半解,需要在网上查询很久。...
- Python类从入门到精通,一篇就够!
-
一、Python类是什么?大家在生活中应该都见过汽车吧,每一辆真实存在、能在路上跑的汽车,都可以看作是一个“对象”。那这些汽车是怎么生产出来的呢?其实,在生产之前,汽车公司都会先设计一个详细的蓝图...
- 学习Python从入门到精通:30天足够了,这才是python基础的天花板
-
当年2w买的全套python教程用不着了,现在送给有缘人,不要钱,一个月教你从入门到精通1、本套视频共487集,本套视频共分4季...
- 30天Python 入门到精通(3天学会python)
-
以下是一个为期30天的Python入门到精通学习课程,专为零基础新手设计。课程从基础语法开始,逐步深入到面向对象编程、数据处理,最后实现运行简单的大语言模型(如基于HuggingFace...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)