JVM基础详解
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
1.JVM内存结构
1.1 内存结构划分
以上代码执行过程
执行 javac 命令编译源代码为字节码
执行 java 命令
- 创建 JVM调用类加载子系统加载 class将类的信息存入方法区
- 创建 main 线程使用的内存区域是 JVM 虚拟机栈开始执行 main 方法代码
- 如果遇到了未见过的类会继续触发类加载过程同样会存入方法区
- 需要创建对象会使用堆内存来存储对象
- 不再使用的对象会由垃圾回收器在内存不足时回收其内存
- 调用方法时方法内的局部变量、方法参数所使用的是 JVM 虚拟机栈中的栈帧内存
- 调用方法时先要到方法区获得到该方法的字节码指令由解释器将字节码指令解释为机器码执行
- 调用方法时会将要执行的指令行号读到程序计数器这样当发生了线程切换恢复时就可以从中断的位置继续
- 对于非 java 实现的方法调用使用内存称为本地方法栈
- 对于热点方法调用或者频繁的循环代码由 JIT 即时编译器将这些代码编译成机器码缓存提高执行性能
定义
类加载子系统在运行程序时首次运行类时进行 加载——连接——初始化
可运行数据区共享区
和独占区
。共享区主要包括方法区和堆区独占区包括程序计数器、虚拟机栈和本地方法栈方法区 存放已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等
堆 存放对象、数组、非静态变量
程序计数器 可以正确控制Java程序中的流程控制正确轮换多线程
虚拟机栈每个方法对应一个栈帧栈帧包含局部变量表、操作数栈、动态链接、方法返回值等
本地方法栈 不是使用Java实现的函数用来支持本地方法的调用逻辑的
1.2 内存溢出的区域
-
不会出现内存溢出的区域——程序计数器
-
出现 OutOfMemoryError 的情况
-
堆内存耗尽 – 对象越来越多又一直在使用不能被垃圾回收
-
方法区内存耗尽 – 加载的类越来越多很多框架都会在运行期间动态产生新的类
-
虚拟机栈累积 – 每个线程最多会占用 1 M 内存线程个数越来越多而又长时间运行不销毁时
-
-
出现 StackOverflowError 的区域
- JVM 虚拟机栈原因有方法递归调用未正确结束、反序列化 json 时循环引用
1.3 方法区永久区元空间
定义
方法区 JVM规范中定义的一块内存区域用来存储类元数据、方法字节码、即时编译器需要的信息等
永久代HotSpot虚拟机对JVM规范的实现JDK1.8之前
元空间 HotSpot虚拟机对JVM规范的另一种实现JDK1.8之后使用本地内存作为这些信息的存储空间
解释
- 当第一次用到某个类的时候由类加载器将class文件的类元信息读入并存储于元空间
- 类元信息是存储于元空间中无法直接访问
- 可以用 .class文件间接访问类元信息它们两属于Java对象我们在代码中可以使用
- 堆内存中当一个类加载器对象这个类加载器对象加载的所有类对象这些类对象对应的所有实例对象都没人引用时GC就会对它们占用的内存进行释放
- 元空间中内存释放以类加载器为单位 当堆中类加载器内存释放时对应的元空间中的类元信息也会释放
2. JVM内存参数
2.1 堆内存设置
具体参数
-Xms 最小堆内存 包括新生代和老年代
-Xmx 最大堆内存包括新生代和老年代
通常建议将最小堆内存和最大堆内存设置为大小相等即不需要保留内存不需要从小到大增长这样性能较好
-XXNewSize 与 -XXMaxNewSize设置新生代的最小与最大值但一般不建议设置由JVM自己控制
**-Xmn**设置新生代大小相当于同时设置了 -XXNewSize 与 -XXMaxNewSize 并且取值相等
图中的保留是指一开始不会占用那么多内存随着使用内存越来越多会逐步使用这部分保留内存
按比例设置
🔔
-XX:NewRatio=2:1 表示老年代占两份新生代占一份
-XX:SurvivorRatio=4:1 表示新生代分成六份伊甸园占四份from 和 to 各占一份
2.2 元空间内存设置
🔔
class space 存储类的基本信息最大值受 -XX:CompressedClassSpaceSize 控制
non-class space 存储除类的基本信息以外的其它信息如方法字节码、注解等
class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制
2.3 代码缓存内存设置
🔔
- 如果 -XX:ReservedCodeCacheSize < 240m所有优化机器代码不加区分存在一起
- 否则分成三个区域图中笔误 mthod 拼写错误少一个 e
- non-nmethods - JVM 自己用的代码
- profiled nmethods - 部分优化的机器码
- non-profiled nmethods - 完全优化的机器码
3.JVM垃圾回收
3.1 3种垃圾回收算法
1.标记清除算法
🔔解释
- 找到 GC Root 对象即那些一定不会被回收的对象如正执行方法内局部变量引用的对象、静态变量引用的对象
- 标记阶段沿着 GC Root 对象的引用链找直接或间接引用到的对象加上标记
- 清除阶段释放未加标记的对象占用的内存
要求
要点
- 标记速度与存活对象线性关系
- 清除速度与内存大小线性关系
- 缺点会产生内存碎片无法找到足够的连续内存
2.标记整理算法
🔔解释
- 前面的标记阶段、清理阶段与标记清除法类似
- 相较之前多了一步整理的动作将存活对象向一端移动可以避免内存碎片的产生
特点
- 标记速度与存活对象成线性关系
- 清除、整理速度与内存大小成线性关系
- 缺点移动对象极为负重必须全程暂停用户应用程序才能进行性能上较慢
3.标记复制算法
🔔解释
将整个内存分成两个大小相等的区域from 和 to其中 to 总是处于空闲from 存储新创建的对象
标记阶段与前面的算法类似
在找出存活对象后会将它们从 from 复制到 to 区域复制的过程中自然完成了碎片整理
复制完成后交换 from 和 to 的位置即可
特点
标记与复制速度与存活对象成线性关系
缺点是会占用成倍的空间
3.2 GC与分代回收算法
1 GC的目的 实现无用的对象内存自动释放减少内存碎片、加快分配速度
2GC的要点
-
回收区域是堆内存不包括虚拟机栈
-
判断无用的对象的方法有可达性分析算法、三色标记法标记存活的对象回收未标记的对象
-
GC的具体实现称为垃圾回收器
-
GC 大都采用了分代回收思想建立在弱分代假说、强分代假说和跨代引用假说之上
-
理论依据是大部分对象朝生夕灭用完立刻就可以回收另有少部分对象会长时间存活每次很难回收
-
根据这两类对象的特性将回收区域分为新生代和老年代新生代采用标记复制法、老年代一般采用标记整理法
-
-
根据 GC的规模可以分成 Minor GC、Mixed GC、Full GC
3判断无用的对象的方法***
可达性分析算法从GC Roots为起点开始遍历整个对象图和GC Roots直接或间接相连的对象才是存活对象反之就是死亡对象。从GC Roots搜索过的路径叫做引用链。
三色标记法使用三种颜色表示对象的标记状态分别是 黑色——已标记灰色——标记中白色——未被标记
42.分代回收
- 伊甸园 eden最初对象都分配到这里与幸存区 survivor分成 from 和 to合称新生代
2. 当伊甸园内存不足标记伊甸园与 from现阶段没有的存活对象
3. 将存活对象采用复制算法复制到 to 中复制完毕后伊甸园和 from 内存都得到释放
4.
5. 将 from 和 to 交换位置
6. 经过一段时间后伊甸园的内存又出现不足
7. 标记伊甸园与 from现阶段没有的存活对象
- 将存活对象采用复制算法复制到 to 中
-
复制完毕后伊甸园和 from 内存都得到释放
-
将 from 和 to 交换位置
-
老年代 old当幸存区对象熬过几次回收最多15次晋升到老年代幸存区内存不足或大对象会导致提前晋升
5GC 规模
- Minor GC 发生在新生代的垃圾回收暂停时间短
- Mixed GC 对新生代和老年代的部分区域进行垃圾回收G1垃圾收集器特有
- Full GC 新生代和老年代完整垃圾回收暂停时间长应全力避免
6) 三色标记
即用三种颜色记录对象的标记状态
- 黑色 – 已标记
- 灰色 – 标记中
- 白色 – 还未标记
- 起始的三个对象还未处理完成用灰色表示
- 该对象的引用已经处理完成用黑色表示黑色引用的对象变为灰色
- 依次类推
- 沿着引用链都标记了一遍
- 最后为标记的白色对象即为垃圾
3.3 并发漏标问题
比较先进的垃圾回收器都支持并发标记即在标记过程中用户线程仍然能工作。但这样带来一个新的问题如果用户线程修改了对象引用那么就存在漏标问题。
因此对于并发标记而言必须解决漏标问题也就是要记录标记过程中的变化。有两种解决方法
- Incremental Update 增量更新法CMS 垃圾回收器采用
- 思路是拦截每次赋值动作只要赋值发生被赋值的对象就会被记录下来在重新标记阶段再确认一遍
- Snapshot At The BeginningSATB 原始快照法G1 垃圾回收器采用
- 思路也是拦截每次赋值动作不过记录的对象不同也需要在重新标记阶段对这些对象二次处理
- 新加对象会被记录
- 被删除引用关系的对象也被记录
3.4 垃圾回收器
1.Paraller GC并行垃圾回收器
eden 内存不足发生 Minor GC采用标记复制算法需要暂停用户线程
old 内存不足发生 Full GC采用标记整理算法需要暂停用户线程
注重吞吐量的时候使用这种垃圾回收器
2.ConncurrentMarkSweep GCCMS垃圾回收器
工作在 old 老年代支持并发标记的一款回收器采用标记清除算法
- 并发标记时不需暂停用户线程
- 重新标记仍需暂停用户线程
如果并发失败回收速度赶不上创建新对象的速度会触发 Full GC
注重响应时间的时候使用Paraller GC并行垃圾回收器)
3.G1 GC
将整个堆内存划分为多个大小相等区域每个区域都可以充当 edensurvivoroldhumongous专为大对象准备
分为三个阶段新生代回收、并发标记、混合收集
1G1 回收阶段 - 新生代回收
-
初始时所有区域都处于空闲状态
-
创建了一些对象挑出一些空闲区域作为伊甸园区存储这些对象
-
当伊甸园需要垃圾回收时挑出一个空闲区域作为幸存区用复制算法复制存活对象需要暂停用户线程
-
复制完成将之前的伊甸园内存释放
-
随着时间流逝伊甸园的内存又有不足
-
将伊甸园以及之前幸存区中的存活对象采用复制算法复制到新的幸存区其中较老对象晋升至老年代
-
释放伊甸园以及之前幸存区的内存
2G1 回收阶段 - 并发标记与混合收集 -
当老年代占用内存超过阈值后触发并发标记这时无需暂停用户线程
-
并发标记之后会有重新标记阶段解决漏标问题此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象随后进入混合收集阶段。此时不会对所有老年代区域进行回收而是根据暂停时间目标优先回收价值高存活对象少的区域这也是 Gabage First 名称的由来。
3.混合收集阶段中参与复制的有 eden、survivor、old下图显示了伊甸园和幸存区的存活对象复制
4.复制完成内存得到释放。进入下一轮的新生代回收、并发标记、混合收集
如果并发失败会触发 Full GC
响应时间和吞吐量兼顾
4. 内存溢出
这里举几种典型的导致内存溢出的情况
- 误用线程池导致的内存溢出
- 查询数据量太大导致的内存溢出
- 动态生成类导致的内存溢出
4.1 误用线程池导致的内存溢出
示例1 通过Executors自动创建 FixedThreadPool 线程池代码如下
代码
private static void case1() {
ExecutorService executor = Executors.newFixedThreadPool(2);
LoggerUtils.get().debug("begin...");
while (true){
executor.submit(() -> {
try {
LoggerUtils.get().debug("send sms...");
TimeUnit.SECONDS.sleep(30);
}catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
运行结果
17:31:44.144 [main] DEBUG G - begin...
17:31:44.148 [pool-1-thread-1] DEBUG A - send sms...
17:31:44.148 [pool-1-thread-2] DEBUG B - send sms...
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
造成原因
我们查看源码可以发现其中使用的工作队列为 LinkedBlockingQueue它是一个无界的工作队列任务数量将队列塞满
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
解决办法
我们在使用Executors自动创建线程的时候尽量不要使用 newFixedThreadPool 这种方式
示例2 通过 Executors 自动创建带缓存的线程池 CachedThreadPool代码如下
static AtomicInteger c = new AtomicInteger();
private static void case2() {
ExecutorService executor = Executors.newCachedThreadPool();
while (true){
System.out.println(c.incrementAndGet());
executor.submit(() -> {
try {
TimeUnit.SECONDS.sleep(30);
}catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
这里测试可以学习视频中在linux系统中修改进程和线程的最大数来进行测试这里建议最好不要在Windows下直接测试
造成原因
我们查看源码可以发现其中使用的工作队列为 SynchronousQueue 它创建的线程是没有数量限制的创建的线程数量过多耗尽系统的线程资源
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
解决办法
我们在使用Executors自动创建线程的时候尽量不要使用 newCachedThreadPool 这种方式
4.2 查询数据量太大导致的内存溢出
代码
public class TestOomTooManyObject {
public static void main(String[] args) {
//对象本身内存
long a = ClassLayout.parseInstance(new Product()).instanceSize();
System.out.println(a);
// 一个字符串占用内存
String name = "联想小新Air14轻薄本 英特尔酷睿i5 14英寸全面屏学生笔记本电脑(i5-1135G7 16G 512G MX450独显 高色域)银";
long b = ClassLayout.parseInstance(name).instanceSize();
System.out.println(b);
String desc = "【全金属全面屏】学生商务办公全新11代处理器MX450独显100%sRGB高色域指纹识别快充更多好货";
long c = ClassLayout.parseInstance(desc).instanceSize();
System.out.println(c);
System.out.println(16 + name.getBytes(StandardCharsets.UTF_8).length);
System.out.println(16 + desc.getBytes(StandardCharsets.UTF_8).length);
// 一个对象估算的内存
long avg = a + b + c + 16 + name.getBytes(StandardCharsets.UTF_8).length + 16 + desc.getBytes(StandardCharsets.UTF_8).length;
System.out.println(avg);
// ArrayList 24, Object[] 16 共 40
System.out.println((1_000_000 * avg + 40) / 1024 / 1024 + "Mb");
}
static public class Product {
private int id;
private String name;
private int price;
private String desc;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
}
演示结果
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
# WARNING: Unable to attach Serviceability Agent. sun.jvm.hotspot.memory.Universe.getNarrowOopBase()
32
24
24
144
157
381
363Mb
可以看出占用的内存很大在高并发的情况下就有可能出现内存溢出
4.3 动态生成类导致的内存溢出
代码
public class TestOomTooManyClass {
static GroovyShell shell = new GroovyShell();
public static void main(String[] args) {
AtomicInteger c = new AtomicInteger();
while (true) {
try (FileReader reader = new FileReader("script")) {
shell.evaluate(reader);
System.out.println(c.incrementAndGet());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
造成原因
GroovyShell对象无法被回收导致对象中的 GroovyClassLoader 类加载器无法被回收导致元空间的内存无法被释放直至溢出
解决办法
将静态变量的GroovyShell改为方法中的局部变量循环完一次对象不再使用就可以回收其内存然后就可以回收对象中的类加载器这样就可以回收其所占的内存
5.类加载
5.1 类加载过程的三个阶段
1.加载
- 将类的字节码载入方法区并创建类.class 对象
- 如果此类的父类没有加载先加载父类
- 加载是懒惰执行
2.链接
- 验证 – 验证类是否符合 Class 规范合法性、安全性检查
- 准备 – 为 static 变量分配空间设置默认值
- 解析 –将常量池的符号引用解析为直接引用
3.初始化
- 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值会被合并成一个 方法在初始化时被调用
- static final 修饰的基本类型变量赋值在链接阶段就已完成
- 初始化是懒惰执行
验证手段
- 使用 jps 查看进程号
- 使用 jhsdb 调试执行命令 jhsdb.exe hsdb 打开它的图形界面
- Class Browser 可以查看当前 jvm 中加载了哪些类
- 控制台的 universe 命令查看堆内存范围
- 控制台的 g1regiondetails 命令查看 region 详情
- scanoops 起始地址 结束地址 对象类型 可以根据类型查找某个区间内的对象地址
- 控制台的 inspect 地址 指令能够查看这个地址对应的对象详情
- 使用 javap 命令可以查看 class 字节码
jdk8的类加载器
5.2 双亲委派机制
所谓的双亲委派就是指优先委派上级类加载器进行加载如果上级类加载器
- 能找到这个类由上级加载加载后该类也对下级加载器可见
- 找不到这个类则下级类加载器才有资格执行加载
双亲委派的目的有两点
-
让上级类加载器中的类对下级共享反之不行即能让你的类能依赖到 jdk 提供的核心类
-
让类的加载有优先次序保证核心类优先加载
对双亲委派的误解
下面面试题的回答是错误的
错在哪了
-
自己编写类加载器就能加载一个假冒的 java.lang.System 吗? 答案是不行。
-
假设你自己的类加载器用双亲委派那么优先由启动类加载器加载真正的 java.lang.System自然不会加载假冒的
-
假设你自己的类加载器不用双亲委派那么你的类加载器加载假冒的 java.lang.System 时它需要先加载父类 java.lang.Object而你没有用委派找不到 java.lang.Object 所以加载会失败
-
以上也仅仅是假设。事实上操作你就会发现自定义类加载器加载以 java. 打头的类时会抛安全异常在 jdk9 以上版本这些特殊包名都与模块进行了绑定更连编译都过不了
6. 4种引用
6.1 强引用
1.普通变量赋值即为强引用如 A a = new A();
2.通过 GC Root 的引用链如果强引用不到该对象该对象才能被回收
6.2 软引用SoftReference
-
例如SoftReference a = new SoftReference(new A());
-
如果仅有软引用该对象时首次垃圾回收不会回收该对象如果内存仍不足再次回收时才会释放对象
-
软引用自身需要配合引用队列来释放
-
典型例子是反射数据
6.3 弱引用WeakReference
-
例如WeakReference a = new WeakReference(new A());
-
如果仅有弱引用引用该对象时只要发生垃圾回收就会释放该对象
-
弱引用自身需要配合引用队列来释放
-
典型例子是 ThreadLocalMap 中的 Entry 对象
6.4虚引用PhantomReference
-
例如 PhantomReference a = new PhantomReference(new A(), referenceQueue);
-
必须配合引用队列一起使用当虚引用所引用的对象被回收时由 Reference Handler 线程将虚引用对象入队这样就可以知道哪些对象被回收从而对它们关联的资源做进一步处理
-
典型例子是 Cleaner 释放 DirectByteBuffer 关联的直接内存
7. finalize
- 它是 Object 中的一个方法如果子类重写它垃圾回收时此方法会被调用可以在其中进行资源释放和清理工作
- 将资源释放和清理放在 finalize 方法中非常不好非常影响性能严重时甚至会引起 OOM从 Java9 开始就被标注为 @Deprecated不建议被使用了
7.1finalize 原理
-
对 finalize 方法进行处理的核心逻辑位于 java.lang.ref.Finalizer 类中它包含了名为 unfinalized 的静态变量双向链表结构Finalizer 也可被视为另一种引用对象地位与软、弱、虚相当只是不对外无法直接使用
-
当重写了 finalize 方法的对象在构造方法调用之时JVM 都会将其包装成一个 Finalizer 对象并加入 unfinalized 链表中
-
Finalizer 类中还有另一个重要的静态变量即 ReferenceQueue 引用队列刚开始它是空的。当狗对象可以被当作垃圾回收时就会把这些狗对象对应的 Finalizer 对象加入此引用队列
-
但此时 Dog 对象还没法被立刻回收因为 unfinalized -> Finalizer 这一引用链还在引用它嘛为的是【先别着急回收啊等我调完 finalize 方法再回收】
-
FinalizerThread 线程会从 ReferenceQueue 中逐一取出每个 Finalizer 对象把它们从链表断开并真正调用 finallize 方法
-
由于整个 Finalizer 对象已经从 unfinalized 链表中断开这样没谁能引用到它和狗对象所以下次 gc 时就被回收了
7.2 finalize 缺点
- 无法保证资源释放FinalizerThread 是守护线程代码很有可能没来得及执行完线程就结束了
- 无法判断是否发生错误执行 finalize 方法时会吞掉任意异常Throwable
- 内存释放不及时重写了 finalize 方法的对象在第一次被 gc 时并不能及时释放它占用的内存因为要等着 FinalizerThread 调用完 finalize把它从 unfinalized 队列移除后第二次 gc 时才能真正释放内存
- 有的文章提到【Finalizer 线程会和我们的主线程进行竞争不过由于它的优先级较低获取到的CPU时间较少因此它永远也赶不上主线程的步伐】这个显然是错误的FinalizerThread 的优先级较普通线程更高原因应该是 finalize 串行执行慢等原因综合导致