JVM精简版【待背诵1013】
JVM组成、运行流程
Java Virtual Machine,就是Java虚拟机
好处:
一次编写,到处运行
内存管理,垃圾回收机制
即时编译JIT ,提升性能的最核心手段
【Java比C/C++性能低的原因就是为了实现跨平台需要在运行时将字节码实时解释成机器码】
由什么组成?
类加载器、运行数据区、执行引擎、本地库接口
运行流程
类加载器:将java代码转换成字节码
运行数据区:将字节码加载到内存中
执行引擎:将字节码翻译为机器码,交由CPU执行,
此时还需要调用本地库接口(JNI,C++编写,与操作系统交互如硬件接口系统API等)来实现整个程序功能。
类加载器
作用:加载 Java 类的字节码(.clas
)到 JVM 中,在内存中生成一个代表该类的 Class
对象
类加载器的种类
JDK8及以前
启动类加载器(Bootstrap ClassLoader,C++编写,加载核心的类,加载/jre/lib 下的类文件,如
resources.jar
扩展类加载器(允许扩展java中比较常见的类,加载/jre/lib/ext 下的类文件,如 dns 相关的包)
应用类加载器(加载应用使用的类,加载 classpath 下的类文件,默认加载的是项目中的类以及通过 maven 引入的第三方 jar 包中的类)
自定义类加载器:-Xbootclasspath/a:jar ,继承
ClassLoader
抽象类
JDK9及以后
扩展类加载器变成了平台类加载器(因为模块化系统的引入),同时启动类加载器变为java实现
双亲委派模型
原则:自底向上查找判断类是否被加载,自顶向上尝试加载类
好处:
避免某个类重复加载,保证唯一性
为了安全,保证类库API不会被修改,比如 java.lang.String
具体实现:java.lang.ClassLoader
的 loadClass()
方法中
打破双亲委派机制
自定义加载器需要继承 ClassLoader
并重写 loadClass()
方法
有个误区是重写 findClass()
方法还是重写 loadClass()
方法
重写
findClass()
:遵循双亲委派模型,父类加载器无法加载的类才会由自定义类加载器的findClass()
方法加载。因此重写findClass()
不破坏默认的类加载流程。重写
loadClass()
:可以打破双亲委派模型,绕过父类加载器的优先加载机制,直接由自定义类加载器加载类。
举例子: Tomcat 打破双亲委派模型
Tomcat的每个Web应用需要相互隔离,防止类冲突。为每个应用分配了一个独立的类加载器WebappClassLoader
,并首先在自己的类加载路径(WEB-INF/lib
)中查找类,优先加载Web应用的类和资源,而不是依赖父类加载器
SPI:通常依赖第三方库或服务提供者来实现接口,允许服务提供者通过META-INF/services
目录中的配置文件动态提供实现类,因此需要跳过父类加载器,直接从服务实现所在的类加载器中加载。通过ServiceLoader
实现
举例:JDBC,之前是通过class.forName加载数据驱动,现在通过SPI只要把相关的jar包放在指定目录下就可以
类装载过程 ❌
加载:相当于“门”
通过类的全限定名来获取定义此类的二进制字节流
将字节流转化为方法区的运行时数据结构
在内存中生成一个Class对象作为这个类的数据访问入口
链接(vpn过程)
v:verify验证:文件格式验证,字节码语义验证,程序语义验证,符号引用验证
p:prepare准备:正式为类变量分配内存并设置类变量初始值
n:neicun内存解析:常量池内的符号引用替换为直接引用
初始化
执行初始化方法
<clinit> ()
方法,JVM 才开始真正执行字节码一般来说只有当**对类的首次主动使用的时候才会导致类的初始化**
使用
卸载
主动使用:
创建类的实例,如new
执行静态变量初始化
执行静态代码块/静态方法
反射(如 Class.forName(“com.gx.yichun”))
初始化某个类的子类,则其父类也会被初始化
Java虚拟机启动时被标明为启动类的类(包含
main
方法的类)
运行数据区
包含了程序计数器、堆、方法区、虚拟机栈、本地方法栈
线程私有的:
程序计数器
虚拟机栈
本地方法栈
线程共享的:
堆
方法区
直接内存
简单介绍直接内存
不属于JVM管理,是虚拟机的系统内存,常见于NIO的操作,用于数据缓冲区,回收成本高,但读写性能强,用
ByteBuffer
创建常规IO的数据拷贝流程:磁盘文件——系统内存的缓冲区——Java堆内存的缓冲区
NIO:不需要在堆中开辟空间进行数据的拷贝,操作直接内存,从而使数据读写传输更快。
程序计数器
线程私有,唯一一个不会 OOM 的
背景:在任何时间点上,一个处理器只会处理执行一个线程
实现代码的流程控制
多线程记录当前线程执行的位置,当线程被切换回来的时候能够知道该线程上次运行到哪儿。
虚拟机栈
虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,
而本地方法栈则为虚拟机使用到的 Native 方法服务
线程私有,方法执行时的内存模型,生命周期与线程相同,每个方法被执行的同时会创建栈桢。
保存执行方法时的**局部变量(存储方法内的参数、临时变量和对象的引用)**、方法返回地址信息等等。
先进先出,调用入栈弹出出栈,所以这块区域不需要进行 GC。
局部变量表:存放编译期可知的各种数据类型和对象引用
操作数栈:中间计算结果、临时变量
动态链接:一个方法需要调用其他方法,将常量池中指向方法的符号引用转化为其在内存地址中的直接引用
Q:栈内存是越大越好吗?
栈帧过大,会导致线程数减少,默认是1024K,如果扩大一倍,那能活动的栈帧就会缩小一倍
Q:线程安全吗?
看有没有逃离方法的范围(传入参数/return),如果逃离了就可能是不安全的。
Q:什么情况下导致栈内存溢出?
栈帧过多:典型就是递归调用没有终止
StackOverFlowError
栈帧过大:比较少见
OutOfMemoryError
Q:对象一定在堆里面吗?具体怎么分析对象在栈中?栈中的对象和堆中的对象有什么不同
不一定,一般在堆中,但如果对象只在局部方法里面,没有外部引用,会直接在栈中创建(逃逸分析)
为什么大部分对象在堆中分配:
堆内存的大小可以动态调整,这允许程序在运行时动态地创建和销毁对象,而不会受到固定栈大小的限制。
大多数对象的生命周期较长,存在于方法调用之外,分配在堆上更为合适。
JVM 如何分析对象应该在栈中还是堆中:
栈内存:主要用于保存基本类型变量和执行方法调用时的局部变量。
堆内存:用于存储对象实例和数组。
如果对象的大小是在编译期可知的,并且生命周期可以确定为方法调用结束,则可能被分配在栈上。
对于生命周期较长或者大小不确定的对象,通常会被分配在堆上。
栈中的对象和堆中的对象有什么不同:
生命周期:栈中的对象的生命周期与方法调用的范围相关;而堆中的对象直到没有任何引用指向它时才会被回收。
访问速度:栈更快,因为线程私有,数据存取更加简单高效;而堆中的数据可能会发生碎片化,访问速度相对较慢。
大小和灵活性:栈内存大小固定,分配的对象大小也确定,而堆的内存大小和对象的大小都可以动态调整
本地方法栈
同虚拟机栈非常相似
本地方法栈则为虚拟机使用到的 Native 方法服务
不需要进行GC。
在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
堆
线程共享:主要用来保存对象实例、数组,内存不够就会抛出OOM
主要包括年轻代和老年代
年轻代:Eden区和两个大小相同的幸存者区(8:1:1)
老年代:主要保存生命周期比较长的对象,一般是老的对象
JDK1.7之前:1.7还有一个永久代,存储的是类信息、静态变量、常量、编译后的代码
JDK1.8后:移除了永久代,把数据存储在了本地内存的元空间中,防止内存溢出。
区别:元空间使用本地内存。因此大小仅受本地内存限制
在栈上通过
s1
和s2
两个局部变量保存堆上两个对象的地址,从而实现了引用关系的建立。
方法区
是各个线程共享的内存区域,主要存储类的元信息、运行时常量池、字符串常量池,虚拟机启动的时候创建,关闭虚拟机时释放
OutOfMemoryError:Metaspace
类的元信息:类的字段、方法等字节码文件中的内容
常量池可以看做是一张表,虚拟机指令根据这张表找到要执行的类名、方法名等信息。当类被加载时,它的常量池信息就会放入运行时常量池,并把里面的符号地址改为真实地址。
GC垃圾回收
1.7 前分为新生代、老年代、永久代
1.8 后分为新生代、老年代、元空间(直接内存)
内存分配原则
大多数情况下,对象在新生代中 Eden 区分配;Eden 不够发起 MinorGC,期间发现新生代内存还是不够就触发空间分配担保机制,部分对象移入老年代, 解决新生代内存不足问题防止 OOM
大对象直接进入老年代,减少新生代垃圾回收成本
长期存活对象进入老年代【先在 Eden 区域分配,新生代垃圾回收后还存在,就进入 S0 或者 S1,对象年龄+1,到了 15 就会被晋升到老年区(这个值是可以修改的,但是还是在 15 以内,因为对象头的年龄占用大小是 4 位)】
GC 种类
部分收集 (Partial GC):
新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集,暂停时间短
老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集,G1收集器持有
整堆收集 (Full GC):收集整个 Java 堆和方法区,暂停时间长,应该尽力避免
对象什么时候进行垃圾回收?
引用计数法:循环引用会导致内存泄露,一般**不推荐使用**
可达性算法:看能否沿着GC ROOT对象为起点的引用链找到该对象,找不到就是垃圾
三色标记法
白色。表示对象尚未被垃圾收集器访问过,在可达性分析的开始阶段,所有对象都是白色的。如果对象最终保持白色,则表示该对象不可达,需要被回收。
灰色。表示对象已经被垃圾收集器访问过,但至少存在一个引用还未被扫描,即整个引用链尚未完全扫描完毕。
黑色。表示对象及其所有引用均已被扫描过,这些对象是安全存活的,如果有其他对象引用指向了黑色对象,无需重新扫描。
https://www.cnblogs.com/chanshuyi/p/head-first-of-triple-color-marking-algorithm.html
什么对象可以作为GC ROOT
虚拟机栈(栈帧中的局部变量表)中的引用对象
方法区中类的静态属性引用的对象(static修饰的变量)
方法区中的常量池中保存的常量
本地方法调用时使用的全局对象
活跃的线程
同步锁持有的对象
类加载器
引用类型总结
强引用:必不可少,宁愿 OOM 也不回收
软引用:可有可无,空间够就不回收
弱引用:可有可无,扫描到此块区域时,内存够不够都回收
虚引用:必须配合引用队列来使用,被引用对象回收时,会将虚引用对象入队,由handler处理,唯一目的就是就是在这个对象被回收器回收的时候收到一个系统通知或者后续添加进一步的处理
ThreadLocal 内存泄漏问题,就是因为 JVM 的弱引用
key 是对象本身,是弱引用
value 是
ThreadLocalMap
,是强引用,无法自动清除,导致无法回收,内存泄漏解决方法:调用 remove 手动清理;避免在线程池中是用,因为线程池的线程是长生命周期的
垃圾清除算法【方法论】
标记清除算法
过程:从GCRoot开始标记,再统一清除没有标记的
缺点:内存碎片,导致的空闲链表分配速度慢
标记整理算法
标记:将所有存活的对象进行标记。Java 中使用可达性分析算法,从 GC Root 开始通过引用链遍历出所有存活对象。
整理:将存活对象移动到堆的一端。清理掉存活对象的内存空间。
清除:
解决了碎片化的问题,多了一步对象移动位置的步骤,对效率有一定的影响。
复制算法
分成 FROM 和 TO 两块空间,互相搬运
优点:在垃圾对象多的情况下效率高,因为无内存碎片,分配内存时只需要移动指针
缺点:内存使用率低,只能用一半,不适合老年代(因为如果存活数量比较大复制会很慢)
分代垃圾回收算法
区域划分:
新生代:老年代=1:2
新生代的eden和幸存者区s0,s1=8:1:1
流程
【对象分代】新创建的对象,分配到新生代的 eden区,新生代占比5%-6%
【新生代垃圾回收】eden区不足以后,触发 Minor GC(复制算法)
标记现阶段存活对象
将存活对象复制到幸存者区中的其中一块 (需要暂停用户线程,时间短)/ 晋升到老年代(如果幸存者区不足或者对象比较大会提前晋升)
一直重复
【老年代回收】老年代不够(默认是45%),触发 MajorGC 或者 FullGC
根据不同算法进行不同清除逻辑
标记清除:从GCRoot开始标记,再统一清除没有标记的
垃圾回收器【具体体现】
Serial 串行垃圾收集器
垃圾回收时单线程工作,并且java应用中的所有线程都要暂停(STW)
Serial:新生代,复制算法
Serial old:老年代,标记整理算法
单 CPU 好,多CPU 吞吐量不好,STW时间过长,适合个人电脑
ParNew 并行垃圾收集器(JAVA8 默认)
多线程版的Serial,算法使用一样
parallel Scavenge:新生代,复制算法
Parallel old:老年代,标记整理算法
CMS并发垃圾收集器(只有JDK14有)
流程:初始标记(阻塞,只标记GC ROOT一级关联)—— 并发标记 —— 重新标记(阻塞)---并发清理
老年代:标记清除算法
优点:并发收集,保证 STW时间短
缺点:内存碎片、浮动垃圾
G1(Garbage-First)垃圾回收器(JDK9 开始默认)
面向服务器,低 STW 高吞吐量
老年代+新生代:都是复制算法
TODO:ZGC 垃圾回收
G1 垃圾回收器工作流程
初始标记:标记与 GC Roots 直接关联的对象,修改 TAMS 值(确保新对象再可用内存区域创建,避免和垃圾回收过程冲突),确保并发运行时能正确分配对象。需要短暂停顿,但耗时极短,常与 Minor GC 同步完成,无额外停顿
并发标记:从 GC Roots 开始进行堆对象的可达性分析,找出存活对象。此阶段与用户程序并发执行,耗时较长,但不影响程序运行,并处理并发时引用变化的对象。
最终标记:进行短暂停顿,处理并发标记阶段后遗留的引用变化,确保所有存活对象已被标记。
筛选回收:根据回收价值和成本对 Region 进行排序,制定回收计划,复制存活对象至空的 Region,清空已选定的 Region。
除了并发标记外,其余过程都要 STW
Q:为什么要 分成并发标记和初始标记
提高效率,初始标记要 STW
Q:如果并发失败,即回收速度赶不上创建新对象的速度,怎么办?
触发 Full GC
JVM 调优参数
看 JVM 高级篇的黑马笔记【E:\Project\Java\JavaStudy\JVM\2024暑假学习\docs,感觉一般?】
相关工具
jclasslib
工具查看字节码文件:
基础信息:魔数(确定文件类型 0xcafebabe)、字节码文件对应的 Java 版本号、访问标识(public final 等等)、父类和接口信息
常量池:保存了字符串常量、类或接口名、字段名,主要在字节码指令中使用【字节码指令中通过编号引用到常量池的过程称之为 符号引用 】
字段:当前类或接口声明的字段信息
方法:当前类或接口声明的方法信息,核心内容为方法的字节码指令
属性:类的属性,比如源码的文件名、内部类的列表等
javap
JDK 自带的反编译工具,可以通过控制台查看字节码文件的内容。适合在服务器上查看字节码文件内容。
输入 javap -v
字节码文件名称 查看具体的字节码信息
jclasslib
idea插件,可以直接在idea看字节码文件内容
Arthas
最后更新于