常话说:面试造火箭,上班拧螺丝,这是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官网内容
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没有任何实现
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规范-方法区定义
5.2 组成
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 位置是在堆中
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,导致两个对象都无法被释放。
1.2 可达性分析算法
- Java 虚拟机中的垃圾回收期采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
- 哪些对象可以作为 GC Root?
1.3 四种引用
1.强引用
2.软引用
3.弱引用
4.虚引用
5.终结器引用(这也算一种)
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:标记清除
- 速度较快
- 会产生内存碎片
2.2 标记整理
Mark Compact:标记整理
- 速度慢
- 没有内存碎片
2.3 复制
Copy:复制
- 不会有内存碎片
- 需要占用两倍内存空间
3.分代垃圾回收
分区
- Eden:伊甸园
- SurvivorFrom:幸存区From
- SurvivorTo:幸存区To
- Old:老年代
- 对象首先分配在伊甸区
- 新生代空间不足时,触发 minor gc,伊甸区和 from 存活的对象使用 copy 复制到 to 中, 存活的对象年龄加1并且交换 from to
- minor gc 会应发 stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
- 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
3.1 相关VM参数
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收集器串行的收集,就会导致停顿的时间长
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
新生代内存越大越好吗?
新生代理想大小:
- 新生代能容纳所有【并发量*(请求-响应)】的数据
- 幸存区大到能保留【当前活跃对象+需要晋升对象】
- 晋升阈值配置得当,让长时间存活的对象尽快晋升
设置参数:
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistrubution
5.5 老年代调优
以CMS为例
- CMS 的老年代内存越大越好
- 先尝试不做调优,如果没有 Full GC 那么已经...,否则先尝试调优新生代
- 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4~1/3
- -XX:CMSInitiatingOccupancyFraction=percent