[JVM]-[深入理解Java虚拟机学习笔记]-第二章-Java内存区域

前言

JVM的主要组成

JVM包括 两个子系统和两个组件,两个子系统为 Class Loader 类加载器Execution Engine 执行引擎;两个组件为 Runtime Data Area 运行时数据区Native Interface 本地接口

类加载器 通过给定的全限定类名加载 Class 文件到 运行时数据区 的方法区中
执行引擎 执行 class 文件中的指令
执行过程可能需要调用 本地接口 用来与本地库进行交互
运行时数据区就是常说的 JVM 内存

JVM 的作用

首先通过编译器将 Java 代码转化为字节码,类加载器将字节码加载到内存中,将其放在方法区中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎,将字节码翻译成底层系统指令,再交由 CPU 执行,这个过程会调用其他语言的本地库接口来实现整个程序的功能

Java内存区域

概述

对于 C/C++ 的开发人员来说,他们拥有每一个对象 (malloc等空间分配函数得到的区域) 的 ”所有权“,又担负着每一个对象生命从开始到终结的维护 (使用delete,free等函数进行),而 Java 程序员在虚拟机自动内存管理机制的帮助下,不需要为每个 new 操作去写配对的 delete/free 代码,不容易出现内存泄漏和内存溢出问题;但另一方面正是因为控制内存的权利交给了 Java 虚拟机,一旦出现内存泄漏和内存溢出等问题,如果不了解虚拟机是怎样使用内存的,那么查找错误,解决问题将会异常艰难,因此有必要对这方面的内容进行学习

内存溢出:可以理解为,内存空间被过多地占用,导致当前有程序想要申请内存时没有足够的内存可以申请来使用
内存泄漏:可以理解为,内存中有部分区域被申请使用后,因为某些原因没有被释放,而其它程序也无法申请到这部分空间,导致内存空间被浪费。最终就会导致内存溢出

运行时数据区

在这里插入图片描述

  • 程序计数器:可以看作是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条要执行的字节码指令

    由于虚拟机的多线程是通过线程轮流切换,分配处理器执行时间的方式来实现的,所以在任何一个确定的时刻,一个 CPU (或者说一个核) 都只会执行一条线程中的指令,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器

    此内存区域是唯一一个在Java虚拟机规范中没有规定任何 O u t O f M e m o r y E r r o r OutOfMemoryError OutOfMemoryError 情况的区域

  • 虚拟机栈:线程私有,生命周期与线程相同。描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,虚拟机同步创建一个栈帧用于存储局部变量表,操作数栈,动态连接,方法出口等信息,方法被调用到执行完毕,对应着一个栈帧的入栈到出栈

  • 本地方法栈:与虚拟机栈相似,区别是虚拟机栈为虚拟机执行 Java 方法 (字节码) 服务,本地方法栈则是为虚拟机使用本地方法 (native) 服务

  • :所有线程共享,在虚拟机启动时创建。其唯一目的就是存放对象实例。堆是垃圾收集器管理的内存区域,又被称作 “GC堆”

  • 方法区:各个线程共享。用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码缓存等数据

  • 运行时常量池:是方法区的一部分,Class 文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池

  • 直接内存:JDK1.4 中加入了 NIO(New Input/Ouput) 类,引入了一种基于通道 Channel 与缓冲区的 IO 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据

对象的创建,布局,访问

对象创建

  • 当虚拟机遇到一条字节码 new 指令时,首先 检查这个指令的参数是否能在常量池中定位到一个类的符号引用,而且 检查这个符号引用代表的类是否已被加载,解析和初始化过,如果没有 就要执行 类加载 过程
  • 类加载检查通过后,就要为新生对象分配内存,为对象分配空间其实等同于把一块确定大小的内存块从 Java 堆中划分出来。空间分配方法有指针碰撞空闲列表两种方式,选择哪种方式由Java堆是否规整决定,而Java堆是否规整由所采用的垃圾收集器是否具有空间压缩整理能力决定。SerialParNew等带压缩整理过程的收集器使用的就是指针碰撞,简单而高效;CMS 收集器基于清除算法,理论上就使用较复杂的空闲列表来分配。之所以说理论上,是因为在 CMS 的实现中,为了大多数情况下分配得更快,设计了一个叫做 L i n e a r A l l o c a t i o n B u f f e r Linear Allocation Buffer LinearAllocationBuffer 的分配缓冲区,通过空闲列表拿到一大块分配缓冲区后,在其中仍然可以使用指针碰撞的方式来分配
  • 对象创建是非常频繁的行为,因此并发情况下不是线程安全的。解决方案有两种:一是 对分配空间的操作进行同步处理,虚拟机采用的是 CAS 配上失败重试二是 把内存分配的动作按照线程划分出不同的空间,即每个线程在 Java 堆中先分配一小块内存,称之为 本地线程分配缓冲 Thread Local Allocation Buffer,线程在各自的分配区中进行分配,就不会发生线程问题,只有在本地缓冲区用完了要分配新的缓冲区时才需要同步锁定

对象的内存布局

对象在堆内存中的存储布局可分为三部分:对象头 Header实例数据 Instance Data对齐填充 Padding

对象头

包含两类信息:自身的运行时数据 以及 类型指针

  • 对象自身的运行时数据:如哈希码GC分代年龄锁状态标志线程持有的锁。这部分数据的长度在32位和64位机的虚拟机中分别为 32个比特 和 64个比特,官方称其为 Mark Word,考虑到虚拟机的空间效率,Mark Word 被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间
  • 类型指针:即对象指向它的类型元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例
  • 如果对象是数组,那在对象头中还必须有一块用于记录数据长度的内存,这块内存只占 4B,所以 Java 数组可以申请的最大长度为 232

实例数据部分

对象真正存储的有效信息,即代码中定义的各种类型的字段

对齐填充

HotSpot 要求对象起始地址必须是8字节的整数倍,也就是说任何对象的大小都必须是 8 字节的整数倍,而对象头部分已经精心设计成 8 字节的倍数,所以如果实例数据部分没有对齐的话就要靠对齐填充来补全

对象的访问

我们通常使用的引用类型数据存储在栈中,是一个指向对象的引用 reference,通过这个引用来访问对象,主流的访问方式有两种:句柄直接指针。句柄方式下,reference 指向句柄,然后句柄再指向对象的实例数据以及类型数据,所以在访问对象时 reference 要先定位到句柄,再进一步定位到对象的位置上,句柄地址稳定,在对象被移动时只会修改句柄中指向对象实例数据的指针,而reference本身不需要被修改;而使用直接指针的好处就是节省了一次指针定位的时间开销,由于对象访问在 Java 中非常频繁,因此这类开销积少成多也是一项可观的执行成本。所以在 JVM 中使用的是 直接指针 的方式

猜你喜欢

转载自blog.csdn.net/Pacifica_/article/details/124430395