【有书共读】《深入理解java虚拟机》读书笔记021

第二部分:自动内存管理机制

§1 - Java内存区域与内存溢出异常

Author:Sirice

Sirice-Github

java和C++的设计思路最特色的体现就在于内存回收机制,C++:“垃圾回收这么重要的事情当然要我自己来做”,java:“垃圾回收这么重要的事情当然要虚拟机来做”因此C++有delete/free函数,而java采用GC完成垃圾回收。


Java的内存模型

picture

程序计数器

程序计数器是一块较小的内存空间,可以看作当前执行的字节码行号指示器。字节码解释器就是根据这个计数器的值来选取下一条指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
注意这个区域是线程私有的,而且这个区域是java虚拟机内存规范中唯一没有规定任何OutOfMemoryError的区域。

Java虚拟机栈

线程私有,描述每个方法在执行的同时创建一个栈桢,用于存储局部变量表、动态链接、方法出口等信息。每一个方法从调用到结束的过程,就是一个栈桢在虚拟机栈中从入栈到出栈过程。

虚拟机规范中规定,线程请求的栈深度大于虚拟机所允许的深度,会抛出StackOverflowError异常(常见的为递归深度过高)。如果内存动态拓展时,无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

虚拟机栈是虚拟机栈为虚拟机执行java方法服务,而本地方法栈是为虚拟机执行Native方法服务。本地方法也会抛出StackOverflowError异常和OutOfMemoryError异常

Java Heap(堆)

java虚拟机中所管理内存中最大的一块,是被所有java线程共享的一块内存区域,在虚拟机启动时创建。此内存唯一的目的就是存放java对象实例,几乎所有的对象实例都会在这里分配内存,Java虚拟机规范原文:“所有对象实例以及数组都要在堆上分配”。

堆是GC主要管理的区域,因此也称“GC堆”,现代垃圾回收算法基本都采用分代收集算法,因此可以细分为“新生代”和“老年代”。

堆可以处于物理不连续的内存空间中,只要逻辑上是连续的即可。当前大部分虚拟机可以指定堆的大小。在堆中没有内存完成实例分配时会抛出OutOfMemoryError异常。

方法区

这是所有java线程共享的一块内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量部分、即时编译器编译之后的代码等数据。虽然在java虚拟机规范中描述为堆的一个逻辑部分,但是它的别名是Non-Heap(非堆)

很多人把方法区变量称为“永久代”(静态方法区)但本质上两者不等价,因为这个区域的限制比较宽松,不仅可以自定义大小,还可以设定是否进行GC。在方法区中不能满足内存分配时会抛出OutOfMemoryError异常。

运行常量池

方法区的一部分。Class文件中除了类的版本、字段、方法、接口等描述信息外,还有一部分常量池。常量池无法再申请到内存时会抛出OutOfMemoryError异常。

直接内存

直接内存是在NIO(New Input/Output类)中基于通道与缓冲区IO,直接分配堆外内存,这样能在一些情况下I按住提高内存,避免了java堆与Native堆中来回复制数据。

对象的创建

大体上分为4个步骤:类加载、分配内存、内存区域的初始化、虚拟机对对象进行必要的设置。

  • 类加载:虚拟机遇到一条new指令的时候,首先检查是否有必要进行类加载,即检查指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用是否已经被加载、解析和初始化,若没有,则进行类加载。
  • 分配内存:若是虚拟机的垃圾回收器有压缩整理内存的功能,即指针将内存区域分为两个部分,一边已分配,另一边未分配,则采用指针碰撞(Bump the Pointer)进行内存分配。否则采用空闲列表(Free List)分配内存。
  • 内存空间初始化为零值:顾名思义,将对象分配到的内存空间进行初始化,但是不包括对象头。
  • 对对象进行一些必要的设置:例如这个对象是哪个类的实例,如何找到类的元数据信息,对象哈希码,GC分代年龄等信息。这些信息存放在对象头(Object Header)中。

至此,虚拟机认为这个对象已经创建完毕,但是在我们程序中,这个对象才刚刚可以使用。

对象的布局

以HotSpot虚拟机为例,对象在内存中的存储布局分为三个部分:对象头(Object Header)、实例数据(Instance Data)、对齐填充(Padding)。

  • 对象头:包括两部分的信息,第一部分用于存储对象本身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。第二部分用于存储对象的类元数据指针,虚拟机可以通过这个指针找到对象的类元数据。(还有通过句柄寻找对象的并不需要这个指针,下面将会说到)
  • 实例数据:实例数据没啥好说的,就是对象真正存储的有效信息,代码中定义的各类型字段内容。
  • 对齐填充:它并不是必须的,仅仅起到了占位符的作用。因为HotSpot虚拟机的自动内存管理系统要求对象起始地址必须要是8的整数倍,而对象头刚好就是8的整数倍,因此我们只需要对实例数据进行部分填充就可以使得对象的起始地址是8的整数倍。这种考虑是为了设计简单和寻址效率。

对象的访问定位

了解了对象的创建和对象在内存中的布局之后,接下来就应该考虑如何去访问它了。对象的访问方式大体上分为两种,一种是直接指针,一种是句柄池,取决于具体的虚拟机实现。

  • 句柄池:这种方式java堆需要划分一块内存来作为句柄池,对象的引用reference指向的就是句柄地址,而句柄地址中存放的是对象的实例数据和类型数据的地址指针。这种方式也就是我们上面说到的,可以不通过对象来找到其类元数据,此时对象的对象头可以不存类型数据的指针。模型图如下:picture2

  • 直接指针:这种方式就是我们常规的认识,因为我们经常是使用HotSpot虚拟机的,而这个虚拟机就是采用直接指针实现。这种方式对象的引用reference指向对象的存放空间地址。模型如下:picture3

这种访问方式有什么区别呢?句柄访问方式是稳定的句柄地址,在对象被移动的时候,只会改变句柄中实例数据的指针,而reference本身不需要修改,但是多了一次寻址开销。直接指针则是快,但是相应地在同场景下需要修改reference指针值。这些修改时虚拟机自动完成的,对程序员透明,但是相应的开销是无法避免的。

java内存溢出异常

此部分分为

  • java堆溢出
  • 虚拟机栈和本地方法栈溢出
  • 方法区和运行时常量池溢出
  • 本机内直接内存溢出

上述4个可能的溢出原因在前面文章中提到,实际的监控工具和jvm参数调优将在笔者亲自试验之后后续更新。


(部分图片来自网络)

=======第2章结束=========

#笔记##读书笔记#
全部评论

相关推荐

程序员小白条:学历和简历问题,你想走开发,现在很难的啦,尤其后端方向很难走,前端、测开,都会好很多,另外要等8月底和9月初去投日常
点赞 评论 收藏
分享
评论
点赞
5
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务