《深入理解java虚拟机》读书笔记 第二章 java内存区域与溢出异常

java与c++ 相比,对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出问题,由虚拟机管理内存。这一切看起来都很美好。不过,也正是因为Java程序员把内存控制的权力交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会成为一项异常艰难的工作。

此问题类似于springboot和spring。springboot将默认配置集成于AutoConfig,在使用时十分方便,无需配置就可以将一个应用运行起来。但在出现配置问题时,不了解组件内部的人将被“如何正确配置”的问题困扰。

java虚拟机运行时数据区图
在这里插入图片描述

1. 运行时数据区域

程序计数器

线程私有,是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2. java虚拟机栈(Java Virtual Machine Stacks )

线程私有,为虚拟机执行java方法(即字节码)在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),
如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈( Native Method Stack )

与java虚拟机栈类似,但是其为本地方法(native)服务,在hotSpot虚拟机中,将java虚拟机栈和本地方法栈二合一。

java堆(Heap)

又称为GC堆,属于线程共享区域。
对于大多数应用来说,java堆是jvm所管理的内存中的最大一块。
几乎所有的对象实例都在这里分配内存。
现代垃圾收集器基本都采用分代回收法,所以java堆中还可以细分为:
1. 新生代
2. 老年代
根据Java虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑
上是连续的即可,就像我们的磁盘空间- -样。在实现时,既可以实现成固定大小的,也可以
是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError
异常。

方法区(Method Area)/(Non-Heap )

属于线程共享区域,用于存储已被虚拟机加载的
1. 类信息
2. 常量
3. 静态变量
4. 即时编译器编译后的代码等数据

java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap (非堆),目的
应该是与Java堆区分开来。

根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池 ( Runtime Constant Pool )

是方法区的一部分。自然会受到方法区内存限制,当常量池无法申请到内存,将抛出OutOfMemoryError异常。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一-定只有编译期才能产生,也就是并非预置人Class文件中常量池的内容才能进人方法区运行时常量池,运行期间也可能将新的常量放人池中,这种特性被开发人员利用得
比较多的便是String 类的intern() 方法。

直接内存

直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分,也不是Java;
虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致
OutOfMemoryError异常出现,所以我们放到这里一起讲解。
在JDK 1.4 中新加入了NIO (New Input/Output)类,引人了- -种基于通道(Channel)
与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过--
个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一- 些场
景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是
会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限
制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽
略直接内存,使得各个内存区域总和大于物理内存限制( 包括物理的和操作系统级的限制),
从而导致动态扩展时出现OutOfMemoryError异常。

hotspot 对象探秘

书中讨论的仅为普通java对象,不包括数组、Class对象。

对象的创建

虚拟机遇到一条new指令

首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用

并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

内存分配方式:

  • 设java堆中内存绝对规整,则通过简单的“指针碰撞”(Bump The Point)即可。
  • 设java堆中内存不规整,则需要通过“空闲列表”(Free List)的方式
graph LR
A[GC是否带有压缩整理功能]-- 决定 -->B[java堆是否规整]
B -- 选择--> C[分配方式]
Serial、ParNew等带Compact的收集器,采用指针碰撞
CMS 基于 标记回收(Mark-Sweep)算法的收集器时,采用空闲列表

接下来,虚拟机要对对象进行必要的设置

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。

执行init方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程
序的视角来看,对象创建才刚刚开始。<init> 方法还没有执行。所
以,一般来说,(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着
执行<init>方法,

至此,一个真正可用的对象才算完全产生出来。

对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:
1. 对象头(Header)
2. 实例数据(Instance Data)
3. 对齐填充(Padding)

对象头

1. 对象头一部分存储对象自身的运行时数据,如哈希码(HashCode)、 GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称为“Mark Word”。
2. 对象头另一部分存储类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指
针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型
指针,换句话说,查找对象的元数据信息并不一-定要经过对象本身,这点将在2.3.3节讨论。
另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因
为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据
中却无法确定数组的大小。

在这里插入图片描述

以下是32bit下Mark Word的代码
在这里插入图片描述

接下来的实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种
类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部
分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定
义顺序的影响。
HotSpot 虚拟机默认的分配策略为longs/doubles、ints、 shorts/chars、 bytes/
booleans、oops (Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被
分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果
CompactFields参数值为true (默认为true),那么子类之中较窄的变量也可能会插人到父类
变量的空隙之中。
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
由于HotSpotVM的自动内存管理系统要求对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1 倍或者
2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位

由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式
也是取决于虚拟机实现而定的。目前主流的访问方式有:

1. 使用句柄
2. 直接指针
SunHotSpot是使用"直接指针"方式进行对象访问的,
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,
由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本

猜你喜欢

转载自blog.csdn.net/lik_lik/article/details/89112509