JVM内存区域与内存溢出异常

序言:在java应用中,内存溢出是很常见的问题.只有了解内存区域的分布和各自负责的功能,我们才能在编写代码时注意编码质量,并且在发生溢出异常的时候能够根据异常提升快速找到溢出原因。

 

1:JVM虚拟机所管理的内存包括以下几个区域,如图所示:

 1.1 Java堆内存

对于我们应用环境来说,堆内存的占比是Jvm所管理内存是最大的。该内存是被所有线程所共享的,在虚拟机启动的时候被分配。

应用程序中大部分的对象的实例和数组都会在堆上被创建(涉及JIT,对象逃逸技术)。这一块也是垃圾回收器管理的主要区域(后续会在垃圾回收机制中描述)

1.2 方法区

方法区和java堆一样,也是被线程所共享的,该区域主要存放的是虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码(这是一种规范)。不论是jdk1.6-jdk1.7中的永久代还是jdk1.8的元空间其实都是方法区这个规范的具体实现(针对的是Sun HotSpot虚拟机来说)。下面是HotSpot对于方法区变更:

  1. 常量池从方法区移到堆中(jdk1.7开始)
  2. 取消了永久代,使用元空间(Metaspace)替代(1.8开始)
  3. 那么对应的虚拟机加载的类信息(class matadata)也从1.7的永久代中转移到了元空间中
  4. 永久代中的静态变量转移到堆内存中(jdk1.8开始)
  5. 永久代参数PermSize(设置方法区初始值大小)和 MaxPermSize(设置方法区最大内存大小) 替换为元空间参数MetaspaceSize(设置元空间初始值大小)和 MaxMetaspaceSiz(设置方法区最大内存大小)(jdk1.8开始)

为何oracle的sun团队从1.8开始舍弃永久代而启用元空间。原因在于:

  1. 每一个类被加载的时候都将自己类元数据加载到永久代中,而永久代的内存空间是有大小限制的(元空间它使用的是本地内,它的内存瓶颈只会是机器本身可分配的最大内存),一旦需要记载类过多,可能会存在内存溢出的风险;
  2. 类及方法的信息难以确定其大小,所以对于永久代的大小指定比较困难,太小容易出现永久代内存溢出,太大则容易导致老年代(小了)溢出。
  3. HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理永久代中的元数据。永久代会为GC带来不必要的复杂度,并且回收效率偏低

1.3 Java虚拟机栈

虚拟机栈是被线程所私有的,它的生命周期和线程相同(创建线程同时会创建与之相对应的虚拟机栈)。虚拟机栈为Java方法(也就是字节码)的执行提供支持:在每个方法被执行的时候会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法从调用到结束,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表:是一组变量值存储空间,用于存放方法参数和方法定义的局部变量。例如编译期就可知的8大基础数据类型(boolean,byte,char,short,int,float,long,double),对象引用(对象本身存储在堆中)和returanAddress类型 (指向了一条字节码指令的地址)。

操作数栈:它是一个先入后出栈,最大深度也在编译时写入Code属性的max_stacks数据项中,里面存储的元素可以是任意Java类型,32位数据类型占栈容量为,64位数据类型所占栈容量位2。当一个方法刚执行的时候该栈位空,在方法的执行过程中会有各种字节码指令往操作数栈中写入和提取。列入在做算数运算的时候是通过操作数栈来进行的;或者在调用其它方法的时候也是通过操作数

方法出口信息(方法返回地址):当一个方法执行只有两种可以结束,第一种当执行引擎遇到任意一个方法返回的字节码指令这种为正常完成出口,根据遇到字节码指令不同确定是包含返回值。第二种当方法执行中遇到异常,并且异常在方法体中未得到异常处理,这种退出方式为异常完成出口。

1.4 本地方法栈

与虚拟机栈的功能很相似,也是线程私有的,只不过Java虚拟机栈为虚拟机执行方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

被native关键字修饰的方法一般称为本地方法,该方法的实现由非java语言实现。例如在某java应用程序出有某个需求需要调用底层硬件,而java是无法做到,这个时候我们一般使用c语言来实现底层的调用,再通过native关键字表明使该程序可以在java中使用(其实java源码中本身就存在大量调用本地方法,有兴趣可以了解下Java中Unsafe类)。

Hotspot虚拟机将本地方法栈和虚拟机方法栈合二为一。

1.5程序计数器

和虚拟机栈一样,程序计算器也是线程私有的。它本身是一快很小的内存,用于记录着当前线程所执行的字节码(方法)的行号.

java中的多线程的执行是轮流切换并分配cpu的执行时间来实现的,那么在任何一个时刻一个cpu( 如果是多核处理器指的是一个内核)只会执行任意一个线程中代码,为了保证线程切换后能够恢复到正确的执行位置,每个线程都需要一个独立的组件去记录当前线程执行的位置,这个组件就是程序计数器。

如果线程执行的是一个java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是一个Native方法那么这个计数器为空(Undefined).

1.6直接内存

直接内存并不是JVM管理的内存区域, 它实际指定的jvm运行环境中的机器内存。Java中对于此内存处理依赖于Unsafe提供的操作堆外内存的native方法。DirectByteBuffer是Java用于实现直接内存的一个重要类,通常用在通信过程中做缓冲池,如在Netty、MINA等NIO框架中应用广泛。DirectByteBuffer( DirectByteBuffer的构造函数是包级私有的,因此外部是调用不到的。可以通过ByteBuffer.allocateDirect()构建)对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现。

2:内存溢出异常

2.1 java堆内存溢出

java中对象实例的存储在堆中,如果不断的创建对象,并垃圾回收器无法回收清除这些对象时(可参考垃圾回收机制)。当堆中内存总量达到设定阀值时,就会产生内存溢出异常。如代码所示:

   public static void main(String[] args) {

        List<byte[]> datas = new ArrayList<>();

        while(true){
            datas.add(new byte[1024*1024]);
        }
}

通过参数设置堆内存大小:-Xms10m(最小堆内存数) -Xmx10m(最大堆内存数)。尽量使最大内存数与最小内存数一致避免java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。在测试项目稳定性时。可通过设置-XX:+HeapDumpOnOfMemoryError使当jvm方式内存溢出是Dump出当前内存堆转储快照帮助我们发现内存溢出点,完善代码。

2.2虚拟机栈和本地方法栈溢出

虚拟机栈和本地方法栈会产生两种异常

  1. 如果线程请求的栈深度大于虚拟机所容许的最大深度,此时就会抛出StackOverflowError异常。(无法评估的方法递归递归)很容易产生此异常
  2. 如果虚拟机在扩展栈时无法申请到足够的内存空间时,就会抛出OutOfMemoryError异常。

测试代码:

1:对于StackOverflowError异常非常容易产生,进行无终止的递归方法调用就可以产生

    //方法递归
    public static void test(){
        test();
    }

    public static void main(String[] args) {
        test();
    }

2:对于虚拟机栈产生OutOfMemoryError异常

/**
 * 测试虚拟机栈尝试OOM异常
 * 此代码会导致机器死机 在使用之前先保存当前工作 不然易丢失数据
 * -Xss=10m 设置 Java 线程堆栈大小
 * @author  fangyuan
 */
public class JavaStackOOM {

    private void dontStop(){
        while (true){

        }
    }

    public void test(){

        //不断创建线程 每创建一个线程都会创建与之对应的虚拟机栈
        while(true){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) {
        new JavaStackOOM().test();
    }
}

运行结果:Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread;

因为Hotspot虚拟机将虚拟机栈和本地方法栈合并在一起,-Xoss参数(设置本地方法栈大小)已无用。对于栈容量只能通过-Xss参数设置。

2.3方法区内存溢出

测试代码:

/**
 * 通过cglib动态产生大量的类 导致方法区内存溢出
 *-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m jdk1.8
 *-XX:PermSize=10m -XX:MaxPermSize=10m jdk1.7
 * @author fangyuan
 */
public class MethodMemoryErrorTest {

    public static void main(String[] args) {

        while (true) {

            Enhancer enhancer = new Enhancer();

            enhancer.setSuperclass(Test.class);

            enhancer.setUseCache(false);

            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }

    }

    /**
     * 测试类
     */
    static class Test {}
}

2.3.1: 对于jdk1.7之前是永久代内存溢出:

 

2.3.2:对于jdk1.8之后是元空间内存溢出:

2.4直接内存溢出

我们可以通过-XX:MaxDirectMemorySize指定。如果不指定默和Java堆最大值(-Xmx指定)一样。

测试代码:

/**
 * 这里通过调用Unsafe方法直接操作直接内存
 * -XX:MaxDirectMemorySize=20m 设置直接内存使用最大数为50m 快速产生效果
 * @author fangyuan
 */
public class NativeMemoryOOM {

    private static final int _1MB = 1024 *1024;

    public static void main(String[] args) throws Exception {

        Field field =Unsafe.class.getDeclaredFields()[0];

        field.setAccessible(true);

        Unsafe unsafe =  (Unsafe)field.get(null);

        while(true){
            unsafe.allocateMemory(_1MB);
        }

    }
}

借鉴:周志明老师的<深入理解Java虚拟机>

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 1. SparkJVM内存使用及配置详情: SparkJVM内存使用主要包括堆内存和非堆内存堆内存用于存储对象实例,而非堆内存用于存储类信息、方法信息等。在Spark,可以通过以下参数来配置JVM内存使用: - spark.driver.memory:用于配置Driver进程的堆内存大小,默认为1g。 - spark.executor.memory:用于配置Executor进程的堆内存大小,默认为1g。 - spark.driver.extraJavaOptions:用于配置Driver进程的非堆内存大小和其他JVM参数。 - spark.executor.extraJavaOptions:用于配置Executor进程的非堆内存大小和其他JVM参数。 2. Spark报错与调优: 在Spark运行过程,可能会出现各种报错,如内存溢出、任务失败等。针对这些报错,可以采取以下调优措施: - 内存溢出:增加Executor进程的堆内存大小、减少每个任务的数据量、使用缓存等方式来减少内存使用。 - 任务失败:增加Executor进程的数量、减少每个任务的数据量、调整任务的并行度等方式来提高任务的执行效率。 3. Spark内存溢出OOM异常: Spark内存溢出OOM异常是指Executor进程的堆内存不足以存储当前任务所需的数据,导致任务执行失败。可以通过增加Executor进程的堆内存大小、减少每个任务的数据量、使用缓存等方式来减少内存使用,从而避免内存溢出异常的发生。 ### 回答2: SparkJVM内存使用及配置详情: Spark使用JVM来执行任务,其一个非常重要的参数是堆内存(Heap Memory)的大小。堆内存用于存储对象实例和方法调用的信息。在使用Spark时,可以通过spark.driver.memory和spark.executor.memory参数来配置JVM堆内存的大小,默认情况下,它们都是1g。需要根据具体的任务需求和集群资源情况来进行调整。如果遇到内存不足的情况,可以增加堆内存的大小,但是需要保证集群资源充足。 Spark报错与调优: 在使用Spark过程,常见的报错有内存溢出、数据倾斜、任务运行时间过长等问题。对于这些问题,可以采取一些调优策略进行处理。例如,在遇到内存溢出(Out of Memory)异常时,可以通过增加堆内存大小或者减少数据量来解决;对于数据倾斜的情况,可以考虑数据重分区或者使用一些聚合策略来优化;对于任务运行时间过长的情况,可以考虑增加Spark任务的并行度或者使用缓存机制来加速计算等。 Spark内存溢出(OOM)异常: Spark内存溢出异常通常是由于使用的内存超过了配置的阈值引起的。在配置Spark应用程序时,可以设置spark.driver.memory和spark.executor.memory参数来调整JVM堆内存的大小。如果内存不足,则需要增加内存配置或者优化代码逻辑。另外,可以通过设置spark.memory.offHeap.enabled参数来开启堆外内存,将一部分内存放到堆外,从而减少对JVM堆内存的占用。此外,还可以通过设置spark.memory.fraction参数来调整JVM堆内存的分配比例,更好地利用内存资源。如果调整参数后仍然出现内存溢出问题,还可以考虑调整Spark任务的并行度或者增加集群资源。 ### 回答3: Spark是一个基于内存的数据处理框架,能够高效地处理大规模数据集。在SparkJVM内存的使用及配置对于保证程序的稳定和性能的提升非常重要。 首先,Spark的JVM内存分为堆内存和非堆内存两部分。堆内存是用来存储对象实例的,而非堆内存则用来存储JVM本身的运行时数据。为了合理配置JVM内存,可以通过配置spark.driver.memory和spark.executor.memory参数来设置堆内存的大小。根据集群的硬件配置和任务的需求情况,可以根据具体情况来调整这两个参数的数值。 其次,在Spark运行过程,经常会遇到各种报错。常见的报错有内存溢出(OutOfMemoryError)、任务失败(TaskFail)等。当遇到内存溢出错误时,可以尝试以下几种方法来调优: 1. 增加可用内存:可以通过增加executor内存或调整任务分区大小来扩大可用内存。 2. 减少数据规模:可以通过过滤数据、采样数据或者使用压缩算法来减少数据的大小。 3. 优化代码:可以优化代码逻辑和算法,减少内存使用。 4. 调整缓存策略:可以通过手动控制缓存的数据量,及时释放不再使用的缓存。 最后,Spark的内存溢出(OOM)异常通常是由于数据量过大,超出了可用内存的限制而导致的。当出现内存溢出异常时,可以参考上述的调优方法来解决问题。 总之,合理配置JVM内存、及时处理报错、避免内存溢出异常是保证Spark程序稳定与性能的关键。希望以上回答对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值