持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的8天,点击查看活动详情
介绍
这篇文章简要而精炼地介绍了 JVM 的内存结构,垃圾回收 和 类加载部分,是基础部分的介绍,相当于八股,而字节码等相关技术日后会继续更新;
内存结构
程序计数器
作用:记录下一条指令的执行的地址
特点:
- 线程私有
- 不会存在内存溢出
虚拟机栈 JVM Stacks
定义
作用:每个线程运行时需要的内存空间,有多个栈帧组成;
栈帧:每个方法运行时需要的内存,当前执行的栈帧称为活动栈帧;
注意:
- 栈内存不是越大越好
- 栈内的没有逃离方法作用范围的局部变量是线程私有的,即该参数不是作为参数传入或结果返回(特别是引用类型)
栈内存溢出
- 栈帧过多,递归调用
- 栈帧过大
jstack + 进程id:查看某个进程中所有线程运行情况
本地方法栈 Native Method Stacks
与虚拟机栈差不多,但调用的都是native方法
堆
定义
通过new关键字,创建对象都会用到堆内存
特点:
- 线程共享,要考虑线程安全问题
- 有垃圾回收机制
堆内存诊断
- jsp:查看当前系统中有哪些Java进程
- jmap: 查看堆内存占用情况 ===> jmap + heap + 进程id
- jconsole:多功能的监测工具,可以连续监测,图形界面
- jvirsualvm:功能与jconsole类似,但比jconsole高级
方法区
定义:所有Java线程共享,存储类的结构,类的字段,成员方法和构造器方法,运行时常量池,在虚拟机启动时创建
结构:
二进制字节码:类基本信息,常量池,类方法定义(包含虚拟机指令)
常量池
就是一张表,虚拟-机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息
运行时常量池
当类被加载时,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
常量池和串池的关系
StringTable
- 常量池中的字符串只是符号,第一次用到时才会变成对象(字符串延迟加载)
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接原理是StringBuilder
- 字符串常量拼接原理是编译器优化
- 可以使用intern方法,主动将串池中还没有的字符串放入串池
- 1.8将这个字符串对象尝试放入串池,如果有则不放入(不改变原来字符串的指向,且返回池中的对象),如果没有则放入串池,会把串池中的对象返回。(即该字符串对象指向串表中,而不是堆中)
位置
1.8位于堆中,1.6之前位于方法区中
垃圾回收
stringtable里面的对象是可以被垃圾回收的
调优
- 调整 -XX:StringTableSize=桶个数
- 考虑将字符串对象入池,存放在stringtable中
直接内存
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,
- 但读写性能高 不受 JVM 内存回收管理
分配和回收原理
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调 用 freeMemory 来释放直接内存
垃圾回收
判断垃圾
- 引用计数法
- 可达性分析算法
-
- Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以 回收
四种引用
- 强引用
- 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
- 软引用
- 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用 对象
- 可以配合引用队列来释放软引用自身
public class String1 {
private final static int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
List<SoftReference<byte[]>> list = new ArrayList<>();
// 使用引用队列 排除已经垃圾回收的引用,若该对象已经被垃圾回收,弱引用就会添加到该队列中
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
for (int i = 0; i < 10; i++) {
// 创建SoftReference对象, 实现软引用
SoftReference<byte[]> softReference = new SoftReference<>(new byte[_4MB], referenceQueue);
// 添加集合中
list.add(softReference);
}
// 排除为空的信息
Reference<? extends byte[]> poll = referenceQueue.poll();
while (poll != null) {
list.remove(poll);
poll = referenceQueue.poll();
}
System.out.println("===================");
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
System.in.read();
}
}
- 弱引用
- 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
- 可以配合引用队列来释放弱引用自身
public class String1 {
private final static int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
List<WeakReference<byte[]>> list = new ArrayList<>();
// 引用队列
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
// 创建对象
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB], referenceQueue);
list.add(ref);
}
Reference<? extends byte[]> poll = referenceQueue.poll();
// 排除无效的内容
while (poll != null) {
list.remove(poll);
poll = referenceQueue.poll();
}
System.out.println("=============");
for (WeakReference<byte[]> weakReference : list) {
System.out.println(weakReference.get());
}
}
}
- 虚引用
- 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存
- 终结器引用
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象
垃圾回收算法
- 标记清除
速度较块,但会造成内存碎片
- 标记整理
速度慢,没有内存碎片
- 复制
不会有内存碎片,需要占用双倍内存空间
分代垃圾回收
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用copy 复制算法到 to中,存活的对象年龄加1并且交换 from to
- minor gc 会引发stop the world ,暂停其他的用户线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升为老年代,最大寿命为15(4bit)
- 当老年代空间不足时,先会尝试触发minor gc,如果之后空间仍不足,那么会触发 full gc,STW的时间更长
- 相关JVM参数
垃圾回收器
- 串行
- 单线程
- 堆内存较小,适合个人电脑
- 吞吐量优先
- 多线程
- 堆内存较大,多核CPU
- 让单位时间内,STW的时间最短
- 响应时间优先
- 多线程
- 堆内存较大,多核CPU
- 尽可能让单次 (每次) STW的时间最短
串行
垃圾回收后可能会改变内存空间的地址,所以要设定安全点让进程停下来
吞吐量优先
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio ,公式 1/(1+ratio) ,表示在运行期间的总时间中进程停止时间占总运行时间的比例
-XX:MaxGCPauseMillis=ms ,表示每次暂停的最大时间
-XX:ParallelGCThreads=n
响应时间优先
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads :并发线程数/并发GC线程数
-XX:CMSInitiatingOccupancyFraction=percent :占用多大内存时触发垃圾回收
-XX:+CMSScavengeBeforeRemark :新生代的对象引用老年代的对象,重新标记时先对新生代进行垃圾回收
G1垃圾回收器
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
- 超大堆内存,会将堆划分为多个大小相等的 Region
- 整体上是 标记+整理 算法,两个区域之间是 复制 算法
三个阶段
Young Collection
会 STW
Young Collection + Concurrent Mark
在 Young GC 时会进行 GC Root 的初始标记
老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
Mixed Collection
会对 E、S、O 进行全面垃圾回收
- 最终标记(Remark)会 STW
- 拷贝存活(Evacuation)会 STW
类加载与字节码技术
字节码指令
类加载阶段
加载
- 将类的字节码载入方法区中
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
链接
- 验证
验证类是否符合 JVM规范,安全性检查,检查字节码文件的魔数
- 准备
为 static 变量分配空间,设置默认值
- static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
- static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
- 解析
将常量池中的符号引用解析为直接引用
初始化
初始化即调用 ()V ,即构造方法,虚拟机会保证这个类的『构造方法』的线程安全
发生时机
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
不导致初始化的情况
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName 的参数 2 为 false 时