深入理解JVM(一)JAVA内存区域和内存溢出异常

JAVA内存区域

图一
      上图展示了java内存划分成了哪几个区域,这些区域都有各自的用途,创建和销毁时间,有的区域随着虚拟机的启动而存在,而有的则依赖用户线程的启动和结束而创建和销毁。
      接下来我们来说说具体的区域。

程序计数器

      程序计数器是一块较小的空间,它可以看作当前线程所执行字节码的行号指示器。在虚拟机的概念模型里(实际上不同的虚拟机对此都有更高效的方式去实现),字节码解释器就是通过改变计数器来选取下一条要执行的字节码的。
  由于java虚拟机的多线程是通过线程轮流切换并分配时间片的方式实现的,所以在任意一个时刻,一个处理器只能执行一个线程。所以为了线程切换后能正常执行,每个线程应该独有自己的程序计数器。
  如果线程正在执行一个java方法,那么程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行一个Native方法,那么这个计数器为空(Undefined)。此内存区域是唯一一个在java虚拟机规范中没有规定任何OOM情况的区域。

java虚拟机栈

      java虚拟机栈也是线程私有的,生命周期和线程相同。java虚拟机栈描述的是java方法执行的内存模型,每个方法在执行时都会创建一个栈帧来保存局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从被调用到结束,对应着一个栈帧在虚拟机栈中入栈出栈的过程。
在这里插入图片描述
  局部变量表存放了编译器克制的各种基本类型(boolean,int,float,double,short,char,byte,long),对象引用(reference类型)和returnAddress类型(指向了一条字节码指令的地址)。
  其中64位长的long和double占2个局部变量空间,其他类型占一个。局部变量表的大小在编译期间完成分配,每次进入一个方法时,它需要多大的局部变量空间是完全确定的,运行期间不会改变。
  java虚拟机规范中,在此处规定了俩种异常;一种是请求的栈深入超过虚拟机允许的最大深度,此时抛出StackOverflowError异常;如果虚拟机可以动态扩展(大部分虚拟机都可以动态扩展,但是虚拟机也可以固定栈深度),如果扩展到无法申请到足够空间则抛出OOM异常。

本地方法栈

      本地方法栈和虚拟机栈发挥的作用是类似的,只不过虚拟机栈为了java方法服务,而本地方法栈为了Native方法服务。虚拟机规范中没有对本地方法使用的语言、数据结构进行强制规定,取决于虚拟机本身如何去实现。有的虚拟机将俩者合二为一(如Sun HotSpot)。本地方法栈也会抛出同样的异常。

java堆

      java堆是java虚拟机管理的内存中最大的一块,是线程共享的,在java虚拟机启动时创建。此区域唯一目的是存放对象实例,几乎所有的对象实例都是在这里分配内存的。
      java堆是垃圾收集器管理的主要区域,因此很多时候也叫GC堆。因为现在垃圾收集器基本都采用分代收集算法,所以java堆还可以细分为年轻代和老年代。年轻代可以分为Eden 区,survivor区(from,to)。从内存分配的角度来看,线程共享的java堆还可能分配多个线程私有的分配缓冲区(TLAB)。划分区域的目的是为了更好的回收内存。(有关于java垃圾收集器会在后续博客中整理)
      堆的分配是逻辑上连续的即可。java堆既可以是固定大小,也可以是动态扩展的(主流虚拟机的做法),如果没有内存完成实例分配,则出现OOM异常。

方法区

方法区也是线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。虽然虚拟机将方法区描述为堆的一个逻辑部分,但是它有一个别名叫non-heap(非堆),表示与堆的区别。

运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有关类的字段、版本、方法、接口等描述信息外,还有一项信息是常量池,用于存储编译时期产生的各种字面量和符号引用,这部分内存将在类加载后进入方法区的运行时常量池中存放。

运行时常量池相对于Class文件常量池的一个重要特征就是动态性,在运行期间也可以动态的将新的常量放入,比如String的intern()方法。因为它是方法区的一部分,所以无法申请到内存时会发生OOM异常。

猜你喜欢

转载自blog.csdn.net/machine_Heaven/article/details/103846924