关于java:Java虚拟机1Java内存模型

38次阅读

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

前言

在昨天我答复了一个对于 Java 虚拟机的问题,顺带温习了一边 Java 虚拟机,就打算写一篇对于内存模型的文章坚固记忆。在 Java 中,内存溢出异样不想 C /C++ 那样频繁,然而一旦呈现却难解决的多,须要丰富的 Java 虚拟机方面的常识。身为一个 Java 程序员,是有必要在这方面多做积攒的。本文以介绍概念与根本术语为主

运行时数据区域

Java 虚拟机在执行 Java 程序的时候会将他所治理的内存分为若干个不同的数据区域。这些区域有各自的用处,以及创立和销毁的工夫。有的区域随着 JVM 过程的启动就始终存在,而有的区域依赖用户线程的启动和完结而建设和销毁。依据《Java 虚拟机标准》,本文将简略介绍几个运行时区域:

  • 程序计数器:以后线程所执行的字节码的行号指示器
  • Java 虚拟机栈和本地办法栈:办法执行的线程内存模型,寄存了编译期可知的各种 Java 虚拟机根本数据类型、对象援用和 returnAddress 类型。
  • Java 堆:寄存对象实例
  • 办法区:用于存储被虚拟机加载的类型信息、常量、动态变量、即时编译器编译后的代码缓存等数据

程序计数器

程序计数器 (Program Counter Register) 是一块较小的内存空间,它能够看做是 以后线程所执行的字节码的行号指示器。在 Java 虚拟机概念模型里,字节码解释器工作时就是通过扭转这个指示器来选取下一条须要执行的字节码指令。他是程序控制流的指示器,分支、循环、异样解决等操作都依赖这个指示器实现。多线程的轮流切换、复原线程继续执行也依赖这个指示器。

“线程公有”的内存

重点说说这个多线程的状况。通过 Java 根底的学习咱们都晓得多线程是通过工夫片轮转调度算法调配每个线程在 CPU 上的执行工夫实现的。而每一个处理器(多核处理器来说是一个内核)同一时刻只会执行一条指令,为了让线程从新拿到 CPU 执行权时可能持续实现为执行完的程序。每条线程都会有一个计数器,这个计数器用于记录线程让出 CPU 时执行到的地址,不便线程的复原。而各条线程之间的计数器互不影响,独立存储。咱们称这类内存区域为“线程公有”的内存。

如果线程在执行 Java 办法,这个计数器记录的时正在执行的虚拟机字节码指令的地址;如果正在执行一个本地办法(Native)这个计数器值为空(Undefined)。这个内存区域也是《Java 虚拟机标准》惟一一个没有规定任何 OutOfMemoryError 状况的区域。

Java 虚拟机栈

Java 虚拟机栈 (Java Virtual Machine Stack) 也是线程公有的,它的生命周期与线程雷同 。Java 虚拟机栈形容的是Java 办法执行的线程内存模型。每个 Java 办法被执行的时候,Java 虚拟机都会同步创立一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动静连贯、办法进口等信息。每一个办法被调用直至执行结束的过程,就对应着一个栈帧在虚拟机从入栈到出栈的操作。咱们通常说的“栈”就指的这里的虚拟机栈。

局部变量表

虚拟机栈中,最广为人知的、也是咱们接触的最多的就是局部变量表局部。局部变量表 寄存了编译期可知的各种 Java 虚拟机根本数据类型 (int,double,float,short)、对象援用(reference,指向对象地址的指针或者是一个代表对象的句柄,这个前面再讲) 和 returnAddress 类型(指向了一条字节码指令的地址)。这些数据类型以 局部变量槽 (Slot) 来示意,其中 64 位的 long 和 double 类型的数据会占用两个变量槽。

局部变量表所需的内存空间切实编译期间实现调配的,在办法运行期间不会扭转局部变量表的大小。这里说的大小指的是变量槽的数量,而具体的内存空间(变量槽的大小),由虚拟机本人决定。

异常情况

在《Java 虚拟机标准》中,规定了两类异常情况。如果线程申请深度大于虚拟机容许的最大深度,将抛出 StackOverflowError 异样;如果 Java 虚拟机栈容量能够动静扩大,当栈扩大时无奈申请到足够的内存将会抛出OutOfMemoryError(之后简称 OOM)异样

在 HotSpot 虚拟机中是不容许栈容量的主动扩大的,所以不会呈现因为虚拟机栈无奈扩大而抛出 OOM 异样的问题

本地办法栈

本地办法栈 (Native Method Stacks) 与虚拟机栈所施展的作用是相似的。然而 Java 虚拟机栈是为执行 Java 办法服务的,而本地办法栈是为底层的本地(Native) 办法服务的

在 HotSpot 虚拟机中将本地办法栈与虚拟机栈合二为一。

Java 堆

Java 堆 (Java Heap) 是虚拟机所治理的在内存中最大的一块,Java 堆是被所有线程共享的一块内存区域。这块内存区域惟一的目标就是寄存对象实例,Java 中简直所有的对象实例都在这里分配内存。

尽管《Java 虚拟机标准》中形容是“所有对象实例以及数组都该当在堆上调配”,但随着即时编译技术的提高,尤其是逃逸剖析技术的日渐弱小,栈上调配、标量替换优化伎俩导致 Java 对象实例在堆上分配内存不再那么相对。

Java 堆分配内存的特点

Java 堆是垃圾收集器治理的内存区域,因而也被称为“GC 堆”(Garbage Collected Heap),对于垃圾收集器当前会专门写文章记录,这里只简略提一下。Java 堆分配内存有以下几个特点:

  1. 所有线程共享的 Java 堆能够 划分出多个线程公有的调配缓冲区(Thread Local Allocation Buffer,TLAB),用来晋升对象调配的效率。不过无论怎么划分都无奈扭转 Java 堆的共性:所有区域存储的都只能是对象的实例。
  2. Java 堆 能够处于物理上不间断的内存空间中,但在逻辑上他应该被视为间断的,这点和咱们应用磁盘存储文件是一样的。不过对于大对象(例如数组对象),大多数虚拟机出于实现简略、存储高效的思考,很可能会要求间断的内存空间。
  3. Java 堆既能够被实现成固定大小,也能够是可扩大的 。以后的支流虚拟机都是依照可扩大来实现的(通过参数-Xmx-Xms设定)。如果在 Java 堆中没有内存来实现实例调配,并且堆也无奈再扩大时,Java 虚拟机将会抛出 OutOfMemoryError 异样。

办法区

办法区 (Method Area) 也是线程共享的内存区域,它用于存储被虚拟机加载的 类型信息 常量 动态变量 即时编译器编译后的代码缓存 等数据

在这里咱们提一嘴 永恒代 的概念,在晚期 HotSpot 的实现中,办法区是和 Java 堆连在一起的。那时 Java 堆是基于分代收集实践设计的,办法区也被习惯称为永恒代(因为《Java 虚拟机标准》对办法区的束缚十分宽松。因为垃圾收集在这个区域很少见,能够不抉择实现垃圾收集,所以很多常量随着程序编译就始终存在)。然而到了 JDK8 的时候,HotSpot 就齐全放弃了永恒代的概念。办法区不再与 Java 堆间断,而是放在了 本地内存 (Native Memory) 中实现的 元空间 (Meta-space) 中。

放弃永恒代的起因有很多。最大的起因就是 Oracle 心愿能将 JRockit 的长处整合到 Hotspot 中,然而因为办法区差别过大而呈现很多问题。思考到 hotspot 将来的倒退,Oracle 决定放弃永恒代的概念。

依据《Java 虚拟机的规定》,如果办法区无奈满足新的内存调配的需要是,将会抛出 OutOfMemoryError 异样。

运行时常量池

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

重要特色:

  • 对于运行时常量池,《Java 虚拟机标准》并没有任何细节上的要求,不同的虚拟机能够 依照本人的需要实现这个内存区域。一般来说,除了保留 Class 文件中形容的符号援用歪,还会把由符号援用翻译过去的间接援用也存储到运行时常量池。
  • 运行时常量池相较于 Class 文件常量池的另外一个重要的特色就是 具备动态性 。Java 语言并不要求常量肯定只有在编译器能力产生。也就是说,运行期间产生的常量也可能退出池中。这种个性被开发人员用的比拟多的就是 String 类中的intern() 办法。

????:不论如何批改,上述所有内存都是基于本机内存的。不能使各个区域内存总和大于物理内存限度的状况。否则会呈现因为动静扩大申请不到足够的空间而产生 OutOfMemoryError 异样的状况。

HotSpot 虚拟机对象探秘

对象的创立

在 Java 语言中创建对象只用一个 new 关键字就够了,但实际上呢?接下来就让咱们看看 HotSpot 虚拟机是如何创立一个对象的。对象的创立通过了上面几个步骤:

  1. 查看类加载。当 Java 虚拟机遇到一条字节码 new 指令时,首先会查看这个指令的参数是否能在常量池定位到一个类的符号援用,并查看符号所代表的类是否已被加载、解析和初始化过。如果没有先进行类加载过程;
  2. 为新生对象分配内存 。对象所需内存在类加载过程中就能够确定下来,随后就从 Java 堆中划分出一块内存给这个对象。调配的办法有 指针碰撞 (间断的空间调配)和 闲暇列表(零散的调配)两种,取决于
  3. 内存调配实现之后,虚拟机必须将调配到的内存空间进行初始化(如对 int 类型赋值为 0,对 boolean 类型赋值为 false)
  4. Java 虚拟机对对象进行必要的设置(对象头)
  5. 执行构造方法

对象的内存布局

在 HotSpot 虚拟机里,对象在堆内存的存储布局能够分为三个局部:对象头 (Header) 实例数据 (Instance Data)对齐填充(Padding)

对象头

对象头局部蕴含两局部信息:

  1. 存储对象本身的运行数据,如哈希码(HashCode)、GC 分代年龄、锁状态标记、线程持有的锁、偏差线程 ID、偏差工夫戳等。这部分在 32 位和 64 位虚拟机(未开启压缩指针)中别离为 32 个比特和 64 个比特。官网称为“Mark Word”
  2. 类型指针,即对象指向它的类型元数据的指针。

如果对象是个 Java 数组,对象头中还必须有一块用于记录数组长度的数据。

对于类型指针,并不是所有虚拟机的实现都必须在对象数据上保留内存指针,也就是说查找对象的元数据信息并不一定要通过对象自身。例如通过句柄拜访对象的虚拟机就不须要类型指针。HotSpot 虚拟机应用间接指针拜访对象,因而有这一部分,具体往后看。

实例数据

寄存对象真正存储的无效信息

对齐填充

为了不便虚拟机治理,HotSpot 虚拟机要求对象的起始地址必须是 8 字节的整数倍,所以有余 8 字节整数倍的都用对齐填充来补全

对象的拜访

为了后续的应用对象,Java 程序会通过栈上的 reference 数据来找到 Java 堆中的具体对象。这个 reference 就是指向对象的一个援用。而拜访堆上的具体对象的形式是没有明确要求的,常见的对象的拜访形式有两种:

  1. 应用句柄拜访:在 Java 堆中划分出一块内存作为句柄池。当有对象在实例池中创立时,句柄池就会创立一个句柄,蕴含了两个指针(一个指向 Java 堆的实例数据,另外一个指向了办法区的对象类型数据)。reference 存储的被创建对象的句柄地址。
  2. 间接指针拜访:这就是在对象实例数据外面加了一个类型数据的指针,即类型指针。reference 间接指向对象的实例数据,再通过类型指针找到办法区外面的对象类型数据。

两种拜访形式各有千秋,句柄最大的益处就是批改简略,不须要扭转 reference 数据即可实现对象的挪动(垃圾收集时,对象的挪动十分广泛)。而间接拜访最大的益处就是速度快,节俭了一次指针定位的工夫。因为对象的拜访十分频繁,这是一笔十分可观的数据。

总结

Java 内存模型这一块是十分重要的,也是所有常识的根底。这里概念比拟多,我只选取记录了最重要的几局部,不便日后针对性温习,一些具体的信息请看周志明老师的《深刻了解 Java 虚拟机》一书。

正文完
 0