Java内存溢出异常(上)

上一篇文章我们讲了JVM运行时数据区域与内存溢出异常,其中对于内存溢出异常这部分将的不够详细,这篇文章将着重讲解Java内存溢出异常的相关知识。如果有没看过上一篇文章的小伙伴们,请点击Java内存区域与内存溢出异常

Java的内存溢出异常主要分为两类:分别是内存溢出和栈溢出。在以下几种情况,会抛出内存异常:Java堆溢出、虚拟机栈和本地方法栈溢出、方法区和运行时常量池溢出、以及本机直接内存溢出,下面讲一一介绍这几类异常。

Java堆溢出

在Java内存区域与内存溢出异常中讲过,Java堆主要是用来存储对象实例的。这部分的内存区域的大小可以通过-Xms参数和-Xmx参数进行设置,通常将-Xms和-Xmx的值设置为相同的值,以减少内存扩展或者收缩时的开销。

Java堆的空间是有限的,受到物理内存与虚拟机内存的双重限制(通常虚拟机内存的会设置成小于物理内存)。因此,如果对象实例的数量不断增加,而垃圾回收机制没有进行及时清理的时候,对象实例所占用的空间就会达到Java堆的空间最大值。此时,就会因为Java堆内存不足,导致无法为新的实例分配空间,从而抛出OutOfMemoryError异常。

通过设置-Xms20m -Xmx20m运行以下代码可以模拟这一情况:

/**
 * VM Args: -Xms20m -Xmx20m
 *
 * @author bdq
 */
public class HeapOOM {
    static class OOMObject {

    }

    public static void main(String[] args) {
        List<OOMObject> objects = new ArrayList<>();
        while (true) {
            objects.add(new OOMObject());
        }
    }
}

  

运行结果:

java.lang.OutOfMemoryError: Java heap space

  

这即是常见的OOM异常,针对这类异常,往往在打印异常信息的同时会进一步提示异常原因,如上图所示的”Java heap space”。当然,只靠这点信息不足以判断到底是内存容量设置小了,还是出现了内存泄漏(关于内存泄漏的知识将会在后面的文章中进行讲述)。因此我们还要辅以其他手段来进一步确定问题的根源,比如加上-XX:+HeapDumpOnOutOfMemoryError参数使得虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照,然后用相关的工具进行分析。这类知识,本篇文章暂不作过多的讲解,将会在后面的文章一一介绍。

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

为什么要把虚拟机栈和本地方法栈的溢出放在一起讨论呢,因为在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈。对于HotSpot来说,虽然说-Xoss参数是用来设置本地方法栈大小,但实际上是无效的,栈的容量只由-Xss参数设定。

在Java虚拟机规范中对虚拟机栈和本地方法栈描述了两种异常:

  1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  2. 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

这种分类其实并不是很明确,因为内存太小或者已使用的栈空间太大都会导致栈空间无法继续分配。

StackOverflowError的出现条件很简单,下面这段简单的代码就会出现栈溢出:

public class JavaVMStackSOF {
    private int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        JavaVMStackSOF javaVMStackSOF = new JavaVMStackSOF();
        try {
            javaVMStackSOF.stackLeak();
        } catch (Throwable e) {
            System.out.println("Stack length:" + javaVMStackSOF.stackLength);
            throw e;
        }
    }
}

  

运行结果如下:

Stack length:18663
Exception in thread "main" java.lang.StackOverflowError
	at cn.bdqfork.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
	at cn.bdqfork.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
	at cn.bdqfork.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
	at cn.bdqfork.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
    ......

  

对上面的运行结果,不同的计算机Stack length的大小是不确定的,从输出的异常信息来看,是因为stackLeak方法递归调用层数过多导致的。在大多数情况下,栈深度在虚拟机默认参数下是够用的。

OutOfMemoryError异常比较难以出现,一般发生在多线程环境下。当创建一个线程时,虚拟机会分配一个私有的栈空间给相应的线程,这个空间的大小可以用-Xss参数来设置。通过不断的创建新的进程,可以产生内存溢出异常。

原因是这样的,当进程运行时,操作系统分配给进程的内存是有限的,Java堆和方法区这两部分占了大部分,忽略到程序计数器所占用的很小的一块内存,不计算虚拟机本身占用的内存,剩下的就由虚拟机栈和本地方法栈所占用。因此,创建的线程数量到达一定程度时,虚拟机栈和本地方法栈所占用的空间就会使得进程的内存空间不够用,从而抛出内存溢出异常。

这部分的测试代码如下:

public class JavaVMStackOOM {
    private void dontStop() {
        while (true) {

        }
    }

    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) {
        JavaVMStackOOM javaVMStackOOM = new JavaVMStackOOM();
        javaVMStackOOM.stackLeakByThread();
    }
}

  

这段代码的运行有一定的风险,因为Java的线程并不是完全的用户级线程,有映射到操作系统的部分,所以可能会产生系统假死的现象,请谨慎运行。

运行结果如下:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

  

由此可以看出,我们在进行多线程开发时,对于线程的数量要有一定的把握,线程池的复用是很有必要的。

受限于篇幅原因,剩余的知识点,将会在下一篇进行讲解。

原文地址:https://www.cnblogs.com/qianpangzi/p/10558040.html

时间: 2024-10-14 00:40:16

Java内存溢出异常(上)的相关文章

Java内存溢出异常(下)

此篇是上一篇文章Java内存溢出异常(上)的续篇,没有看过的同学,可以先看一下上篇.本篇文章将介绍剩余的两个溢出异常:方法区和运行时常量池溢出. 方法区和运行时常量池溢出 这部分为什么会放在一起呢?在Java内存区域与内存溢出异常这篇文章中我们说过,运行时常量池实际上属于方法区的一部分,所以就放在一起讨论. 常量池溢出 在讨论常量池的溢出之前,先说明一下String.intern()方法,该方法会检查字符串常量池中是否已经包含了一个等于此String对象的字符串,如果已经包含了,则返回代表池中这

深入理解java虚拟机系列(一):java内存区域与内存溢出异常

文章主要是阅读<深入理解java虚拟机:JVM高级特性与最佳实践>第二章:Java内存区域与内存溢出异常 的一些笔记以及概括. 好了开始.如果有什么错误或者遗漏,欢迎指出. 一.概述 先上一张图 这张图主要列出了Java虚拟机管理的内存的几个区域. 常有人把Java内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java内存区域的划分实际上远比这复杂,从上图就可以看出了.堆栈分法中所指的"栈"实际上只是虚拟机栈,或者说是虚拟机栈中的局部变量表部分.接下

深入了解Java虚拟机(1)java内存区域与内存溢出异常

java内存区域与内存溢出异常 一.运行时数据区域 1.程序计数器:线程私有,用于存储当前所执行的指令位置 2.Java虚拟机栈:线程私有,描叙Java方法执行模型:执行方法时都会创建一个栈帧,存储局部变量,基本类型变量,引用等信息 3.Java本地方法栈:线程私有,为虚拟机使用到的Native方法服务 4.Java堆:线程共享,是垃圾收集器的主要工作地方:存储对象实例等 5.方法区:线程共享:存储类信息,常量,静态变量等 运行时常量:存放编译时生成的各种字面量和符号引用 6.直接内存:机器的内

Java虚拟机结构及常见内存溢出异常

每个Java虚拟机都有一个类加载器子系统,根据某个全限定名来装入类型,同样每个Java虚拟机都有一个执行引擎,它负责执行那些包含在被装载类的方法中的指令. 当虚拟机运行一个程序时,就需要从已加载的文件中得到信息,将这些信息组织到运行时数据区,以便于管理. Java运行时的数据区域划分 1.程序计数器:程序计数器是一块较小的内存空间,可以看做是当前线程的字节码的行号指示器. Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个时刻,一个处理器只会执行一条线程中的指

Java内存区域与内存溢出异常-内存区域

Java内存区域与内存溢出异常 概述 对于 C 和 C++程序开发的开发人员来说,在内存管理领域,程序员对内存拥有绝对的使用权,但是也要主要到正确的使用和清理内存,这就要求程序员有较高的水平. 而对于 Java 程序员来说,在虚拟机的自动内存管理机制的帮助下,不再需要为每一个 new 操作去写配对的 delete/free 代码,而且不容易出现内存泄漏和内存溢出问题,看起来由虚拟机管理内存一切都很美好.不过,也正是因为 Java 程序员把内存控制的权力交给了 Java 虚拟机,一旦出现内存泄漏和

如何写出让java虚拟机发生内存溢出异常OutOfMemoryError的代码

程序小白在写代码的过程中,经常会不经意间写出发生内存溢出异常的代码.很多时候这类异常如何产生的都傻傻弄不清楚,如果能故意写出让jvm发生内存溢出的代码,有时候看来也并非一件容易的事.最近通过学习<深入理解java虚拟机-JVM高级特性与最佳实践>这本书,终于初步了解了一下java虚拟机的内存模型.本文通过写出使jvm发生内存溢出异常的代码来对自己的学习结果进行总结,同时也提醒自己以后写代码时候不要再跳进这个坑啦. java的内存管理是由java虚拟机自动进行管理的,并不需要程序员过多的手动干预

深入理解JVM读书笔记一: Java内存区域与内存溢出异常

Java虚拟机管理的内存包括几个运行时数据内存:方法区.虚拟机栈.本地方法栈.堆.程序计数器,其中方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区. 2.2 运行时数据区域 2.2.1程序计数器 程序计数器是一块较小的内存,他可以看做是当前线程所执行的行号指示器.字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码的指令,分支.循环.跳转.异常处理.线程恢复等基础功能都需要依赖这个计数器来完成.如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚

JVM高级特性与实践(一):Java内存区域 与 内存溢出异常

对于从事C.C++的开发人员而言,在内存管理领域,他们具有绝对的“权利”——拥有每个对象的控制权,并担负着每个对象生命周期的维护责任.而对于Java开发人员而言,在虚拟机自动内存管理机制的帮助下,无需为每一个创建new操作去配对 delete/free 代码,减少内存泄漏和内存溢出的问题,这些都交给了Java虚拟机去进行内存控制,但是正因如此,当出现相关问题时,若不了解JVM使用内存规则,就难以排查错误.接下来以此篇文章记录学习Java虚拟机内存各个区域概念.作用.服务对象以及可能产生的问题.

Java内存存放区域与内存溢出异常(一)

**Java内存存放区域与内存溢出异常(一)** Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有着各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖于用户进程的启动和结束而建立和销毁,java虚拟机所管理的内存将会包括以下几个运行时数据区域,如图一. 1.在这里先介绍程序计数器 程序计数器(Program Counter Register)是一块内存较小的内存空间,它的作用可以看作是当 前线程所执行的字节码的