Java内存区域

作为Java开发人员,大家都知道内存都是虚拟机自动管理的,我们不需要做任何操作,项目就能正常运行。那么问题来了,我们需要了解Java中内存是如何分配的吗?没错,虚拟机帮助我们管理内存,内存分配以及回收都不用手动操作,但是如果系统出现内存溢出或者是泄漏,我们如何定位是哪一块内存出现的问题呢。因此,了解虚拟机是如何使用内存,是我们前进的必要一步。

运行时数据区域的划分

在java程序运行过程中,java虚拟机会把它管理的内存划分为几个不同的数据区域,各个数据区域有着各自不同的作用。根据《Java虚拟机规范》的规定,主要包含以下几个数据区域:

关于这几部分数据区域,我相信大家听过最多的就是堆区和栈区了吧。在《Java虚拟机规范》中,栈区分为本地方法栈和虚拟机栈,不过大家平常使用的都是Hotspot虚拟机,在Hotspot虚拟机中本地方法栈和虚拟机栈合二为一,统称为栈区。下面我们就来看看这几个数据区域到底有什么作用。

堆(Java Heap)在虚拟机启动时进行创建,用于存放对象实例,是所有线程共享的内存区域,可以说是最大的内存区域。基本上所有的对象实例以及数组都要在堆上分配,正因为如此,Java堆也是垃圾回收管理的主要区域。针对Java堆,从不同的角度,堆还可以进行细致的划分。由于java基本上采用分代垃圾回收,堆还可以划分为:新生代和老年代;新生代还可以再划分为一个Eden区、两个Survivor区(From Survivor区、To Survivor区)等,方便更好的分配内存和回收内存。

Java堆在实现上,既可以是扩展的,也可以是固定大小。在程序启动时,我们可以通过-Xms与-Xmx进行控制,其中-Xms代表堆的初始大小,-Xmx代表堆的最大值。例如:-Xms1024m -Xmx2048m,代表堆的初始值是1G,当1G不能满足程序使用时,可以扩展到2G。当堆内存不足以满足对象分配时,将会抛出OOM异常。

方法区

线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、编译后的代码等。在《Java虚拟机规范》中,把方法区解释为堆的一个逻辑部分,但又命名为非堆,应该是为了和堆区别开来。通常我们也习惯把方法区叫做“永久代”,这是因为我们使用HotSpot虚拟机的原因,使用永久代的方式实现方法区,方便Java虚拟机像管理java堆一样管理这部分内存,但是永久代的使用受到-XX:MaxPermSize的上限限制,容易出现内存溢出问题,所以在jdk1.8,java使用元空间代替永久代,在JDK7的HotSpot中,就已经把放在永久代的字符串常量池移出。

方法区中还有一部分重要的内容,那就是运行时常量池,用于存放编译器生成的各种字面量和符号引用。另外,常量池里的常量不一定只有编译期才能产生,运行期间的常量也能进入常量池,例如:String的intern()方法。

当方法区无法满足内存分配时,就会抛出OOM异常。

虚拟机栈

线程私有,生命周期与线程相同,描述的是Java方法执行的内存模型:每个方法在运行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。通常我们说的“栈”,就是对应的该数据区域。

局部变量表,顾名思义存放的是局部变量,若是基本数据类型,则存放的是基本数据类型的值,若是引用类型则存放的是指向对象的引用。局部变量表的内存空间在编译期间进行分配,所以在方法运行期间局部变量表的大小不会改变。

在虚拟机栈中,规定了两种可能出现的异常:1.当请求栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。例如:在递归时,没有退出条件。2.当虚拟机栈内存不足,会抛出OOM异常。

本地方法栈

线程私有,该数据区域和虚拟机区域非常的相似,它们之间唯一的区别在于虚拟机栈为java方法服务,本地方法栈为Native修饰的方法服务。该区域也可能出现两种异常:StackOverflowError异常和OOM异常。

程序计数器

线程私有,内存较小,它可以看作是当前线程所执行的字节码的行号指示器。程序运行过程中,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,例如:分支、循环、跳转、异常处理、线程恢复等。

在多线程运行过程中,为了保证线程在切换后能够恢复到正确的执行位置,每条线程都有一个独立的程序计数器,独立存储,互不影响。

如果线程执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是Native方法,这个计数器的值则为空(Undefined)。此内存区域是唯一一个不会出现OOM异常的数据区域。

内存异常

在编程的生涯中,大家应该或多或少都遇见过内存溢出、内存泄漏异常吧,比如常见的内容有:Java heap space,PermGen space(该异常在jdk1.8之前还存在,1.8之后不存在,因为方法区的实现由永久代方式改为Metaspace元空间方式),unable to create new native thread,StackOverFlowError。

异常本身并不可怕,可怕的是知道出现异常,而不知道如何去解决,导致异常问题一直得不到处理,留下隐患。希望通过本文,大家能够知道如何去寻找异常出现的内存区域,以及如何去解决。

Java heap space

通过名称大家应该就知道了,当出现该异常时,说明堆空间不够用了。出现该异常,表明堆中可达对象太多,并且还在继续创建对象,达到堆的最大内存之后,就会出现该异常。上面我们已经介绍了,通过-Xms 和-Xmx可以决定堆的内存大小,现在我们就简单实现一下:

在这里,我是使用Idea操作的,其它开发工具操作类似。

代码如下:

public class HeapOOM {
    public static void main(String[] args) {
        ArrayList<Object> list = new ArrayList<>();
        while (true){
            list.add(new HeapBean());
        }
    }
}
class HeapBean {
}

我们定义堆的最大值为20m并且不可扩展,-XX:+HeapDumpOnOutOfMemoryError参数会在抛出oom异常时,会Dump出当前的内存快照以供分析。

运行如下:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid10428.hprof ...
Heap dump file created [28095284 bytes in 0.075 secs]
Disconnected from the target VM, address: '127.0.0.1:61048', transport: 'socket'
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

大家可以发现,抛出OOM异常的同时,生成了java_pid10428.hprof文件,拿到该文件,我们可以通过使用jdk/bin/jvisualvm.exe文件打开,打开步骤:文件->装入->java_pid10428.hprof,之后就会出现一个信息面板,

打开信息面板,我们可以按照这样的顺序查找问题:

1.查看概述面板,在该面板中,我们可以发现出现OOM的线程是哪一个,然后查看下面的线程信息,找到出现OOM的代码位置以及代码细节:

2.若在概述面板中看不出问题所在,我们还可以点击类栏位,其中列表展示的是当前系统类的实例数,以及占据比例、总大小等,按照实例数降序排列。从中我们可以发现HeapBean的实例数占据99%,现在我们就拿到了占据系统堆资源的类。之后我们就可以反查找,查看该类在程序中的使用情况,然后分析代码是否存在问题,继而优化代码。

3.若经过上述分析,没有发现代码存在明显的缺陷,我们还可以分析对象的生命周期是否过长,堆内存大小是否可以适当调大等等。

上述只是一个简单的思路分析,在真实项目中,出现OOM的因素可能错综复杂,就需要大家静下心来,仔细分析,相信一切都难不倒你。

StackOverFlowError

在HotSpot虚拟机中,使用-Xss设置栈大小,上述分析栈的时候,就说过在栈中有一种异常StackOverflowError。当使用栈深度超过虚拟机设定最大栈深度时,将会抛出StackOverflowError异常。大家对这个异常应该非常熟悉吧,当我们在使用递归时,若没有退出条件,让方法无限递归下去,则会导致该异常发生。

示例如下:

/**
 * VM Options:-Xss128k
 */
public class StackError {

    public static void main(String[] args) {
        StackError stackError = new StackError();
        stackError.sum(1);
    }

    public void sum(int i) {
        i++;
        sum(i);
    }
}

上面我们制定栈大小为128k,运行此程序,毫无意外会抛出StackOverflowError,这里我就不展示结果了,比较简单。在写递归函数时,必须要有退出条件。上述我们也说过,在栈内存中,还可能出现OOM异常,这是为什么呢?我们可以这样理解,每个进程的内存是有限的,如果我们定义了堆和方法区的最大容量,除去程序计数器和虚拟机本身消耗掉的内存,那么剩下的内存就是栈的了。假设我们给每个线程分配足够大的栈内存,程序中创建线程的数量就会变小,很容易出现OOM:unable to create new native thread异常。

在实际项目中,抛出StackOverflowError异常,我们可以从堆栈中找到问题,如果是创建过多线程导致的内存溢出,在不能减少线程数量的时候,我们可以减少堆和栈容量来支撑项目运行。

PermGen space

产生该异常,首先我们的第一反映是方法区出现内存溢出。上面已经谈到,方法区主要用于存储Class的相关信息,例如类名、常量池、字段描述、方法描述等。要想在方法区抛出OOM异常,我们可以在运行时产生大量的类去填充方法区。方法区的内存大小,我们可以通过-XX:PermSize和 -XX:MaxPermSize参数指定。

示例如下:

public class MethodSpaceOOM {
    public static void main(String[] args) {
        while (true){
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MethodSpaceOOM.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(o,objects));
            enhancer.create();
        }
    }
}

在这里我们使用CGLib字节码技术,不断的在运行时创建类,在jdk1.8之前,我们运行这段代码会出现PermGen space异常,这是因为生成的类越多,就需要更大的方法区来保证Class可以加载到内存。但是该问题在jdk1.8已经不存在了,这是因为方法区的实现由元空间所代替,永久带的实现方式已经被丢弃。元空间并不在虚拟机中,而是使用本地内存,因此元空间的内存只受本地内存限制,类元数据放到本地内存中,另外,常量池和静态变量放到 Java 堆里。在这种架构下,类元信息就突破了原来 -XX:MaxPermSize 的限制,可以使用更多的本地内存。元空间的内存限制,我们可以通过-XX:MetaspaceSize和 -XX:MaxMetaspaceSize参数来指定,现在,我们修改上述方法的元空间大小-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m,运行代码,我们可以发现,程序抛出的是java.lang.OutOfMemoryError: Metaspace异常。

总结

上述我们谈论了java运行时的内存区域划分、作用以及各内存区域可能出现内存溢出异常。我们都知道java自带垃圾回收机制,但是并不代表不会出现内存溢出异常,所以希望大家能够在出现异常时,准确定位到异常出现的区域以及解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值