问题:
宽依赖主要有两个过程: shuffle write 和 shuffle fetch. 类似 Hadoop 的 Map 和 Reduce 阶段.shuffle write 将 ShuffleMapTask 任务产生的中间结果缓存到内存中, shuffle fetch 获得 ShuffleMapTask 缓存的中间结果进行 ShuffleReduceTask 计算,这个过程容易造成OutOfMemory.
shuffle 过程内存分配使用 ShuffleMemoryManager 类管理,会针对每个 Task 分配内存,Task 任务完成后通过 Executor 释放空间.这里可以把 Task 理解成不同 key 的数据对应一个 Task.
早期的内存分配机制使用公平分配,即不同 Task 分配的内存是一样的,但是这样容易造成内存需求过多的 Task 的 OutOfMemory, 从而造成多余的 磁盘 IO 过程,影响整体的效率.(**例:**某一个 key 下的数据明显偏多,但因为大家内存都一样,这一个 key 的数据就容易 OutOfMemory).
1.5版以后 Task 共用一个内存池,内存池的大小默认为 JVM 最大运行时内存容量的16%,分配机制如下:假如有 N 个 Task,ShuffleMemoryManager 保证每个 Task 溢出之前至少可以申请到1/2N 内存,且至多申请到1/N,N 为当前活动的 shuffle Task 数,因为N 是一直变化的,所以 manager 会一直追踪 Task 数的变化,重新计算队列中的1/N 和1/2N.但是这样仍然容易造成内存需要多的 Task 任务溢出,所以最近有很多相关的研究是针对 shuffle 过程内存优化的.
什么是OOM?
OOM,全称“Out Of Memory”,翻译成中文就是“内存用完了”,来源于java.lang.OutOfMemoryError。
官方说明:
/**
* Thrown when the Java Virtual Machine cannot allocate an object
* because it is out of memory, and no more memory could be made
* available by the garbage collector.
*
* {@code OutOfMemoryError} objects may be constructed by the virtual
* machine as if {@linkplain Throwable#Throwable(String, Throwable,
* boolean, boolean) suppression were disabled and/or the stack trace was not
* writable}.
*
* @author unascribed
* @since JDK1.0
*/
public class OutOfMemoryError extends VirtualMachineError {
}
当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。
为什么会OOM?
为什么会没有内存了呢?
1)分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。
2)应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。
内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。
在之前没有垃圾自动回收的日子里,比如C语言和C++语言,我们必须亲自负责内存的申请与释放操作,如果申请了内存,用完后又忘记了释放,比如C++中的new了但是没有delete,那么就可能造成内存泄露。偶尔的内存泄露可能不会造成问题,而大量的内存泄露可能会导致内存溢出。
而在Java语言中,由于存在了垃圾自动回收机制,所以,我们一般不用去主动释放不用的对象所占的内存,也就是理论上来说,是不会存在“内存泄露”的。但是,如果编码不当,比如,将某个对象的引用放到了全局的Map中,虽然方法结束了,但是由于垃圾回收器会根据对象的引用情况来回收内存,导致该对象不能被及时的回收。如果该种情况出现次数多了,就会导致内存溢出,比如系统中经常使用的缓存机制。Java中的内存泄露,不同于C++中的忘了delete,往往是逻辑上的原因泄露。
OOM的类型
JVM内存模型:
按照JVM规范,JAVA虚拟机在运行时会管理以下的内存区域:
- 程序计数器:当前线程执行的字节码的行号指示器,线程私有
- JAVA虚拟机栈:Java方法执行的内存模型,每个Java方法的执行对应着一个栈帧的进栈和出栈的操作。
- 本地方法栈:类似“ JAVA虚拟机栈 ”,但是为native方法的运行提供内存环境。
- JAVA堆:对象内存分配的地方,内存垃圾回收的主要区域,所有线程共享。可分为新生代,老生代。
- 方法区:用于存储已经被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Hotspot中的“永久代”。
- 运行时常量池:方法区的一部分,存储常量信息,如各种字面量、符号引用等。
- 直接内存:并不是JVM运行时数据区的一部分, 可直接访问的内存, 比如NIO会用到这部分。
按照JVM规范,除了程序计数器不会抛出OOM外,其他各个内存区域都可能会抛出OOM。**
开发常见异常
1.Java.lang.OutOfMemoryError: Java heap space
java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
Xms | 初始堆内存 | 一般为内存的1/16 |
---|---|---|
-Xmx | 最大堆内存 | 一般为内存的1/4 |
public class JavaHeapSpaceDemo {
public static void main(String[] args) {
String str = "xxxxxxxxxdddddddddddd";
while (true){
//循环创建字符串对象
str += str + new Random().nextInt(1111111111) + new Random().nextInt(222222222);
//从常量池中获取字符串,若不存在,则创建一个字符串放到常量池中
str.intern();
}
}
}
2.java.lang.StackOverflowError
不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。
**示例:**递归调用后方法特别多,将栈空间撑爆
public class StackOverFlowErrorDemo {
public static void main(String[] args) {
test();
}
private static void test() {
//递归调用
test();
}
}
补充
递归本质:程序调用自身的编程技巧叫做递归。
3 .java.lang.OutOfMemoryError: GC overhead limit exceeded
GC回收时间过长,时间过多耗费在GC中,但是回收效果不佳。
原理:
回收过长指的是超过98%的时间用来做GC,并且回收了不到2%的堆内存,
连续多次GC,都只回收了不到2%的极端情况下才会抛出异常,
如不抛出异常,GC清理后的内存也会很快再次填满,迫使GC再次执行,
就此形成恶性循环
JVM参数设置:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemory=5m
4.java.lang.OutOfMemoryError: PermGen space
java永久代溢出,即方法区溢出了
一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。过多的常量尤其是字符串也会导致方法区溢出。此种情况可以通过更改方法区的大小来解决
JVM参数设置:
-XX:PermSize=64m -XX:MaxPermSize=256m
5.java.lang.OutOfMemoryError: unable to create new native thread
高并发情况下会出现该异常,该异常与对应的平台有关
原因分析
一个应用进程创建太多的线程,超过系统承载极限。如Linux默认允许单个进程可以创建的线程数是1024个。
解决方法
降低应用程序创建线程的数量,分析应用是否真的需要创建那么多线程,将线程数降到最低
修改服务器配置,如修改Linux服务器配置,扩大Linux默认限制
示例:
public class UnableCreateNewThreadDemo {
public static void main(String[] args) {
//不断for循环创建线程
for (int i = 0; ; i++) {
new Thread(()->{
//设置Integer.MAX_VALUE 以保持线程还在运行中
try{ TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);}catch (InterruptedException e){e.printStackTrace();}
}).start();
}
}
}
6.java.lang.OutOfMemoryError: Metaspace
metaspace存放数据:(永久代 JAVA8之后被metaspace取代了)
metaspace并不在虚拟机内存中而是使用本地内存
存储信息如下:
虚拟机加载的类信息
常量池
静态变量
即时编译后的代码
JVM参数配置
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
示例:
public class MetaspaceOOMTest {
//准备一个静态类
static class OOMTest {
}
;
public static void main(String[] args) {
int i = 0;
try {
while (true) {
//循环不断创建静态类来填充metaspace的空间
i++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTest.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();
}
} catch (Exception e) {
//记录查看创建了多少个静态类OOMTest
System.out.println(i + "次后发生异常");
e.printStackTrace();
}
}
}
参考
https://blog.csdn.net/weichi7549/article/details/107897248
https://blog.csdn.net/qq_42447950/article/details/81435080
https://blog.csdn.net/qq_20397315/article/details/106094917