1.前言
前两节的内容,说实在的,书上讲的比较宏观,接下来就是看看OOM,从细节上分析吧。
第一节学的运行时数据区讲过了,可能出现OOM问题的区域。程序计数器是不可能出现的,堆是高发区,栈的话如果栈帧过大并且没有设置栈深度,可能会出现。方法区的话,也可能会出现,比如运行时常量池,还有类信息的加载。以及非运行时数据区直接内存影响堆。这些都是,接下来就细化代码写一下。
2.OOM举例
2.1 java堆溢出
2.1.1 代码
VM options:-Xmx30m -Xms30m -XX:+HeapDumpOnOutOfMemoryError
最大内存30M,最小内存30M,内存不够生成Dump文件
package com.bo.jvmstudy.scondchapter;
import java.util.ArrayList;
/**
* @Auther: zeroB
* @Date: 2022/8/26 20:24
* @Description:
*/
public class HeadOOM {
static class OOMObject{
public int[] arr1 = new int[1024*1024];
}
public static void main(String[] args) {
ArrayList<OOMObject> oomObjects = new ArrayList<>();
while (true){
oomObjects.add(new OOMObject());
}
}
}
就是不停创建对象,然后导致内存溢出的一个样例。
异常报错,并导出对应hprof文件。
这个内容不是很熟悉,不过也可以看出,int类型占比比较大
int数组类型的对象较多,点进去再看看。对象很明显是OOMObject类型对象比较多。并且可以看到GC ROOT来自于main方法。
可以找到内存溢出的对象。
2.2 java栈溢出
在java虚拟机规范中,记录了两种异常,主要针对于虚拟机栈以及本地方法栈是否支持动态扩展。
在不支持动态扩展的情况下,线程请求的栈深度大于虚拟机所允许的最大深度时,报stackoverflowError异常。
在支持动态扩展的情况下,则当扩展的栈容量要大于当前虚拟机提供的内存时,报OOM异常。
这两种方式是虚拟机层面的东西,我们平时使用的hotspot虚拟机,其底层是不支持动态扩展的。也就是说,除非在创建线程的时候,申请不下足够的内存,才会报OOM。否则,其余的,都是按stackoverflowError来走。
2.2.1 减小栈空间大小
可以通过-Xss来控制栈内存大小。其实控制的是线程内存空间所占用的大小,虚拟机栈,本地方法栈,程序计数器均是其内部的一部分。
package com.bo.jvmstudy.scondchapter;
/**
* @Auther: zeroB
* @Date: 2022/8/31 17:53
* @Description: 就是无限递归,顺便控制一下栈大小 -Xss128k
*/
public class StackOverflowErrorTest {
public static void main(String[] args) {
//128k 栈深度差不多970左右,是上下浮动的
//256k 2000多,依赖与内存大小
int num = 0;
stackflow(num);
}
private static void stackflow(int num) {
num++;
System.out.println("测试栈深度"+num);
stackflow(num);
}
}
这里的异常其实都是StackOverflowError,即限制了栈的高度,达到了上限。
2.2.2 创建过多无用变量
这种测试方式,创建很多变量也是报oom,因为代码太费事无用,不写了。
2.2.3 创建过多线程
我们平时所说的线程私有,线程公有(堆,方法区)。所以线程所分配的内存区域,是独立于方法区,以及堆的。
在操作系统中,分配给一个进程的内存是有限的。线程的空间=总内存-堆内存-方法区内存。(有没有其它零碎内存也没查过,先这么理解)。-Xss是控制线程大小的,所以意味着,如果-Xss设置的过大,虽然是虚拟内存,但注定创建的线程数量也会减少。内存也越容易耗尽。
package com.bo.jvmstudy.scondchapter;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Auther: zeroB
* @Date: 2022/8/31 18:00
* @Description: 虽然在书中,这块内容放到栈内存溢出这块内容,导致oom,设置JVM参数-Xmx,-Xms看来没法控制
* -Xss 堆栈内存,其实也是分配的线程内存,默认是1M,这里的话是虚拟内存,就像程序计数器中线程私有的东西,都在这块内存的中包含
*/
public class CreateManyThreadOOM {
public static void main(String[] args) {
while(true){
new Thread(new Runnable() {
@Override
public void run() {
while(true){
}
}
}).start();
}
}
}
2.3 方法区常量池溢出
想想,方法区中存放了什么,常量,静态变量,运行时常量池(jdk1.6),类信息等等。
原先的常量池在永久代中,也就是方法区。这里不测试了。在1.8时,永久代移除,元空间上位。
2.3.1 运行时常量池溢出
运行时常量池在堆中,证明一下。String内部的intern方法,如果常量池中没有该对象,将其加入倒常量池并返回该对象,否则返回常量池中的对象。
package com.bo.jvmstudy.scondchapter;
import java.util.HashSet;
/**
* @Auther: zeroB
* @Date: 2022/8/31 18:39
* @Description: 静态方法区溢出,因为JVM静态方法区其实就是存放静态变量,常量,运行时常量池以及类信息这些,溢出的话,需要控制的是运行时常量池
* 但在JDK7时运行时常量池存放到堆中,所以在现有情况下,肯定是heap溢出了。
*/
public class ConstantPoolOOM {
public static void main(String[] args) {
//6M报异常Java heap space -Xmx6M -Xms6M
//10M报异常 GC overhead limit exceeded -Xmx10M -Xms10M
HashSet<String> set = new HashSet<>();
Integer num = 0;
while(true){
//将字段放入常量池中
set.add(String.valueOf(num++).intern());
}
//想打印JVM运行时常量池信息,没查到
}
}
6M的情况下,堆溢出,确实是在堆中。
String内部的intern方法,如果常量池中没有该对象,将其加入倒常量池并返回该对象,否则返回常量池中的对象。这句也证明一下。
package com.bo.jvmstudy.scondchapter;
/**
* @Auther: zeroB
* @Date: 2022/8/31 19:33
* @Description: 就是看一下jdk8,运行时常量池已经移动到堆的证明
*/
public class JVM6And7RunConstantPoolDiff {
public static void main(String[] args) {
//这里为true,因为运行时常量池中没有该数据,所以运行时常量池中记录了首次出现字符的引用
String s = new StringBuilder("德莱").append("联盟").toString();
System.out.println(s.intern() == s);
//false相当于重新拼接了一个字符串对象,和原先的地址肯定是不一样的,如果和s对比,那就一样了
String s1 = new StringBuilder("德莱").append("联盟").toString();
System.out.println(s1.intern() == s1);
//true
System.out.println(s1.intern() == s);
String s2 = new StringBuilder("ja").append("va").toString();
//false java在之前已经被加载到运行时常量池中
System.out.println(s2.intern() == s2);
}
}
2.3.2 方法区溢出
使用cglib代理,加载类信息,可以看到方法区溢出。因为1.8元空间,所以按元空间的参数来配置。
package com.bo.jvmstudy.scondchapter;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* @Auther: zeroB
* @Date: 2022/8/31 19:42
* @Description: 通过CGLIB代理来添加对象,这样就可以做到动态生成类信息,最终使类加载让静态方法区溢出
*/
public class CGLIBProxyStaticOOM {
public static void main(String[] args) {
//1.6情况下的场景咱不试了,-XX:MaxPermSize=10M -XX:PermSize=10M
//用元空间设置-XX:MaxMetaspaceSize=10M -XX:MetaspaceSize=10M Metaspace
while(true){
//cglib代理类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(o,args);
}
});
enhancer.create();
}
}
}
上方是一个CGLib的动态代理生成,不断往内部加载类,报Metaspace OOM了。
2.4直接内存溢出
直接内存这块不太了解,先贴着代码,以后再看。
package com.bo.jvmstudy.scondchapter;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* @Auther: zeroB
* @Date: 2022/8/31 19:53
* @Description: 直接内存溢出,这块其实不是很明白,直接内存的概念也不明白,只知道是本内存调度了堆内存,导致内存溢出?
* Unsafe类我倒知道点,底层做CAS,这些都能操作倒
*/
public class DirectMemeryOOM {
private static Integer memorySize = 1024 * 1024;
public static void main(String[] args) throws IllegalAccessException {
Field field = Unsafe.class.getDeclaredFields()[0];
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
int num = 0;
while (true) {
//直接error,无法捕获异常,直接内存还是不太明白,先记录一下
/**
* Exception in thread "main" java.lang.OutOfMemoryError
* at sun.misc.Unsafe.allocateMemory(Native Method)
* at com.bo.jvmstudy.scondchapter.DirectMemeryOOM.main(DirectMemeryOOM.java:24)
*/
unsafe.allocateMemory(Long.valueOf(memorySize.toString()));
}
}
}