JVM
Java 运行时数据区域
程序计数器
当前线程所执行的字节码的行号指示器
程序计数器是唯一一个不会出现 OutOfMemoryError
的内存区域
虚拟机栈
调用除本地方法外所有java方法都会产生一个栈帧,里面存放局部变量表、操作数栈、动态链接、方法返回地址
当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用
本地方法栈
调用本地方法的栈帧,存放信息与虚拟机栈类似,方法返回地址变成出口信息
堆
1.7 之前
- 新生代内存(Young Generation)
- 老生代(Old Generation)
- 永久代(Permanent Generation)
1.8 之后永久代变成元空间
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。你可以使用 -XX:MaxMetaspaceSize
标志设置最大元空间大小,默认值为 unlimited。能存储的信息更加多
方法区
方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
直接内存
Java NIO 中,可以直接调用Native方法分配堆外内存,DirectByteBuffer 可以直接引用这块内存
对象的创建流程
- 类加载检查,先去常量池中是否存在这个类的符号引用,判断是否加载、解析、初始化过,如果没有就进行类加载过程
- 分配内存,指针碰撞取决于内存是否规整,不规整就使用空闲列表方法
- 初始化零值,分配的内存进行零值填充。
- 加载对象头,设置gc年龄、哈希码、分代信息,指向类的指针
- 执行init方法.
什么是TLAB ?
- 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
内存分配策略
- 对象优先在Eden区分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代(没经过一次minorGC年龄就增加一岁,第一次会从Eden区进入Survivor区,岁数为1,默认十五岁进入老年代)
空间分配担保
在进行minorGC之前,先确认老年代是否可以容纳所有新生代的对象,可以容纳就可以进行,否则会检查程序员是否设置参数允许担保失败
GC分类
部分GC
- minorGC, youngGC(触发时机 eden 区满的时候)
- MajorGC, OldGC
- MixedGC(新生代的全部和部分老年代)
fullGC(触发younggc前,发现之前晋升到老年代的比老年代剩余空间大的时候)
永久代没有空间、System.gc()、heap dump的时候
触发fullGC的原因
- 系统并发量高,处理数据量过大导致youngGC每次作用很有限,还是有较多对象存活,年龄到达一定岁数就会进入老年代,晋升的对象太多大于剩余空间时就会触发fullGC
- 内存分配不合理,Survivor区过小,导致对象频繁进入老年代,触发fullGC
- 一次性加载过多数据到内存当中,新生代无法存储,大对象直接进入老年代(数据库查询结果集过大)
- 发生了内存泄漏,大量对象无法回收,导致fullGC(ThreadLocal 内存泄漏,每次使用完后,使用remove())
- System.gc()
- 如果存在永久代,加载的类、反射的类和调用的方法较多的时候,永久代也需要GC,并且只能通过fullGC
死亡对象分析法
引用计数法(互相引用的问题)
可达性分析(GCRoots 出发,可以是虚拟机栈和本地方法栈中的对象,方法区中静态变量或常量引用的对象,被同步锁持有的对象)宣告对象的死亡至少要经过两次标记
引用总结
强引用
平时使用的基本都是强引用,抛出内存不足的异常也不会报错
软引用
可有可无,如果空间不够了就进行回收
弱引用
一旦发现了弱引用就会进行回收
不过垃圾回收器是一个优先级很低的线程,不会很快发现这些弱引用
虚引用
如果持有虚引用那么任何时候都会被回收
虚引用和软引用的区别
虚引用必须和引用队列一起使用,主要目的是在虚引用被回收的时候通知一下
官方说法是程序发现引用队列中有虚引用的时候,能在对象被回收的时候采取必要的动作
如何判断一个变量是一个废弃的变量
该变量没有被任何引用
如何判断一个类是一个废弃的类
- 该类所有的实例都被回收
- 该类的classloader被回收
- 没有在任何地方被引用,包括反射访问其类的方法或者变量
垃圾回收
GC
垃圾收集算法
标记-整理 (适合老年代这种整理情况比较少的)
标记-清除(会产生大量不连续的内存碎片)
复制(需要相同的一块内存,会导致内存减半)
分代收集(各个分代特点不同,新生代产生和消亡快,老年代比较稳定)
默认垃圾收集器
JDK 默认垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version
命令查看):
- JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
- JDK 9 ~ JDK20: G1
垃圾收集器
新生代使用复制算法、老年代使用标记整理算法
Serial收集器
单线程
在垃圾收集的时候会暂停所有其他线程、简单高效
ParNew收集器
新生代使用复制算法
stw的现象仍然存在
Parallel Scavenge收集器
主要关注点在吞吐量
Parallel old收集器
多线程老年代收集器可以和Parallel Scavenge
Serial old 收集器
主要作用1.5和以前和与Parallel Scavenge、和cms收集器配合
CMS收集器
以最短停顿时间为目标
- 初始标记(标记所有和Root连接的对象、STW)
- 并发标记(记录用户线程更新的引用)
- 重新标记(stw重新进行标记)
- 并发清理
cpu资源敏感,需要多个线程进行标记清理并且是与用户线程并行
无法处理浮动垃圾
基于标记-清除算法会有大量不连续内存空间产生(空间碎片)
G1收集器
- 并行与并发:适合多cpu场景
- 分代收集:可以都用g1收集器,也可以分代收集
- 空间整合:宏观上标记-整理,微观上是基于复制算法
- 可预测的停顿:建立可停顿的预测时间模型,
流程
- 初始标记(stw)
- 并发标记
- 最终标记(stw)
- 筛选回收(会维护一个优先列表,根据每次允许的时间,选择回收价值最大的region)(stw)
ZGC
类加载机制
整体分为
加载 连接 初始化 使用 卸载
加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
细分又可分为
加载
通过全类名获取此类的二进制数据流、将静态存储区域数据集转化为方法区运行时数据结构、内存中生成一个class对象作为访问这些数据的入口
验证
检查是否符合java虚拟机规范
- class文件格式检查
- 元数据检查(是否继承父类、是否继承了不能继承的父类)
- 字节码检查(通过数据流和控制流分析程序语义,对象类型转化是否合理、函数的参数类型是否正确)
- 符号引用检查(是否引用了其他的类、方法、字段是否存在或者拥有正确的访问权限)
准备
为类变量分配内存并且设置初始化值
解析
将常量池的符号引用转化为直接引用(转化为内存中偏移量)
(字段、类方法
、类、接口方法、方法类型、方法句柄、调用限定符)
初始化
执行clinit方法
类在以下情况会主动初始化
- new一个类的时候、获取或设置静态字段值的时候、调用静态方法的时候
- 反射
- 初始化类的时候父类没有初始化
- main主类
- MethodHandler和VarHandle 反射调用机制
- default修饰的接口方法,实现该接口的类初始化了这个类也需要进行初始化
使用
卸载
卸载一个类3个前提
- 所有实例都被gc
- 没有在任何地方被引用
- 该类的所有类加载都被回收
类加载器
类加载器的主要目的加载java字节码(.class文件)到jvm中(在内存中生成一个class对象)
双亲委派模型
不仅要看类名,还要看此类的类加载器是否一样
优点
保证类不会被重复加载
保证java核心api稳定性(如果重写了java.lang.object的话,由于是自顶向下加载类,启动类加载器一开始就加载过了,到不了自定义的类加载器)
破坏双亲委派模型
重写loadclass()方法