【JVM系列8】JVM经典面试问题(内存溢出和内存泄露)解答及调
前言
JVM系列介绍到这里,其实理论知识和基本工具的使用基本上都介绍过了,当然,JVM的理论知识也不仅仅只是这些,如果想要更深入的里面还是会有很多细节值得深入了解,但是就目前来说,掌握了前面几篇文章介绍的内容,我们已经可以对JVM进行基本的调优工作了,所以本篇文章会以一些常见问题并结合实际例子来进行分析。
常见问题及调优实战
1、内存泄漏与内存溢出的区别
内存泄漏(Memory Leak):指的是对象无法得到及时的回收,导致其持续占用内存空间,造成了内存空间的浪费。 内存泄露一般是强引用才会出现问题,其他像软引用,弱引用和虚引用影响不大。
内存溢出(Out Of Memory):内存泄漏到一定的程度就会导致内存溢出,但是内存溢出也有可能是大对象导致的。
这两个区别结合下面的问题2可以更好的理解。
2、如何防止内存泄露
我们先来看下面一个简单的例子:
package com.zwx.jvm;
public class JVMTuningDemo {
public static void main(String[] args) {
{
byte[] bytes = new byte[1024 * 1024 * 64];
}
System.gc();
}
}
调用之后打开gc日志,如果不知道怎么获取gc日志的,可以点击这里。
image.png可以看到GC之后,对象并没有回收掉,从代码上来说,因为有{},所以理论上已经离开作用域了,bytes会被回收(如果不加{}是肯定不会被回收的,因为没有离开作用域),但是这里为什么还是没有被回收?
回答这个问题之前我们先对上面的代码改进一下
package com.zwx.jvm;
public class JVMTuningDemo {
public static void main(String[] args) {
{
byte[] bytes = new byte[1024 * 1024 * 64];
bytes = null;
}
System.gc();
}
}
这时候再来看,会发现已经被回收了
image.png这是因为之前虽然已经离开作用域了,但是却并没有收回引用,也就是说栈帧中的局部变量表数组中所对应的slot(局部变量表中数组的每一个位置都被称之为slot)还是有值的,并没有被切断引用,而将其置为Null就等于切断了引用,所以可以被回收。
如果看过我的并发编程系列文章中对AQS同步队列以及阻塞队列的源码分析,那么也应该可以看到,这些源码中也是大量使用了这种方式来帮助虚拟机进行gc:
image.png在有些场景这种设置为null的方式确实是一种解决方式,但是其实最优雅的方式还是以恰当的变量作用域来控制回收变量。
我们再对上面的例子进行改写:
package com.zwx.jvm;
public class JVMTuningDemo {
public static void main(String[] args) {
{
byte[] bytes = new byte[1024 * 1024 * 64];
}
int i = 0;
System.gc();
}
}
运行之后打开gc日志:
image.png我们会发现,bytes对象确实也被回收了,这又是为什么呢?
这是因为栈帧中的局部变量表内的每一个slot都是可以复用的,当bytes变量离开了其作用域之后,Java虚拟机知道这个slot已经无效了,但是虽然无效,引用却还在,所以如果没有新的变量过来占用bytes变量所在的slot,是无法将bytes回收的,而一旦有新的变量过来占用slot,自然而然bytes对象的引用就被切断了,从而被gc掉。
3、GCRoot不可达的对象一定会被回收吗
答案是不一定的。
即使在可达性分析法中被判定不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,对象依然有“逃生”的机会。
一个对象在第一次被标记为不可达对象时,并不会立刻被回收,而是会进行判断是否有必要执行finalize()方法,那么什么时候会执行finalize()方法呢?有两种情况:
-
1、Java虚拟机已经调用过当前对象的finalize()方法
-
2、finalize()方法被我们重写了
如果不满足这两种情况,那么对象就相当于是“死刑立即执行”,没有机会逃生,但是一旦满足执行finalize()方法的条件,而我们又在finalize()方法中将对象重新和引用链中的对象进行了关联,这时候对象就可以顺利“逃生”。
我们来看下面一个例子:package com.zwx.jvm;
import java.util.ArrayList; import java.util.List;
public class ObjEscapeByFinalize { public static ObjEscapeByFinalize objEscapeByFinalize = null;
public static void main(String[] args) throws InterruptedException { objEscapeByFinalize = new ObjEscapeByFinalize(); //首次自救 objEscapeByFinalize = null; System.gc(); Thread.sleep(1000);//finalize()方法优先级比较低,稍微停顿一会等一等 print(); //再次自救 objEscapeByFinalize = null; System.gc(); Thread.sleep(1000); print(); } static void print(){ if (null == objEscapeByFinalize){ System.out.println("obj has been gc"); }else{ System.out.println("obj escape success"); } } @Override protected void finalize() throws Throwable { System.out.println("come in method:finalize"); super.finalize(); objEscapeByFinalize = this; }
}
运行结果为:
come in method:finalize
obj escape success
obj has been gc
从结果可以看到,第一次自救成功,而第二次已经没有了自救机会,因为当前对象已经执行过一次finalize()方法了,而如果我们把finalize()方法中的:
objEscapeByFinalize = this;
替换为:
objEscapeByFinalize = new ObjEscapeByFinalize();
这时候就可以一直自救成功,因为每次自救之后就产生了一个新的对象,新的对象并没有执行过finalize()方法。
上面的demo还有一点需要注意的是,finalize()方法针对的是对象,假如上面的静态对象换成一个其他对象,而finalize()方法又写在当前对象,那么是无效的,例如如下例子:
package com.zwx.jvm;
import java.util.ArrayList;
import java.util.List;
public class ObjEscapeByFinalize1 {
public static List<Object> list = null;
public static void main(String[] args) throws InterruptedException {
list = new ArrayList<>();
//首次自救
list = null;
System.gc();
Thread.sleep(1000);
print();
}
static void print(){
if (null == list){
System.out.println("obj has been gc");
}else{
System.out.println("obj escape success");
}
}
@Override
protected void finalize() throws Throwable {
System.out.println("come in method:finalize");
super.finalize();
list = new ArrayList<>();
}
}
这里是无法实现自救的,因为这里要救的对象是List,而finalize()并不属于List,是属于ObjEscapeByFinalize1对象,所以这一点也是需要明确地。
不过虽然finalize()可以完成对象自救,但是由于这个方法的代价比较大而且运行时有不确定性,一般情况下还是不建议使用
4、Young GC会有STW吗
不管是什么类型的GC,都会有 stop-the-world,只是发生时间的长短,目前Java中所有的垃圾回收器均需要STW,唯一的区别只是时间的长短问题。
5、Major GC和Full GC的区别
之前我们提到了,Major GC通常会伴随着Minor GC,也就等于触发了Full GC,但是虽然如此,Major GC和Full GC并不是完全等价的,因为Full GC 的同时会对方法区(jdk1.8的metaspace,jdk1.7的永久代)进行GC,所以严格来说:Full GC=Major GC+Minor GC+方法区GC
6、方法区会发生GC吗
答案是肯定的。虽然方法区中的回收收益一般都不高,但是也是会被GC的,而方法区中被回收的最主要的就是对废弃常量和无用类的回收,判定一个废弃常量比较简单,但是判定一个类是无用类是比较困难的,那么方法区中的怎么判断一个类是无用类呢?
判断一个类是否无用,需要达到以下三个条件:
- 1、该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 2、加载该类的类加载器ClassLoader已经被回收(从这个条件可以看出,一般只有大量使用了反射,动态代理或者字节码框架等场景条件下才会满足这个条件)。
- 3、该类对应的 java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
这三个条件实际上是非常苛刻的,而即使达到以上三个条件,无用类也仅仅是可以被回收,但是是不是一定会被回收,还是取决于Java虚拟机。HotSpot虚拟机中提供了参数-Xnoclassgc来控制。
7、什么是直接内存
直接内存(Direct Memory)不属于运行时数据区,也被称之为堆外内存,通常访问直接内存的速度会优于Java堆。直接没存也有可能会发生OutOfMemoryError异常,Java 1.4中新加入的nio包下的ByteBuffer就操作了直接内存,直接内存可以通过参数-XX:MaxDirectMemorySize控制大小。
8、CMS收集器和G1收集器的区别
作为同样是并行的2款垃圾收集器,G1的目前是用来取代CMS收集器的,其主要有如下区别:
- 1、CMS收集器是老年代的收集器,需要和其他新生代收集器配合使用,而G1同时适用于新生代和老年代,不需要和其他收集器配合使用
- 2、CMS收集器以最小的停顿时间为目标的并发收集器,G1收集器是一种可预测垃圾回收的停顿时间
- 3、CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片,G1使用了 Region方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生
9、类加载机制经过哪些步骤
类加载机制主要经过了:加载(Loading),连接(Linking),初始化(Initialization),使用(Using),卸载(Unloading) 五个大阶段,而其中连接(Linking)又分为:验证(Verification),准备(Preparation),解析(Resolution)三个阶段。
10、系统CPU经常100%,如何定位
- 1、首先要确认哪个进程占用CPU高,可以使用top命令
2、找到之后可以继续执行top -Hp PID命令查询出占用最大的线程
image.png3、执行jstack命令生成线程快照信息:
jstack -l 进程PID >jstack.log
1
输出之后,我们找到上面占用CPU最高的一个线程pid=11566,将其转换为16进制,得到的结果是2d2e,然后进入生成的jstack.log文件找到这个线程可以查看线程信息。
image.png4、上面就可以定位到了线程调用的方法了,接下来就可以去分析对应的代码寻找问题了
总结
本文主要列举了一些其他比较经典的,而前面在JVM系列其他文章中又没有过多进行说明的问题,JVM学习之后需要不断实战积累调优经验,虽然还有一些理论在JVM系列中没有提及,但是我想如果可以认真把我 JVM系列至本篇为止的8篇文章相关知识和理论都掌握的话,那至少可以说已经具备了调优的理论基础了,剩下的就是不断积累经验,当然,推荐大家可以去通读一下JVM规范,毕竟所有的Java虚拟机都是按照JVM规范来实现的,或者有必要的可以自己去编译JDK来进行更深一步的研究。
请关注我,一起学习进步