1.JVM内存模型
1.1 Java对象布局
对齐填充:为了保证对象的大小为8字节的整数倍,自动对齐,避免多次加载
1.2 Heap堆内存分布
- 首先先使用
jdk/bin/jvisualvm.exe
的工具,也可cmd窗口直接输jvisualvm
。打开以后在工具-插件安装Visual GC
,监控一下当前已启动的java服务
从图中可以分为Metaspace元数据(非堆)、Old区老年代、Eden(Survivor0、Survivor1)区新生代也简称Young区
1.2.1 Old区老年代
- 每经过一次GC回收,对象年龄+1,大于15次没有被回收掉的则加入老年代中
- 对象内存大小大于新生代,则直接加入到老年代中
1.2.2 Young区新生代
因为新生代绝大多数对象生命周期比较短,经过回收会导致Young区空间不连续,造成空间碎片的问题。
当给需要多个内存格的对象进行分配时无法分配,则会造成GC回收导致和CPU抢时间片。
于是将Young区在分成Eden区和Survivor区
- Eden区只存放新生的对象
- 只要经过回收的就放入S区,Eden区就能够相对连续了,但是再次回收后的S区也会出现空间碎片问题,还是会导致对象放不下
- 于是区分了S0(Survivor From)、S1(Survivor To),每次回收都转移到下一区域,永远保证S0或者S1有一个为空,由此解决碎片问题
- Eden区和S0、S1的比例是8:1:1
- 万一新生成的对象比较大,S0、S1内存不够了,需要向老年区借用内存,这称为担保机制
始终会保证S0或者S1有一个为空
2.JVM 垃圾回收
2.1 Young GC
包含了Eden、S区,最小的GC有称为Minor GC
2.2 Old GC
Major GC,通常会伴随着Minor GC,相当于Full GC
2.3 Full GC
Young GC+ Old GC (+MateSpace GC)
会导致 stop the work , 要尽可能减少Full GC的频率
允许一定范围的Young GC
2.4 模拟Heap区 OOM
- 首先准备代码
List<User> str = new ArrayList<>();
@GetMapping("/while")
public void while1() {
while (true){
str.add(new User());
}
}
- 设置JVM堆内存大小
-Xms40M -Xmx40M
- 堆内存分布和日志截图
上图垃圾回收过程: - 新生成一个对象进入Eden区,发现Eden区内存不足,则触发Young GC
- Young GC首先对S1进行回收,未被回收的分代年龄+1,转移到S0或者Old区(满足分代年龄的)
- 然后再对Eden区进行回收,未被回收的转到S0,从而给新对象预留空间
- 经过回收后,Eden区或者S0或者Old区空间不够均会导致OOM
2.5 模拟Metaspace区 OOM
Java8使用Metaspace来替代永久代。Metaspace是方法区在Hotspot中的实现,并不在虚拟机内存中而是直接使用本地内存。
在Java8中,Class、Metadata被存储在Metaspace元数据中,永久代(Java8后被Metaspace取代)存放了一下信息:
1.虚拟机加载的类信息
2.常量池
3.静态变量
4.即时编译后的代码
如要模拟Metaspace空间溢出,可以不断的生成代理类往元空间加,那么久会触发Metaspace OOM
- java 代码
public class MetaspaceTest {
static class User {
String name;
}
public static void main(String[] args) {
int i = 0;
try {
while (true) {
i++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(o, args));
enhancer.create();
}
} catch (Throwable e) {
System.out.println("i=" + i);
e.printStackTrace();
}
}
}
- jvisualvm工具监控和日志
至于Metaspace为什么没有填满,因为在Java8中Metaspace并不在虚拟机内存中而是直接使用本地内存。
3.JVM垃圾回收器
3.1 怎么确定是垃圾对象
- 引用计数,可能会存在循环引用问题而导致两个对象都不能被回收
- 可达性分析,确定一个GC Root,由它出发,检查某个对象是否可达,不可达的则为垃圾对象
3.2 GC Root
- 虚拟机栈中的局部变量表( 栈->来源于方法区正在执行的变量)
- 方法区的常量或者static变量,本地方法栈中的变量
- 类加载器
- Thread
3.3 回收算法
1.标记清除算法(存活对象、标记对象、未被使用空间)
- 扫描过程中确定为垃圾对象,则打上标记,下次扫描才被回收
- 标记和清除都扫描整个堆内存空间,比较耗时,清除后导致空间碎片不连续
2.标记复制算法
- 划分出一块区域,在标记后的同时将存活的对象复制到新的区域中,然后直接清空整个标记区域,等同于s0/1
- 虽说达到空间连续了,但也比较浪费空间
2.标记整理算法
- 在标记后进行清除的过程中移动存活的对象
3.4 垃圾收集器
对上述算法的应用,将不同的回收算法适用于不同的代
新生代:标记复制算法,老年代:标记清除、整理算法
3.4.1 Serial GC
-XX:+UseSerialGC
- 单线程垃圾收集器,比较适用单核CPU,内存空间比较少
- 适用新老年代
- 缺点:会暂停业务代码线程,因为堆中的内存地址会变动,不能让线程使用到已经不存在的对象
3.4.2 Parallerl GC
-XX:+UseParallerlGC
- 多线程,但会使应用程序暂停,适用新老年代
3.4.3 Concurrent Mark Sweep(CMS)
-XX:+UseConcMarkSweepGC
- 使用的是标记清除算法,存在空间碎片问题,只适用于老年代
- 应发类的垃圾收集器,用户线程和垃圾回收线程同时进行,低停顿时间
3.4.4 G1 Garbage Collector
-XX:+UseG1GC
- 尽可能满足用户设置的停顿时间目标,很好的解决了空间碎片问题,吞吐量高
- 用的标记整理算法,适用新老年代
- 在CMS回收器的基础上再加了一个最终标记,然后再跟据回收时间随机回收
如果倾向于G1,尽量满足以下几点
- 堆内存使用率超过了50%
- 竞争比较高
- 希望停顿时间低的
3.4.5 Z GC
-XX:+UseZGC
- 非常小的停顿时间,但是会牺牲一些吞吐量
4. JVM调优纬度
基于业务来确定使用最合理的垃圾回收期,主要有以下几个纬度
- 串行:只有一个垃圾收集器,Serial
- 并行:多个垃圾收集器,业务代码线程会停顿 Parallerl GC,比较关注吞吐量
- 并发:多个垃圾收集器,业务代码共同运行,CMS、G1、ZGC,比较关注停顿时间
4.1 停顿时间
服务接口的响应时间,垃圾收集器进行垃圾回收和Client执行响应的时间
4.2 吞吐量
时间段内完成的请求数或者更新数,运行用户代码时间 /运行用户代码和垃圾收集总时间
5.垃圾回收器搭配图
当你指定了某个区使用某个垃圾收集器时,其他区域会采用配套的垃圾收集器
以上就是本章的全部内容了。
上一篇:JVM第一话 – JVM入门详解以及运行时数据区分析
下一篇:JVM第三话 – JVM性能调优实战和高频面试题记录
书山有路勤为径,学海无涯苦作舟