深入理解java虚拟机系列(一):java内存区域与内存溢出异常

时间:2021-07-16 20:43:43

文章主要是阅读《深入理解java虚拟机:JVM高级特性与最佳实践》第二章:Java内存区域与内存溢出异常

的一些笔记以及概括。

好了開始。假设有什么错误或者遗漏,欢迎指出。

一、概述

先上一张图

深入理解java虚拟机系列(一):java内存区域与内存溢出异常

这张图主要列出了Java虚拟机管理的内存的几个区域。

常有人把Java内存区分为堆内存(Heap)和栈内存(Stack)。这样的分法比較粗糙,Java内存区域的划分实际上远比这复杂。从上图就能够看出了。

堆栈分法中所指的“栈”实际上仅仅是虚拟机栈,或者说是虚拟机栈中的局部变量表部分。

接下来主要解说区域的各个部分.

二、执行时数据区域

1.程序计数器

  • .程序计数器(Program Counter Register)是一块较小的内存空间,它的作用能够看做是当前线程所运行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型。各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条须要运行的字节码指令
  • Java虚拟机的多线程是通过线程轮流切换并分配处理器运行时间的方式来实现的,在不论什么一个确定的时刻。一个处理器(对于多核处理器来说是一个内核)仅仅会运行一条线程中的指令。

    因此。为了线程切换后能恢复到正确的运行位置。每条线程都须要有一个独立的程序计数器。各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

  • 线程正在运行的是一个Java方法,假设正在运行的是Natvie方法。这个计数器值则为空(Undefined)。

    此内存区域是唯一一个在Java虚拟机规范中没有规定不论什么OutOfMemoryError情况的区域。

2.Java虚拟机栈

栈帧是方法执行期的基础数据结构栈容量可由-Xss參数设定

  • 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程同样。

    每个方法被调用直至运行完毕的过程。就相应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  • 常有人把Java内存区分为堆内存(Heap)和栈内存(Stack),这样的分法比較粗糙,Java内存区域的划分实际上远比这复杂。

    当中所指的“堆”在后面会专门讲述,而所指的“栈”就是如今讲的虚拟机栈。或者说是虚拟机栈中的局部变量表部分。

  • 局部变量表所需的内存空间在编译期间完毕分配,当进入一个方法时。这种方法须要在帧中分配多大的局部变量空间是全然确定的,在方法执行期间不会改变局部变量表的大小。

    主要存放了编译期可知的各种基本数据类型、对象引用(reference类型)、returnAddress类型

3.本地方法栈

栈容量可由-Xss參数设定

虚拟机栈为虚拟机运行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

4.Java堆

可通过參数 -Xms 和-Xmx设置  。

Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被全部线程共享的一块内存区域。在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,差点儿全部的对象实例都在这里分配内存。

5.方法区

參数-XX:MaxPermSize可设置 .

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机载入的类信息、常量、静态变量、即时编译器编译后的代码等数据。尽管Java虚拟机规范把方法区描写叙述为堆的一个逻辑部分。可是它却有一个别名叫做Non-Heap(非堆)。目的应该是与Java堆区分开来。

6.执行时常量池

能够通过-XX:PermSize和-XX:MaxPermSize设置

  • 执行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件里除了有类的版本号、字段、方法、接口等描写叙述等信息外,另一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用。这部分内容将在类载入后存放到方法区的执行时常量池中。
  • 执行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定仅仅能在编译期产生。执行期间也可能将新的常量放入池中,这样的特性被开发者利用得比較多的便是String类的intern()方法。

7.直接内存

可通过-XX:MaxDirectMemorySize指定。假设不指定,则默认与Java堆的最大值(-Xmx指定)一样

直接内存(Direct Memory)并非虚拟机执行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,可是这部分内存也被频繁地使用。并且也可能导致OutOfMemoryError异常出现

三、HotSpot虚拟机对象探秘

主要探讨HotSpot虚拟机在Java堆中对象分配、布局和訪问的全过程。

1.对象的创建

虚拟机遇到new指令时。

  • 首先去检查这个指令的參数是否能在常量池中定位到一个类的符号引用。而且检查引用代表的类是否已被载入、解析和初始化过。假设没有,则运行类载入过程(第7章)。
  • 载入检查通过后。分配内存(内存在类载入完毕后便可全然确定)。
  • 内存分配完毕后,虚拟机对对象进行必要的设置。如对象是哪个类的实例、怎样找到类的元数据信息等(都放在对象的对象头中)。
  • 从虚拟机角度看,一个新的对象产生了。但从java程序视角看,对象创建才刚刚開始,由于<init>方法还没有运行。,全部字段为零。运行new指令之后会接着运行<init>方法(构造方法),进行初始化,这样一个真正可用的对象才算完毕产生。

2.对象的内存布局

分为对象头、实例数据、对齐填充三部分。

1)对象头

包括两部分

  • 存储对象自身的执行时数据,如哈希码、GC分代年龄等。

    长度在32位和64位的虚拟机中。分别为32bit、 64bit,官方称它为“Mark Word”

  • 类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

注:假设对象是一个java数组。对象头中还必须有一块记录数据长度的数据。

2)实例数据

对象真正存储的实用信息,也是程序中定义的各种类型的字段内容。

3)对齐填充

因为HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍。通俗的说,就是对象大小必须是8字节的整数倍。

对象头正好是8字节的倍数。当实例数据部分没有对齐时,须要通过对齐填充来补全。

3.对象的訪问定位

对象訪问在Java语言中无处不在,是最普通的程序行为。但即使是最简单的訪问。也会却涉及Java栈、Java堆、方法区这三个最重要内存区域之间的关联关系,如以下的这句代码:

   Object obj = new Object();

 如果这句代码出如今方法体中。那“Object obj”这部分的语义将会反映到Java栈的本地变量表中,作为一个reference类型数据出现。

而“new Object()”这部分的语义将会反映到Java堆中。形成一块存储了Object类型全部实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存

因为reference类型在Java虚拟机规范里面仅仅规定了一个指向对象的引用,并未定义这个引用应该通过哪种方式去定位。以及訪问到Java堆中的对象的详细位置,因此不同虚拟机实现的对象訪问方式会有所不同,主流的訪问方式有两种:使用句柄和直接指针。

四、总结

最后上一张本章结构图:

深入理解java虚拟机系列(一):java内存区域与内存溢出异常