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

一、概述:

对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个new 的对象去写delete/free操作。不容易出现内存溢出或者内存泄漏的问题,正因为如此,一旦出现相关事件,我们应该懂得如何去排查问题所在。

二、运行时的数据区域

JVM在执行Java程序的过程中会把它所管理的内存划分为若干个区域。有的区域是随着jvm的启动而存在,有的区域随着线程的创建而建立。包括了以下几个组成部分:
2.1、程序计数器
程序计数器(Program Counter Register)是一块很小的内存空间,是当前线程所执行的字节码的行号指示器。
Java虚拟机的多线程是通过线程的轮流切换并分配处理器执行时间的方式在实现的。因此为了使得线程切换后能恢复到之前的执行位置,每条线程都需要一个独立的程序计数器,各条线程互不影响。所以我们称这类内存区域为“线程私有”的内存。如果线程正在执行的是Native方法,则计数器值则为空(Undefined)。此内存区域也是唯一一个没有在JVM规范中规定任何OOM(OutOfMemoryError)的内存。

2.2、Java虚拟机栈
首先和程序计数器一样,Java虚拟机栈也是线程私有的,所以它的生命周期和线程相同。虚拟机栈它所描述的是Java方法执行的内存模型;每个方法在执行的时候会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法执行完成的过程就是栈帧在虚拟机栈中入栈、出栈的过程。
局部变量表存放了编译期可知的数据类型(boolean,byte,char,short,int,float,long,double),其中64位长度的long和double类型的数据占用了两个局部变量空间,其余的只占用了一个。局部变量表所需要的内存空间在编译期间完成分配的,在方法运行的时候不会修改局部变量的大小。
在Java虚拟机栈内存区域中,JVM规范定义了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowerError异常;虚拟机栈通过动态扩展,还是没有申请到足够的内存,将会抛出OutOfMemoryError异常。

2.3、本地方法栈
与Java虚拟栈不同的是,本地方法栈使用到的是Native方法,其余的和虚拟机栈类似。

2.4、Java堆
Java堆通常是由JVM所管理的最大一块内存区域,也是一块被所有的线程所共享的内存区域,在JVM启动的时候创建。该内存区域存放的就是对象实例以及数组,而且是垃圾收集器管理的主要区域。由于现代的收集器基本采用的都是分代收集算法,所以Java堆可以细分为:新生代、老生代,再细致一点可以分为Eden空间,From Survivor空间,To Survivor空间。进一步的划分是为了更好的回收内存,或者更好的分配空间。
根据JVM规范的规定,Java堆上的的内存在物理上可以不连续,只要是在逻辑上是连续的即可。在实现上,既可以固定大小,又可以动态扩展(-Xmx和-Xms)。堆中如果没有内存在进行实例分配,将会抛出OutOfMemoryError异常。

2.5、方法区
和Java堆一样,也是一个线程共享的内存区域。方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。也被称之为“永久代”,通过-XX:MaxPermSize设置其上限。方法区中如果无法满足内存分配的需求,将会抛出OutOfMemoryError异常。GC主要回收的是常量池和类型的卸载。

2.6、运行时常量池
运行时常量池是方法区的一部分,用于存在类在编译期生成的各种字面量和符号引用。

2.7、直接内存
并不是虚拟机运行时数据区域的一部分,但是也会导致OutOfMemoryError异常。在JDK1.4中加入的NIO,引入了通道和缓存区的I/O方式,它可以通过Native函数直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,显著的提高了性能。

三:JVM中对象的操作

对象的创建
在这里,Java对象的创建只考虑new关键字。那么在虚拟机遇到new的指令时,首先将去检查这个指令的参数是否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析、和初始化,如果没有,则先执行相应的类加载过程。类加载后虚拟机将为新生对象分配内存。Java堆内存是否规整是由采用的垃圾收集器是否带有压缩整理功能所决定的。因此在Serial、ParNew等带有Compact过程的收集器时,系统采用的算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器,采用的是空闲列表。
解决内存分配并发的方案:对内存分配的动作进行同步处理,另一种把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称之为本地线程分配缓冲(Thread Local Allocation Buffer TLAB),哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完了,分配新的TLAB时,才需要锁定同步。虚拟机是否使用TLAB可以通过-XX:+/-UseTLAB参数来设定。

对象的内存布局
在HotSpot虚拟机中,对象在内存中的存储布局分为3块区域:对象头、实例数据、对齐填充。
对象头包括了两部分信息,第一部分时用于存储对象自身的运行时数据,如哈希码、GC分代、
锁标志、偏向时间戳等。另一部分时类型指针。即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象时哪个类的实例。

对象的访问定位
我们的Java程序需要通过在栈中的reference(一个指向对象的引用)数据来操作堆上的数据。主流的访问方式有:句柄(优点:不会随着对象移动而改变,稳定)和直接指针(速度更快)。

未完,待续。。。

发布了99 篇原创文章 · 获赞 230 · 访问量 32万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章