Java虚拟机(JVM)面试专题 中(初级程序员P6)
Java虚拟机(JVM)面试专题 中(初级程序员P6)
四、内存溢出
- 误用线程池导致的内存溢出
- 查询数据量太大导致的内存溢出
- 动态生成类导致的内存溢出
1. 误用线程池导致的内存溢出
我们模拟一个发送短信的案例
一般来说我们要发送短信,都是调用第三方的API,所以有可能发生因网络问题导致的调用超时,或者是第三方服务“挂了”
(1)问题描述(newFixedThreadPool)
我们来看一下下面的代码
// 设置堆内存为-Xmx64m 64MB
// 模拟短信发送超时,但这时仍有大量的任务进入队列
public class TestOomThreadPool {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
LoggerUtils.get().debug("begin...");
// 不断发送短信
while (true) {
executor.submit(()->{
try {
// 模拟调第三方API
LoggerUtils.get().debug("send sms...");
// 模拟超时30s
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
运行结果
显然内存溢出了!
我们来看看 newFixedThreadPool() 的源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
根据我们上面代码的传参来说:
最大线程数、核心线程数都是2
new LinkedBlockingQueue<Runnable>():表示任务队列,也就是说,当我们这个线程池中最大线程数被占用后,其它的待处理任务会被放到这个任务队列,当任务队列容量超过内存,则发生内存溢出!!!(不会拒绝,只不断添加)
解决方案
不要使用 工具类Executors.newFixedThreadPool(2) 来创建线程池
使用 ThreadPoolExecutor 的构造方法来创建!这样子我们可以指定 LinkedBlockingQueue 的策略。
(2)问题描述(newCachedThreadPool)
// 设置堆内存为-Xmx64m 64MB
// 模拟短信发送超时,但这时仍有大量的任务进入队列
public class TestOomThreadPool {
static AtomicInteger c = new AtomicInteger();
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
while (true) {
System.ont.println(c.incrementAndGet());
executor.submit(()->{
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
运行结果
我们来看看 newCachedThreadPool() 的源码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
显然,这里和上面的 newFixedThreadPool() 不同, newFixedThreadPool() 的核心线程数默认是0,但是临时线程可以开“无数”个!
SynchronousQueue<Runnable>():表示最多只有一个任务!
临时线程数 = 最大线程数 - 核心线程数
所以,这里的错误是因为Java线程池开辟的线程数达到了系统的上限
解决方案
和上面的那个一样,就是不要用 Executors 自带的方法来设置线程池,要自己来new 一个ThreadPoolExecutor!
2. 查询数据量太大导致的内存溢出
在开发中一次性查询过多内容导致内存不足,引发内存溢出!
这里的解决方法很简单
就是一定要使用 limit 关键字!!!
3. 动态生成类导致的内存溢出
问题描述
// -XX:MaxMetaspaceSize=24m
// 模拟不断生成类, 但类无法卸载的情况
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();
}
}
}
}
运行结果
每次调用evaluate() 方法的时候,都会动态生成一个类,这个类是通过GroovyClassLoader加载器加载的!
但是只要类的类加载器存在,类存在,类的实例对象存在,那么这个类是不能被卸载的!类不能被卸载,会导致云空间的内存不能释放!!!
又因为这个GroovyShell是一个 static ,所以不会被垃圾回收,从而就导致了元空间内存不足!
解决方案
很简单,就是不要将其设置为static,将其设置为局部变量,这样子,当内存空间不足的时候,就进行类卸载,可以触发垃圾回收!
五、类加载
1. 类加载过程的三个阶段
类加载过程有三个阶段——加载、链接、初始化
(1)加载
- 将类的字节码载入方法区,并创建类.class 对象(堆中)
- 如果此类的父类没有加载,先加载父类
- 加载是懒惰执行(用到才加载)
(2)链接
可以分成3个小的步骤
- 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
- 准备 – 为 static 变量分配空间,设置默认值(注意,并不是赋值!)
- 解析 – 将常量池的符号引用解析为直接引用
(3)初始化
- 执行 静态代码块 与 非final的静态变量 的赋值(如果是final的变量,在链接阶段就会赋好值的!)
- 初始化是懒惰执行
类初始化的原理
static int a = 0x77; // 类初始化
static {
System.out.println("Student.class init");
} // 类初始化
static int b = 0x88;
static final int c = 0x99;
static final int m = Short.MAV_VALUE + 1;
static final Object n = new Object; //类初始化
看上述代码,编译器会把静态变量的赋值语句、静态代码块中的语句、用final修饰的引用类型的静态变量 全部合到一起,变成一个方法,在类初始化的时候调用!
c 和 m在链接过程就已经赋值了!
final 修饰的基本数据类型的变量
如果只是使用到final修饰的基本类型,是不会触发类加载的!
public class TestFinal {
public static void main(String[] args) throws IOException {
System.out.println(Student.c); // c 是 final static 基本类型
System.in.read();
System.out.println(Student.m); // m 是 final static 基本类型
System.in.read();
System.out.println(Student.n); // n 是 final static 引用类型
System.in.read();
}
}
上述代码中的c与m是final修饰,又是基本类型,其它类想要使用c和m的时候,根本就不会用到它们所在的类(Student);而是直接从常量池中去拿c和m的值,并复制一份到TestFinal 类中!
将常量池的符号引用变成直接引用(解析阶段)
在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替,而在解析阶段就是为了把这个符号引用转化成真正的地址的阶段。
2. jdk 8 的类加载器
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
像 String.class 就是由 Bootstrap ClassLoader 加载器加载
自定义的 Student.class 在上级加载器(Bootstrap、Extension)中没有找到,所以最后由Application加载!
3. 双亲委派机制
所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器
-
能找到这个类,由上级加载,加载后该类也对下级加载器可见(避免重复加载)
-
找不到这个类,则下级类加载器才有资格执行加载
双亲委派的目的有两点
- 让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到 jdk 提供的核心类
- 让类的加载有优先次序,保证核心类优先加载
【BUG】对双亲委派的误解
【问题一】自己编写类加载器就能加载一个假冒的 java.lang.System 吗?
答案是不行!!!
-
假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正的 java.lang.System,自然不会加载假冒的
-
假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的 java.lang.System 时,它需要先加载父类 java.lang.Object,而你没有用委派,找不到 java.lang.Object 所以加载会失败
-
以上也仅仅是假设!事实上操作你就会发现,自定义类加载器加载以 java. 打头的类时,会抛安全异常,在 jdk9 以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了