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

Java虚拟机将内存划分为 堆、方法区、虚拟机栈、本地方法栈、程序计数器五个区域,如下图所示


线程 私有内存:虚拟机栈,本地方法栈,程序计数器,生命周期依赖于线程的启动和结束
线程 共有内存:堆,方法区,所以这里存在线程安全问题

内存区域

1、程序计数器

当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变计数器的值来获取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖于计数器来完成。
之所以计数器是线程私有的内存,是因为多线程是通过轮流切换并分配处理器执行时间的方式实现的,也就是说一个处理器在某一时刻只能执行一个线程中的指令,在其他线程获取处理器执行时间的时候,为了能恢复到之前暂停的执行位置,就需要程序计数器是线程私有的,从而使得各线程之间计数器互不影响。
  • 如果是Java方法,计数器中记录正在执行的虚拟机字节码指令地址;
  • 如果是native方法,则为空,即不记录任何值。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemroyError(OOM)情况的区域。

2、虚拟机栈

Java方法的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了
  • 编译器可知的各种基本数据类型,boolean、byte、char、short、int、long、float、double,其中long和double是64未,所以会占用两个局部变量空间(slot),其余数据类型占用一个。
  • 对象引用,reference类型,不等同对象本身,可能是指向对象起始地址的指针,也可能是一个代表对象的句柄或其他于此对象相关的位置;
  • returnAddress类型,指向一条字节码指令的地址。
局部变量表所需的内存空间在编译器完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是已经完全确定的, 在方法运行期间不会改变局部变量表的大小

3、本地方法栈

其作用与虚拟机栈类似,本地方法栈服务的对象是虚拟机使用到的Native方法。在Sun HotSpot虚拟机中,将其与虚拟机栈合二为一。

4、Java堆

Java虚拟机所管理的内存中最大的一块, 随着虚拟机的启动而创建。几乎所有的对象实例在这里分配内存。这里是垃圾收集器管理的主要区域。故也被成为“GC堆”。
目前的垃圾收集器都采用分代收集算法,所以Java堆可以细分为:新生代和老年代。
如果进一步细分,则可以将新生代分为:Eden空间,From Survivor空间,To Survivor空间。
从内存分配的角度来看,线程共享的Java堆种可能换分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,即TLAB)。
Java堆仅是逻辑上的连续,在物理上可以处于不连续内存空间中。

5、方法区(Non-Heap 非堆)

存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虚拟机规范将其描述为堆的一个逻辑部分。
在HotSpot虚拟机中,方法区也被称为“永久代”,但是两者并不等价,HotSpot虚拟机设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样垃圾收集器可以像管理Java堆一样管理这部分内存,省去了再为方法区专门编写内存管理代码的工作。但是在其他虚拟机中并不存在永久代的概念。但是 在JDK1.7的HotSpot中已经将原本放在永久代的字符串常量池移出
根据 http://openjdk.java.net/jeps/122 中的描述来看,进行了如下变化
  1. 移除了永久代(PermGen),替换为元空间(Metaspace)
  2. 永久代中的class metadata转移到native memory(本地内存,非虚拟机)
  3. 永久代中的interned strings 和class static variables 转移到了 Java堆中
  4. 永久代参数 PermSize MaxPermSize 改为 元空间参数 MetaspaceSize MaxMetaspaceSize

5.1 运行时常量池

方法区中的一部分,用于存放Class文件中编译器生成的各种字面量和符号引用以及翻译出来的直接引用,类加载后进入方法区中。
相比于Class文件常量池的另一个重要特征是 具备动态性,Java语言并不要求常量一定只有编译器才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也可以将新的常量放入,String.intern()方法就体现了这种特性。

6、直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,其内存分配并不会受到Java堆大小的限制,但是会受到本机总内存大小以及处理器寻址空间的限制。
NIO引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免在Java堆和Native堆中来回复制数据。

常见异常

1、OutOfMemroyError(OOM)

  • 虚拟机栈和本地方法栈在动态扩展时,如果无法申请到足够的内存,将会抛出OOM
  • Java堆中没有内存来分配实例并且堆也无法再扩容时,将会抛出OOM
  • 方法区无法满足内存分配需求时,将抛出OOM,方法区中的运行时常量池无法申请到内存时,会抛出OOM
  • 配置虚拟机参数,忽略直接内存,总内存区域总和大于物理内存限制,动态扩展时会出现OOM

2、StackOverflowError

  • 虚拟机栈和本地方法栈,如果线程请求的栈深度大于虚拟机所允许的深度时,将会抛出StackOverflowError

摘取自《深入理解Java虚拟机》,作者周志明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值