导图社区 JVM
当我们在开发 Java 应用程序时,JVM 扮演了非常重要的角色。JVM(Java Virtual Machine)是 Java 应用程序执行的运行环境,它可以在不同的硬件和操作系统上运行 Java 应用程序。学习 JVM 对于 Java 开发人员来说是非常重要的,因为它是 Java 应用程序执行的核心。为了帮助大家更好地理解 JVM,我做了一份详细的思维导图,它涵盖了 JVM 的基本概念、内存管理等重要的内容。这份思维导图可以帮助你更深入地理解 JVM,从而帮助你写出更好的 Java 应用程序。
编辑于2023-02-14 16:07:56 四川省这份思维导图主要按照《python从入门到实践》的大纲来做出来的,并在相关内容的解释处加入了相关代码,欢迎大家一起学习!
职能地图-Java,干货分享~Java语言技术,java技术扩展,数据结构,维优,个人职能,技术面试知识点总结。
当今大型软件系统的开发多采用企业级的开发模式,而Java语言也是目前较为流行的企业级开发语言之一。针对Java企业级开发,涉及的知识点和技术栈较为丰富,包括但不限于Java EE、Spring框架、Hibernate框架、Maven、Git、Jenkins等等。这份思维导图以Java企业级开发为主题,通过图解的形式将涉及的知识点进行了梳理和整理,从Java EE体系结构、Servlet、JSP、Spring框架、Hibernate框架、Maven等基础知识开始讲解,逐步深入到SpringMVC、
社区模板帮助中心,点此进入>>
这份思维导图主要按照《python从入门到实践》的大纲来做出来的,并在相关内容的解释处加入了相关代码,欢迎大家一起学习!
职能地图-Java,干货分享~Java语言技术,java技术扩展,数据结构,维优,个人职能,技术面试知识点总结。
当今大型软件系统的开发多采用企业级的开发模式,而Java语言也是目前较为流行的企业级开发语言之一。针对Java企业级开发,涉及的知识点和技术栈较为丰富,包括但不限于Java EE、Spring框架、Hibernate框架、Maven、Git、Jenkins等等。这份思维导图以Java企业级开发为主题,通过图解的形式将涉及的知识点进行了梳理和整理,从Java EE体系结构、Servlet、JSP、Spring框架、Hibernate框架、Maven等基础知识开始讲解,逐步深入到SpringMVC、
JVM
基本概念
JVM 是一套规范,与 Java 语言没有直接的关系,任何可以形成 .class 文件的语言都可以执行在 JVM 之上,例如:kotliin、scala、grovey
JVM 是虚拟出来的一个计算机
有自己的“操作系统”,有自己的内存管理方式
有自己的指令集、自己的汇编语言
内存管理
JVM 的虚拟内存布局
JVM 也是 OS 中的一个进程,所以 VSS 布局和不同进程没事区别
常说的 Native 内存(也叫直接内存)指的就是 JVM 进程的堆,JVM 进程堆至少被 Class Loader、执行引擎、垃圾回收器使用着
Java 运行时内存区属于 JVM 进程堆。JVM 模仿操作系统,为程序提供了和 OS 进程类似内存布局,目的是 JVM 要接管 Heap、Method Area 等区域内存管理和回收工作。
运行时内存模型
程序计数器
程序计数器是一块很小的内存空间,用于保存当前正在解释执行的字节码地址
程序计数器是线程私有的。为了保存多线程切换后当前正在执行的字节码信息。所以程序计数器必须是线程私有的。
是 JVM 规范中唯一没有规定 OOM 的内存区域
CPU 的 PC 指令寄存器
二者的名字都是: Program Counter Register 但是二者的区别还是明显的
实现原理不同:CPU 的 PC 寄存器是物理单元位于 CPU 的内部;而 Java 运行时的程序计数器位于内存是逻辑单元。
存储内容不同:CPU 的 PC 寄存器保存的是下一条将要执行地指令的内存地址;Java 运行时程序计数器保存的是这在解释执行的字节码地址(这个地址指向方法区)。
服务对象不同:CPU 的 PC 寄存器是为 CPU 服务的;Java 运行时的程序计数器是为执行引擎服务的。
虚拟机栈
每一个线程都有属于自己的栈,栈的最小单位是栈帧
栈帧的结构
局部变量表
编译期可知的各种 Java 虚拟机基本数据类型boolean、byte、char、short、int、float、long、double)、对象引用
操作数
动态链接
方法出口信息
每个方法执行的时候都会形成一个栈帧
方法执行:栈帧入栈
执行完毕:栈帧出栈
StackOverflowError
线程请求的栈深度大于虚拟机允许的深度
常常出现在递归调用上
OutOfMemoryError
还没到达最大允许的深度,但是没有更多的内存扩容了
本地方法栈
为 Native 程序服务,也是线程私有的
和虚拟机栈差不多,只是服务对象不一样
堆(Heap)
堆是存储对象的地方,是线程共享的区域
堆在逻辑上(VSS上)是连续的,在物理上不一定是连续的
Heap 的回收依赖自动垃圾回收机制 —— GC
几乎所有的 GC 算法都采用了分代算法
JVM 规范并没有对 Heap 内部进一步的划分
年轻代、老年代、永生代其实都是 GC 算法划分的
堆是线程共享的,所以在分配内存的时候会出现抢占问题
java 虚拟机使用 CAS(原子性)指令 + 失败重试的机制划分区域。这样就可以抵御线程的资源抢占带来的问题。
线程可以在堆中划分出自己私有的缓冲区(Thread LocalAllocation Buffer,TLAB)。线程先使用 TLAB 的空间等到使用完毕后才用同步机制在堆上开辟空间。
方法区(Method Area)
方法区也是线程共享的
当一个 .class 文件被 Class Loader 加载后 .class 文件的所有信息都保存到了方法区内
类型信息
常量
静态变量
及时编译器编译后的代码缓存等数据
HotSopt 在 JDK 1.5 之前,方法区位于堆内,按照 GC 分代算法它位于永生代。
放在堆中可以使用 GC 统一管理,不必为方法区单独处理内存回收
再此种方案下方法区的大小需要提前划分好(永久代有 - XX:MaxPermSize 的上限,即使不设置也有默认大小)。这样一来方法的大小就有了限制,更容易 OOM
在 JDK 1.6 开始 HotSopt 开始逐步转移方法区到 Native 内存中。到了 JDK 8 方法区已经完全被移动到了 Native 内存
这也就是为啥说 JDK 8 开始就没有用永生代了
对于存储方法区的这段内存我们称作:元数据区。
在 JVM 规范中对方法区也定义了 OOM 异常。在早期方法区放在堆内实现的时候,很容易模拟出 OOM。后面转移到 Native 内存问题就不容易出现了。
对象初始化过程
对象的分类
数组对象
非数组对象-普通对象
类加载
.class 文件从磁盘、网络或者任何形式的存储器中加载到方法区
但加载过程并不是百分之百的,只有方法区中未找到此类的字符引用、或者此类还没有被加载到内存当中,才会执行。
在堆上分配内存
指针碰撞
如果堆内存是规整的,那么只需要一个指针记录已经使用和未使用内存的分割线即可。当分配新对象内存的时候只需要移动指针到对象长度即可。这种形式被称为:指针碰撞。
空闲列表
但如果内存是杂乱的,就没有办法通过一个指针的方式简单表示了。此只能维护一个状态表存储哪些内存已经被使用哪些内存是空闲的,此方式叫做空闲列表。
整理堆内存
在分配内存之后,虚拟机必须将分配的内存(对象头除外)初始化为零值
初始化数据
初始化对象头
分配内存之后,JVM 还要初始化对象的必要信息:HashCode、GC 分代的年龄、类的元数据地址等信息。
初始化对象数据
在经过以上的动作 JVM 遍认为对象已经创建完毕了,但是对于 Java 程序来说,构造函数还没有被执行
对象的内存布局
对象头:对象头保存了对象的重要信息类型指针、Mark word。
对象数据
对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。
对齐填充数据
对齐的目的视为了 GC 更简单
垃圾收集器已 8 byte 作为回收单位。将内存对齐为 8 byte 的整数倍可以更好的回收。
对象的访问定位
reference 储存句柄
reference 存储对象地址
方法的执行过程:Object a = new Object(); a.getName();
将 getName() 对应的栈帧入栈,等待 CPU 执行
以解释执行为例:执行引擎从程序计数器中获取需要执行的字节码,解释为机器码。
CPU 读取指令(程序指令 + 读取栈数据的指令)开始执行。
GC 与 内存分配策略
序计数器、虚拟栈、本地方法栈三部分使用的内存大小是确定的而且回收时机是稳定的。
他们都伴随着线程由生到灭,而栈帧的大小基本上在编译期就已经确定,随着方法的调用栈帧会经历入栈出栈的过程,当栈帧出栈时内存就会自动回收
Java 垃圾回收器针对的就是 Java 堆、方法区这两块内存空间
Java 堆和方法区的内存回收就没那么确定了
一个方法执行时因判断的条件不同所需要的内存大小不同、一个接口的具体实现类只有运行时才能确定所需的内存动态的,这就造成了内存开辟的不确定性
对象“已死”
这里对象已死可以理解为对象被判处了死刑,但是还没有正式执行
引用计数
引用计数是使用一块内存存储一个对象被引用了多少次,当引用次数为 0 即表示对象可以被回收了
原理简单,判断清晰,是一个简单高效的方法,但是在一些特定的场景下有很多例外情况要考虑,必须要配合大量额外判断才能保证正确性。譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
可达性分析
主流虚拟机都是用可达性分析判定对象是否”已死“
原理
根据一系列称为 “GC Root” 的对象作为起点集,从起点开始搜索引用关系
搜索走过的路径叫做引用链
如果一个对象到 GC Root 之间没有任何引用链相链,或者用图论的术语来说 GC Root 到此对象不可达,则证明此对象不可在被使用。
可固定作为 GC Root 的对象
虚拟机栈(栈帧中的局部变量表)中对象的引用,例如,参数、局部变量、临时变量
方法区内的常量、静态变量
JNI 引用的对象
Java 引用
定义:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该 reference 数据是代表某块内存、某个对象的引用
强应用
被强引用的对象对不会被 GC,宁可发生 OOM 都不会被回收
强引用是最常见的,是指代码中普遍存在的引用赋值
例如:Object object = new Object();
软引用
只存在软引用的对象在内存不足即将 OOM 的时候才会被回收
用来描述一些还有用,但非必须的对象
但是在 Android 3.0 开始,垃圾回收器会更积极地回收软引用/弱引用,具体啥时候回收没说,估计和弱引用差不多了
弱引用
只存在弱引用的对象无论当前内存是否充足,只要被标记了就会被回收。只能存活到下一次 GC。
弱引用是用来描述可有可无的对象,它比软引用要弱一些
虚引用
一个对象是否存在虚引用都不会对其生命周期造成任何影响
通过虚引用也获得不到对象实例
他的作用仅仅是在对象回收的时候得到一个通知
不稳定的 finalize()
当对象被判定为不可达后,仅表示对象被判处了死刑,并不是说对象 “已死”或 “非死不可”。而 finalize() 是对象自我救赎的最后机会
GC 流程
GC Root 到对象不可达,对象会被第一次标记
如果对象重写了 finalize 并从未执行过这执行 finalize
必须重写
没有执行过
并不一定每次都执行,只第一次执行且执行一次
需要执行 finalize() 的对象会被放置到 F-Queue 的队列中,等待低优先级的 Finalizer 线程调度执行
稍后收集器会对 F-Queue 内对象进行第二次规模标记
如果在 Finalizer 触发 finalize() 执行时,对象有变的可达(譬如把自己(this 关键字)赋值给某个类变量或者对象的成员变量)那么第二次遍历的时候就会移除回收的范围
如果没有逃脱接下来就会被回收了。
回收
缺点
运行成本高
需要单独的队列承载对象
需要单独的线程执行
不稳定
并不是每次都会执行,执行的时机不确定
虚拟机只承诺会触发 finalize() 的执行,并不保证 finalize() 一定会执行完毕。
为了保证不会因 finalize() 阻塞或死循环导致 F-Queue 持有的对象永远无法回收
finalize() 躲避 GC 实验
GC算法
分代理论
弱分代假说:大多数的对象都是朝生夕灭
强分代加收:经历过多次 GC 还存活的对象,就越难被消除。
GC 分类
部分收集(Partial GC):指目标不是完整收集整个 Java 堆的垃圾收集
新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有 CMS 收集器会有单独收集老年代的行为。另外请注意 “Major GC” 这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。
混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。
标记-清除
在可达性分析算法的技术上对存活的对象或可回收的对象进行标记,然后回收
会造成内存碎片
标记-复制
在标记 - 清除算法的基础上,将内存划分为为两个大小相等的区域,每次只使用其中一个区域。当需要 GC 时将存活对象复制到另一个区域,然后统一回收” 已死 “对象。
此算法解决了内存碎片化的问题,但每次只是用一个区域,对内存的容量是一个挑战。虽然每次复制会有性能上的消耗,但是根据弱分代假说每次复制的数据并不会很大。
算法优化
将年轻代划分为一个较大的 Eden 区 + 两个较小的 Survivor 区
在 HotSot 中 Eden 与 Survivor 的默认比例为 8 :1 :1
即每次新生代中只有 90% 的内存时可用的,另外的 10% 处于空闲状态。
每次分配内存时只会同时使用 Eden 和一个 Survivor 区域。对象优先使用 Eden 区创建
GC 的时候将 Eden 与 Survivor 中存活的对象统一复制到另外一个 Survivor 区域中然后直接清理 Eden 和已经使用过的 Survivor 区域
当存活的对象大于了 10% 的空间即超越了 Survivor 的大小,存活的对象直接放到老年代进行担保。
标记-整理
标记 - 复制算法因其复制数据的特点不适用有大量对象存活的老年代
更关键的是标记 - 清除算法必须有一个空闲空间作为存活对象复制的目标,这就无法应对可能有 100% 存活对象的老年代了。
此算法前期和标记 - 清除算法相同,但在后续的阶段里标记 - 整理算法不会直接释放可回收对象,而是将存活对象统一移动到内存空间的一侧,然后回收另一侧内存。
Stop-The-World
在 GC 时(扫描 + 回收)垃圾回收器会暂停所有的线程,这种状态被称作 Stop The World (STW)
原因:防止 GC 过程中不停的创建新的对象,导致分不清改回收哪些
Class Loading
将字节码加载到内存形成 JVM 可直接使用的 Java 类型的过程
加载
连接
初始化
Class Loading 包括了三部分
加载(Loading)
将字节码加载到内存
字节码文件的来源 JVM 规范没有指定,可以来自网络、流、anywhere
类加载器
实现类加载的动作,也为类提供了命名空间的作用
对于任意一个类,他的唯一决定方式是:类本身+加载此类的类加载器
为什么要如此设计
为了拓展性。这种情况出现在类本身的限制名(包名+类名)无法唯一区分类的时候
在不更改包名的情况,通过自定义类加载器让不同版本的kafka在同一个JVM下运行
类型
双亲委派模型 (Parent Delegation Model)
优先父加载器加载,若果父加载器加载不了,自己才能加载。
连接(Linking)
校验
准备
解析
初始化(Initalization)
运行时环境 JRE