关于JVM:java内存区域与内存溢出异常

41次阅读

共计 4029 个字符,预计需要花费 11 分钟才能阅读完成。

(一)Java 虚拟机运行时数据区
Java 虚拟机运行时数据区 2.jpg
程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,它能够看作是以后线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过扭转这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异样解决、线程复原等根底性能都须要依赖这个计数器来实现。因为 Java 虚拟机的多线程是通过线程轮流切换并调配处理器执行工夫的形式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因而,为了线程切换后能复原到正确的执行地位,每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程公有”的内存。如果线程正在执行的是一个 Java 办法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 办法,这个计数器值则为空(Undefined)。此内存区域是惟一一个在 Java 虚拟机标准中没有规定任何 OutOfMemoryError 状况的区域。

Java 虚拟机栈(Java Virtual Machine Stacks)

Java 虚拟机栈也是线程公有的,它的生命周期与线程雷同。虚拟机栈形容的是 Java 办法执行的内存模型:每个办法在执行的同时都会创立一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动静链接、办法进口等信息。每一个办法从调用直至执行实现的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。局部变量表寄存了编译期可知的各种根本数据类型(boolean、byte、char、short、int、float、long、double)、对象援用(reference 类型,它不等同于对象自身,可能是一个指向对象起始地址的援用指针,也可能是指向一个代表对象的句柄或其余与此对象相干的地位)和 returnAddress 类型(指向了一条字节码指令的地址)。在 Java 虚拟机标准中,对这个区域规定了两种异样情况:如果线程申请的栈深度大于虚拟机所容许的深度,将抛出 StackOverflowError 异样;如果虚拟机栈能够动静扩大(以后大部分的 Java 虚拟机都可动静扩大,只不过 Java 虚拟机标准中也容许固定长度的虚拟机栈),如果扩大时无奈申请到足够的内存,就会抛出 OutOfMemoryError 异样。

本地办法栈(Native Method Stack)

与虚拟机栈所施展的作用是十分类似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 办法(也就是字节码)服务,而本地办法栈则为虚拟机应用到的 Native 办法服务。与虚拟机栈一样,本地办法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异样。

Java 堆(Java Heap)

Java 堆是 Java 虚拟机所治理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创立。此内存区域的惟一目标就是寄存对象实例,简直所有的对象实例都在这里分配内存。Java 堆是垃圾收集器治理的次要区域,因而很多时候也被称做“GC 堆”(Garbage Collected Heap,幸好国内没翻译成“垃圾堆”)。从内存回收的角度来看,因为当初收集器根本都采纳分代收集算法,所以 Java 堆中还能够细分为:新生代和老年代;再粗疏一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存调配的角度来看,线程共享的 Java 堆中可能划分出多个线程公有的调配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与寄存内容无关,无论哪个区域,存储的都依然是对象实例,进一步划分的目标是为了更好地回收内存,或者更快地分配内存。依据 Java 虚拟机标准的规定,Java 堆能够处于物理上不间断的内存空间中,只有逻辑上是间断的即可,就像咱们的磁盘空间一样。在实现时,既能够实现成固定大小的,也能够是可扩大的,不过以后支流的虚拟机都是依照可扩大来实现的(通过 -Xmx 和 -Xms 管制)。如果在堆中没有内存实现实例调配,并且堆也无奈再扩大时,将会抛出 OutOfMemoryError 异样。

办法区(Method Area)

办法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(l 类名、拜访修饰符)、常量、动态变量、即时编译器编译后的代码等数据。尽管 Java 虚拟机标准把办法区形容为堆的一个逻辑局部,然而它却有一个别名叫做 Non-Heap(非堆),目标应该是与 Java 堆辨别开来【有时被叫做永恒代】。依据 Java 虚拟机标准的规定,当办法区无奈满足内存调配需要时,将抛出 OutOfMemoryError 异样。

运行时常量池(Runtime Constant Pool)

运行时常量池(Runtime Constant Pool)是办法区的一部分。Class 文件中除了有类的版本、字段、办法、接口等形容信息外,还有一项信息是常量池(Constant Pool Table),用于寄存编译期生成的各种字面量和符号援用,这部分内容将在类加载后进入办法区的运行时常量池中寄存。

间接内存(Direct Memory)

间接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机标准中定义的内存区域。然而这部分内存也被频繁地应用,而且也可能导致 OutOfMemoryError 异样呈现。在 JDK 1.4 中新退出了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I / O 形式,它能够应用 Native 函数库间接调配堆外内存,而后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的援用进行操作。这样能在一些场景中显著进步性能,因为防止了在 Java 堆和 Native 堆中来回复制数据。显然,本机间接内存的调配不会受到 Java 堆大小的限度,然而,既然是内存,必定还是会受到本机总内存(包含 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限度。服务器管理员在配置虚拟机参数时,会依据理论内存设置 -Xmx 等参数信息,但常常疏忽间接内存,使得各个内存区域总和大于物理内存限度(包含物理的和操作系统级的限度),从而导致动静扩大时呈现 OutOfMemoryError 异样。

(二)对象的创立

内存调配:对象所需内存的大小在类加载实现后便可齐全确定,为对象调配空间的工作等同于把一块确定大小的内存从 Java 堆中划分进去(指针碰撞、闲暇列表)。分配内存时须要思考线程安全性,就批改指针所指向的地位而言,通常有两种解决方案:1 对象所需内存的大小在类加载实现后便可齐全确定(如何确定将在 2.3.2 节中介绍),为对象调配空间的工作等同于把一块确定大小的内存从 Java 堆中划分进去。2 是把内存调配的动作依照线程划分在不同的空间之中进行,即每个线程在 Java 堆中事后调配一小块内存,称为本地线程调配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上调配,只有 TLAB 用完并调配新的 TLAB 时,才须要同步锁定。内存空间初始化(不包含对象头):内存调配实现后,虚拟机须要将调配到的内存空间都初始化为零值。丰盛对象头等信息:虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何能力找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。对象初始化:执行 new 指令之后会接着执行 <init> 办法,把对象依照程序员的志愿进行初始化,这样一个真正可用的对象才算齐全产生进去。

(三)对象的内存布局:

在 HotSpot 虚拟机中,对象在内存中存储的布局能够分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。HotSpot 虚拟机的对象头包含两局部信息,第一局部用于存储对象本身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标记、线程持有的锁、偏差线程 ID、偏差工夫戳等,这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中别离为 32bit 和 64bit,官网称它为“Mark Word”。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要通过对象自身。实例数据局部是对象真正存储的无效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都须要记录起来。对齐填充并不是必然存在的,也没有特地的含意,它仅仅起着占位符的作用。对齐填充并不是必然存在的,也没有特地的含意,它仅仅起着占位符的作用。

(四)对象的拜访定位

建设对象是为了应用对象,咱们的 Java 程序须要通过栈上的 reference 数据来操作堆上的具体对象。因为 reference 类型在 Java 虚拟机标准中只规定了一个指向对象的援用,并没有定义这个援用应该通过何种形式去定位、拜访堆中的对象的具体位置,所以对象拜访形式也是取决于虚拟机实现而定的。目前支流的拜访形式有应用句柄和间接指针两种。通过句柄拜访:如果应用句柄拜访的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中蕴含了对象实例数据与类型数据各自的具体地址信息。

对象拜访方(句柄)式.jpg

间接指针拜访:需思考如何搁置拜访类型数据的相干信息,Java 堆对象的布局中 reference 中存储的间接就是对象地址。

对象拜访形式(指針).jpg

正文完
 0