十分钟让你暴打JVM面试官

83 阅读14分钟

常话说:面试造火箭,上班拧螺丝,这是cv工程师常态了。如今的工作是真难找,要求是一年比一年高,原来会spring已经无数公司抢着要了,现在面试官开口第一句就是JVM调优有没有经验,大家气归气,八股文还得死记硬背,稍微多问一句,又讲不清了,佩恩十分钟教会你们治好面试官喜欢问JVM的病情

本文能带给大家什么?

1.暴打面试官
2.熟悉 JVM 技术栈
3.朋友面前装逼

知识点预览

  • JVM内存结构

    • 程序计数器
    • 虚拟机栈
    • 本地方法栈
    • 方法区
    • 直接内存
  • 垃圾回收

    • 如何判断一个对象可以回收
    • 垃圾回收算法
    • 分代垃圾回收
    • 垃圾回收器

一、JVM内存结构

主要分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区(jdk1.8前后有区别)

1.程序计数器

PC,Program Counter Register:程序计数器

  • 作用,记住下一条jvm指令的执行地址
  • 特点
    • 线程私有
    • 不存在内存溢出

cpu会发生上下文切换,如果没有程序计数器,等下次分到时间片jvm根本不知道从哪行代码开始执行

2.虚拟机栈

2.1定义

Java Virtual Machine Stacks:Java虚拟机栈

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

2.2面试题辨析

1.垃圾回收是否涉及栈内存

不涉及,栈内存会随着方法执行完毕而回收,无需垃圾回收处理,只回收堆内存中的无效对象

2.栈内存分配越大越好吗?

不是,系统内存是固定的,栈内存越大,那么线程数则越少,会影响系统运行,比如1G的系统内存,栈内存每个1M,那么最多可以分配1024个线程,如果每个栈2M,最多只有512个线程

如何指定栈内存大小?
JVM官网内容

image.png

3.方法内的局部变量是否线程安全?
  • 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
  • 如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

2.3占内存溢出问题

  • 栈内存过多导致栈内存溢出
    • 比如递归调用没有结束条件,会无限创建栈
  • 栈内存过大导致栈内存溢出

以下代码演示无限递归调用造成的栈内存溢出:

/**
 * 演示栈内存溢出 java.lang.StackOverflowError
 * -Xss256k 设置真内存大小
 */
public class TestSOF {
    private static int count;

    public static void main(String[] args) {
        try {
            method1();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }

    private static void method1(){
        count++;
        method1();
    }
}

2.4 线程运行诊断

案例1:cpu占用过多

定位

  • 用top定位哪个进程对cpu占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id(用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack 进程id
    • 可以根据线程id 找到有问题的线程, 进一步定位到问题代码的代码行数

ps:命令找到的线程id是十进制的,jstack找到的nid是十六进制的线程id,需要用计算器换算一下

案例2:程序运行很长时间没有结果

jstack分析可能是java-level deadlock

3.本地方法栈

Native Method Stacks

Java语言有很多限制,那些处理不了由C编写的方法,就是本地方法,本地方法栈给本地方法运行提供内存空间 像Object里的一些用native修饰的方法,就是本地方法,Java没有任何实现

image.png

4.堆

4.1 定义

heap:堆,通过new关键字创建的对象都会使用堆内存 特点

  • 它都是线程共享的,因此堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

4.2 堆内存溢出

以下是代码演示堆内存溢出:
报错信息 java.lang.OufOfMemoryError:Java heap space
设置堆最大内存:-Xmx8m

public class TestOOM {
    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<String>();
            String a = "hello";
            while (true) {
                list.add(a);
                a = a + a;
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

4.3 堆内存诊断

1.jps工具

  • 查看当前系统中有哪些java进程

2.jmap工具

  • 查看堆内存占用情况 jmap -heap 进程id

3.jconsole工具

  • 图形界面的,多功能的监测工具,可以连续监测

5.方法区

5.1 定义

JVM规范-方法区定义

image.png

5.2 组成

image.png

5.3 方法区内存溢出

以下代码可以测试方法内存溢出报错:
1.8之前会导致永久代内存溢出,使用 -XX:MaxPermSize=8m 指定永久代内存大小,报错信息为:java.lang.OutOfMemoryError: PermGen space
1.8及之后会导致元空间内存溢出,使用 -XX:MaxMetaspaceSize=8m 指定元空间大小,报错信息为:java.lang.OutOfMemoryError: Metaspace

public class TestMethodArea extends ClassLoader{ // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo01_8 test = new Demo01_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 实现接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class"+i, code, 0, code.length);
            }
        } finally {
            System.out.println(j);
        }
    }
}

以上代码被框架大量使用生成字节码,比如:spring、mybatis

5.4 运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符合地址变为真实地址

常量池中的信息,都会被加载到运行时常量池中
字符串常量池位于运行时常量池中

面试题

public static void main(String[] args),这里的args代表什么?
答:运行时常量池,可以自行debug查看

5.5 StringTable 特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
    • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

5.6 StringTable 存在位置

jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中

image.png

5.7 StringTable垃圾回收

以下代码演示 StringTable 垃圾回收,设置参数: -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc intern()方法将字符串放入常量池中

public class TestStringTable {

    public static void main(String[] args) {
        int i = 0;
        try {
            for(int j = 0; j < 260000; j++) {
                String.valueOf(j).intern();
                i++;
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }
}

5.8 StringTable性能调优

因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间

-XX:StringTableSize=桶个数(最少设置为 1009 以上)

考虑是否需要将字符串对象入池
可以通过 intern 方法减少重复入池

  • 调整 -XX:StringTableSize=桶个数
  • 考虑将字符串对象是否入池

案例: 不保真

推特保存了大量用户的地址信息,如果全部用字符串保存的话,据说需要30G,但是如果放入串池,则只用几百M

垃圾回收

1.如何判断对象可以回收
2.垃圾回收算法
3.分代垃圾回收
4.垃圾回收器
5.垃圾回收调优

1.如何判断一个对象可以回收

1.1 引用计数法

注意:JVM不使用此算法

当一个对象被引用时,就当引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。
这个引用计数法听起来不错,但是有一个弊端,如下图所示,循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。

image.png

1.2 可达性分析算法

  • Java 虚拟机中的垃圾回收期采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
  • 哪些对象可以作为 GC Root?

1.3 四种引用

1.强引用
2.软引用
3.弱引用
4.虚引用
5.终结器引用(这也算一种)

image.png

1.强引用(StrongReference)
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

2.软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
可以配合引用队列来释放软引用自身

3.弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
可以配合引用队列来释放弱引用自身

4.虚引用(PhantomReference)
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存

5.终结器引用(FinalReference)
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象。

软引用演示

VM参数:-Xmx20m -XX:+PrintGCDetails -verbose:gc

public class TestSoftReference {

    public static int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        method2();
    }

    // 设置 -Xmx20m , 演示堆内存不足,
    public static void method1() throws IOException {
        ArrayList<byte[]> list = new ArrayList<>();

        for(int i = 0; i < 5; i++) {
            list.add(new byte[_4MB]);
        }
        System.in.read();
    }

    // 演示 软引用
    public static void method2() throws IOException {
        ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
        for(int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }
        System.out.println("循环结束:" + list.size());
        for(SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }
}

上面的代码中,当软引用引用的对象被回收了,但是软引用还存在,所以,一般软引用需要搭配一个引用队列一起使用。
修改 method2 如下

// 演示 软引用 搭配引用队列
public static void method3() throws IOException {
	ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
	// 引用队列
	ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

	for(int i = 0; i < 5; i++) {
		// 关联了引用队列,当软引用所关联的 byte[] 被回收时,软引用自己会加入到 queue 中去
		SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
		System.out.println(ref.get());
		list.add(ref);
		System.out.println(list.size());
	}

	// 从队列中获取无用的 软引用对象,并移除
	Reference<? extends byte[]> poll = queue.poll();
	while(poll != null) {
		list.remove(poll);
		poll = queue.poll();
	}

	System.out.println("=====================");
	for(SoftReference<byte[]> ref : list) {
		System.out.println(ref.get());
	}
}

弱引用代码与软引用类似,把SoftReference替换成WeakReference即可

2.垃圾回收算法

2.1 标记清除

Mark Sweep:标记清除

  • 速度较快
  • 会产生内存碎片

image.png

2.2 标记整理

Mark Compact:标记整理

  • 速度慢
  • 没有内存碎片

image.png

2.3 复制

Copy:复制

  • 不会有内存碎片
  • 需要占用两倍内存空间

image.png

3.分代垃圾回收

分区

  • Eden:伊甸园
  • SurvivorFrom:幸存区From
  • SurvivorTo:幸存区To
  • Old:老年代

image.png

  • 对象首先分配在伊甸区
  • 新生代空间不足时,触发 minor gc,伊甸区和 from 存活的对象使用 copy 复制到 to 中, 存活的对象年龄加1并且交换 from to
  • minor gc 会应发 stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长

3.1 相关VM参数

image.png

3.2 GC 分析

通过下面的代码,给 list 分配内存,来观察 新生代和老年代的情况,什么时候触发 minor gc,什么时候触发 full gc 等情况,使用前需要设置 jvm 参数:
-Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc

public class TestGC {

    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
    }

}

ps:如果发生内存溢出的是子线程,不会导致其他java线程结束

4.垃圾回收器

1.串行

  • 单线程
  • 堆内存较小,适合个人电脑
  • Serial、ParNew、Serial Old

2.吞吐量优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 让单位时间内,STW 的时间最短
  • Parallel Scavenge

3.响应时间优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 尽可能让单词 STW 的时间最短
  • CMS

垃圾回收器大部分人不会接触到,大家知道有这类东西就好了,有兴趣的可以查资料

了解下G1回收器,Garbage First 适用场景:

  • 同时注重吞吐量和低延迟(响应时间)
  • 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域
  • 整体上是标记-整理算法,两个区域之间是复制算法

JDK1.8并不是默认开启的,所以需要参数开启

-XX:+UseG1GC
-XX:G1HeapRegionSize=size // 要设置为1,2,4,8的倍数
-XX:MaxGCPauseMillis=time

G1 回收垃圾阶段
G1回收会在这3个阶段循环反复,G1在老年代内存不足时(老年代所占内存超过阈值) 如果垃圾产生速度慢于垃圾回收速度, 不会触发 Full GC,还是并发地进行清理 如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 Serial Old收集器串行的收集,就会导致停顿的时间长

image.png

5.垃圾回收调优

查看虚拟机参数命令

D:\JavaJDK1.8\bin\java  -XX:+PrintFlagsFinal -version | findstr "GC"

5.1 调优领域

  • 内存
  • 锁竞争
  • cpu 占用
  • io

5.2 确定目标

  • 【低延迟】还是【高吞吐量】,选择合适的回收器
  • CMS,G1, ZGC
  • ParrallelGC 【高吞吐量唯一选择】
  • Zing

5.3 最快的 GC 是不发生 GC

当然这是理想的情况 首先排除减少因为自身编写的代码而引发的内存问题

查看 Full GC 前后的内存占用,考虑以下几个问题:

  • 数据是不是太多?
    • resultSet = statement.executeQuery(“select * from 大表 limit n”)
  • 数据表示是否太臃肿
    • 对象图
    • 对象大小 16 Integer 24 int 4
  • 是否存在内存泄漏
    • static Map map ...
    • 软引用
    • 弱引用
    • 第三方缓存实现

5.4 新生代调优

新生代的特点

  • 所有的 new 操作的内存分配非常廉价
    • TLAB thread-local allocation buffer
  • 死亡对象的回收代价是零
  • 大部分对象用过即死
  • Minor GC 的时间远远低于 Full GC

新生代内存越大越好吗? image.png

新生代理想大小:

  • 新生代能容纳所有【并发量*(请求-响应)】的数据
  • 幸存区大到能保留【当前活跃对象+需要晋升对象】
  • 晋升阈值配置得当,让长时间存活的对象尽快晋升

设置参数:

-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistrubution

5.5 老年代调优

以CMS为例

  • CMS 的老年代内存越大越好
  • 先尝试不做调优,如果没有 Full GC 那么已经...,否则先尝试调优新生代
  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4~1/3
    • -XX:CMSInitiatingOccupancyFraction=percent