关于java:没人看华为技术专家首次分享JVM内存模型详解网友直呼真香

4次阅读

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

今日分享开始啦,请大家多多指教~

JVM 运行时数据区

  • 线程独占

每个线程都会有它独立的空间,随线程生命周期而创立和销毁

  • 线程共享

所有线程能拜访这块内存数据,随虚拟机或者 GC 而创立和销毁

1 Program Counter Register (程序计数寄存器)

Register 之名源于 CPU 的寄存器,CPU 只有把数据装载到寄存器才可能运行

寄存器存储指令相干的现场信息,因为 CPU 工夫片轮限度,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致常常中断或复原,如何保障分毫无差呢?

每个线程在创立后,都会产生本人的程序计数器和栈帧,程序计数器用来寄存执行指令的偏移量和行号指示器等,线程执行或复原都要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域也不会产生内存溢出异样。

1.1. 定义

这是一块较小的内存空间,可看作以后线程正在执行的字节码的行号指示器。如果以后线程正在执行的是:

  • Java 办法

计数器记录的就是以后线程正在执行的字节码指令的地址

  • 本地办法

那么程序计数器值为 undefined

1.2. 作用

程序计数器(后文简称为 PCR)有两个作用:

  • 字节码解释器通过扭转 PCR 顺次读取指令,实现代码的流程管制,如:程序执行、抉择、循环、异样解决
  • 多线程状况下,PCR 用于记录以后线程执行的地位,从而当线程被切换回来的时候可能晓得该线程上次运行到哪了

1.3. 特点

一块较小的内存空间,【线程公有】。每条线程都有一个独立的程序计数器。

惟一一个不会呈现 OOM 的内存区域。

2. Java 虚拟机栈(JVM Stack)

2.1. 定义

绝对于基于寄存器的运行环境,JVM 是基于栈构造的运行环境。栈构造移植性更好,可控性更强。

JVM 中的虚拟机栈是形容 Java 办法执行的内存区域,属【线程公有】。

栈中的元素用于反对虚拟机进行办法调用,每个办法从开始调用到执行实现的过程,就是栈帧从入栈到出栈的过程。

3. 本地办法栈(Native Method Stack)

和虚拟机栈性能相似,虚拟机栈是为虚拟机执行 JAVA 办法而筹备的

虚拟机标准没有规定具体的实现,由不同的虚拟机厂商去实现。

HotSpot 虚拟机中虚拟机栈和本地办法栈的实现是一样的。同样,超出大小当前

也会拋出 StackOverflowError.

本地办法栈和 Java 虚拟机栈实现的性能与抛出异样简直雷同

只不过虚拟机栈是为虚拟机执行 Java 办法 (也就是字节码) 服务, 本地办法区则为虚拟机应用到的 Native 办法服务.

在 JVM 内存布局中,也是线程对象公有的, 然而虚拟机栈“主内”,而本地办法栈“主外”

这个“内外”是针对 JVM 来说的,本地办法栈为 Native 办法服务

线程开始调用本地办法时,会进入一个不再受 JVM 束缚的世界

本地办法能够通过 JNI(Java Native Interface)来拜访虚拟机运行时的数据区,甚至能够调用寄存器, 具备和 JVM 雷同的能力和权限

当大量本地办法呈现时, 势必会减弱 JVM 对系统的控制力, 因为它的出错信息都比拟黑盒.

对于内存不足的状况,本地办法栈还是会拋出 native heap OutOfMemory

最驰名的本地办法应该是 System.currentTimeMillis(),JNI 使 Java 深度应用 OS 的个性性能,复用非 Java 代码

然而在我的项目过程中,如果大量应用其余语言来实现 JNI, 就会丢失跨平台个性,威逼到程序运行的稳定性

如果须要与本地代码交互,就能够用两头规范框架进行解耦,这样即便本地办法解体也不至于影响到 JVM 的稳固

当然,如果要求极高的执行效率、偏底层的跨过程操作等,能够思考设计为 JNI 调用形式

2.2 构造

栈帧是办法运行的根本构造。

  • 在流动线程中,只有位于栈顶的帧才是无效的,称为以后栈帧
  • 正在执行的办法称为以后办法

在执行引擎运行时,所有指令都只能针对以后栈帧操作,StackOverflowError 示意申请的栈溢出,导致内存耗尽,通常呈现在递归办法。

以后办法的栈帧,都是正在战斗的战场,其中的操作栈是参加战斗的士兵

操作栈的压栈与出栈

虚拟机栈通过压 / 出栈,对每个办法对应的流动栈帧进行运算解决,办法失常执行完结,必定会跳转到另一个栈帧上。

在执行的过程中,如果出现异常,会进行异样回溯,返回地址通过异样处理表确定。

栈帧在整个 JVM 体系中的位置颇高,包含:局部变量表、操作栈、动静连贯、办法返回地址等。

局部变量表

寄存办法参数和局部变量。

绝对于类属性变量的筹备阶段和初始化阶段,局部变量没有筹备阶段,必须显式初始化。

如果是非静态方法,则在 index[0]地位上存储的是办法所属对象的实例援用,随后存储的是参数和局部变量。

字节码指令中的 STORE 指令就是将操作栈中计算实现的局部变量写回局部变量表的存储空间内。

操作数栈

一个初始状态为空的桶式构造栈。因为 Java 没有寄存器,所有参数传递应用操作数栈。在办法执行过程中,会有各种指令往栈中写入和提取信息。JVM 的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈。

字节码指令集的定义都是基于栈类型的,栈的深度在办法元信息的 stack 属性中。

操作栈与局部变量表交互

具体的字节码操作程序如下:

第 1 处阐明: 局部变量表就像个中药柜,外面有很多抽屉, 顺次编号为 0, 1, 2,3,.,. n 字节码指令 istore_ 1 就是关上 1 号抽屉,把栈顶中的数 13 存进去, 栈是一个很深的竖桶,任何时候只能对桶口元素进行操作,所以数据只能在栈顶进行存取.

某些指令能够间接在抽屉里进行,比方 inc 指令,间接对抽屉里的数值进行 + 1 操作

程序员面试过程中,常见的 i ++ 和 ++ i 的区别,能够从字节码上比照进去

  • iload_ 1 从局部变量表的第 1 号抽屉里取出一个数, 压入栈顶,下一步间接在抽屉里实现 + 1 的操作,而这个操作对栈顶元素的值没有影响, 所以 istore_ 2 只是把栈顶元素赋值给 a
  • 表格右列,先在第 1 号抽屉里执行 + 1 操作,而后通过 iload_ 1 把第 1 号抽屉里的数压入栈顶,所以 istore_ 2 存入的是 + 1 之后的值,i++ 并非原子操作。即便通过 volatile 关键字进行润饰,多个线程同时写的话,也会产生数据相互笼罩的问题。

动静连贯

每个栈帧中蕴含一个在常量池中对以后办法的援用,目标是反对办法调用过程的动静连贯。

办法返回地址

办法执行时有两种退出状况:

  • 失常退出

失常执行到任何办法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等。

  • 异样退出

无论何种,都将返回至办法以后被调用的地位。办法退出的过程相当于弹出以后栈帧。

退出可能有三种形式:

  • 返回值压入,下层调用栈帧
  • 异样信息抛给可能解决的栈帧
  • PC 计数器指向办法调用后的下一条指令

Java 虚拟机栈是形容 Java 办法运行过程的内存模型。Java 虚拟机栈会为每一个行将运行的 Java 办法创立“栈帧”。用于存储该办法在运行过程中所须要的一些信息。

  • 局部变量表: 寄存根本数据类型变量、援用类型的变量、returnAddress 类型的变量
  • 操作数栈
  • 动静链接
  • 以后办法的常量池指针
  • 以后办法的返回地址
  • 办法进口等信息

每一个办法从被调用到执行实现的过程, 都对应着一个个栈帧在 JVM 栈中的入栈和出栈过程

留神:人们常说,Java 的内存空间分为“栈”和“堆”,栈中寄存局部变量,堆中寄存对象。

这句话不完全正确!这里的“堆”能够这么了解,但这里的“栈”就是当初讲的虚拟机栈, 或者说 Java 虚拟机栈中的局部变量表局部.

真正的 Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都领有:局部变量表、操作数栈、动静链接、办法进口信息.

特点

局部变量表的创立是在办法被执行的时候,随栈帧创立而创立。

表的大小在编译期就确定,在创立的时候只需调配当时规定好的大小即可。在办法运行过程中,表的大小不会扭转。Java 虚拟机栈会呈现两种异样:

StackOverFlowError

若 Java 虚拟机栈的内存大小不容许动静扩大, 那么当线程申请的栈深度大于虚拟机容许的最大深度时(但内存空间可能还有很多), 就抛出此异样

栈内存默认最大是 1M, 超出则抛出 StackOverflowError

OutOfMemoryError

若 Java 虚拟机栈的内存大小容许动静扩大, 且当线程申请栈时内存用完了, 无奈再动静扩大了, 此时抛出 OutOfMemoryError 异样

Java 虚拟机栈也是线程公有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创立而创立,随线程的死亡而死亡。

4 Java 堆(Java Heap)

JVM 启动时创立,寄存对象的实例。垃圾回收器次要就是治理堆内存。

Heap 是 OOM 故障最次要的发源地,它存储着简直所有的实例对象,堆由垃圾收集器主动回收,堆区由各子线程共享应用

通常状况下,它占用的空间是所有内存区域中最大的,但如果无节制地创立大量对象,也容易耗费完所有的空间

堆的内存空间既能够固定大小,也可运行时动静地调整,通过如下参数设定初始值和最大值,比方

-Xms256M. -Xmx1024M

其中 - X 示意它是 JVM 运行参数

  • ms 是 memorystart 的简称 最小堆容量
  • mx 是 memory max 的简称 最大堆容量

然而在通常状况下,服务器在运行过程中,堆空间一直地扩容与回缩,势必造成不必要的零碎压力,所以在线上生产环境中,JVM 的 Xms 和 Xmx 设置成一样大小,防止在 GC 后调整堆大小时带来的额定压力

堆分成两大块: 新生代和老年代

对象产生之初在新生代,步入晚年时进入老年代,然而老年代也接收在新生代无奈包容的超大对象

新生代 = 1 个 Eden 区 + 2 个 Survivor 区

绝大部分对象在 Eden 区生成,当 Eden 区装填满的时候,会触发 Young GC。垃圾回收的时候,在 Eden 区实现革除策略,没有被援用的对象则间接回收。仍然存活的对象会被移送到 Survivor 区,这个区真是货真价实的存在

Survivor 辨别为 S0 和 S1 两块内存空间,送到哪块空间呢? 每次 Young GC 的时候,将存活的对象复制到未应用的那块空间,而后将以后正在应用的空间齐全革除,替换两块空间的应用状态

如果 YGC 要移送的对象大于 Survivor 区容量下限,则间接移交给老年代

如果一些没有进取心的对象认为能够始终在新生代的 Survivor 区替换来替换去,那就错了。每个对象都有一个计数器,每次 YGC 都会加 1。

-XX:MaxTenuringThreshold

参数能配置计数器的值达到某个阈值的时候,对象从新生代降职至老年代。如果该参数配置为 1, 那么从新生代的 Eden 区间接移至老年代。默认值是 15,能够在 Survivor 区替换 14 次之后,降职至老年代

若 Survivor 区无奈放下,或者超大对象的阈值超过下限,则尝试在老年代中进行调配;

如果老年代也无奈放下,则会触发 Full Garbage Collection(Full GC);

如果仍然无奈放下,则抛 OOM.

堆呈现 OOM 的概率是所有内存耗尽异样中最高的

出错时的堆内信息对解决问题十分有帮忙,所以给 JVM 设置运行参数 -

XX:+HeapDumpOnOutOfMemoryError

让 JVM 遇到 OOM 异样时能输入堆内信息

在不同的 JVM 实现及不同的回收机制中,堆内存的划分形式是不一样的

寄存所有的类实例及数组对象

除了实例数据,还保留了对象的其余信息,如 Mark Word(存储对象哈希码,GC 标记,GC 年龄,同步锁等信息),Klass Pointy(指向存储类型元数据的指针)及一些字节对齐补白的填充数据(若实例数据刚好满足 8 字节对齐,则可不存在补白)

特点

Java 虚拟机所须要治理的内存中最大的一块.

堆内存物理上不肯定要间断, 只须要逻辑上间断即可, 就像磁盘空间一样.

堆是垃圾回收的次要区域, 所以也被称为 GC 堆.

堆的大小既能够固定也能够扩大, 但支流的虚拟机堆的大小是可扩大的(通过 -Xmx 和 -Xms 管制), 因而当线程申请分配内存, 但堆已满, 且内存已满无奈再扩大时, 就抛出 OutOfMemoryError.

线程共享

整个 Java 虚拟机只有一个堆, 所有的线程都拜访同一个堆.

它是被所有线程共享的一块内存区域, 在虚拟机启动时创立.

而程序计数器、Java 虚拟机栈、本地办法栈都是一个线程对应一个

5 办法区

5.1 定义

Java 虚拟机标准中定义方法区是堆的一个逻辑区划局部, 具体实现依据不同虚拟机来实现, 如: HotSpot 在 Java7 中办法区放在永恒代,Java8 放在元数据空间,并且通过 GC 机制对这个区域进行治理。

别名 Non-Heap(非堆),以与 Java 堆辨别。办法区中寄存曾经被虚拟机加载的:

  • 类信息
  • 常量

常量存储在【运行时常量池】

  • 动态变量

即时编译器 (JIT) 编译后的代码等数据

5.2 特点

  • 线程共享

办法区是堆的一个逻辑局部, 因而和堆一样, 都是线程共享的. 整个虚拟机中只有一个办法区.

  • 永恒代

办法区中的信息个别须要长期存在,而且它又是堆的逻辑分区,因而用堆的划分办法,咱们把办法区称为永恒代.

  • 内存回收效率低

Java 虚拟机标准对办法区的要求比拟宽松, 能够不实现垃圾收集.

办法区中的信息个别须要长期存在, 回收一遍内存之后可能只有大量信息有效.

对办法区的内存回收的次要指标是: 对常量池的回收和对类型的卸载

和堆一样,容许固定大小,也容许可扩大的大小,还容许不实现垃圾回收。

当办法区内存空间无奈满足内存调配需要时, 将抛出 OutOfMemoryError 异样.

5.3 运行时常量池(Runtime Constant Pool)

5.3.1 定义

办法区的一部分。

.java 文件被编译之后生成的.class 文件中除了蕴含:类的版本、字段、办法、接口等形容信息外,还有一项就是常量池。

常量池中寄存编译期间产生的各种字面量和符号援用,.class 文件中的常量池中的所有的内容在类被加载后寄存到办法区的运行时常量池中。

JDK6、7、8 三个版本中,运行时常量池的所处区域始终在一直地变动:

  • 6 时,是办法区的一部分
  • 7 时,又放到堆内存
  • 8 时,呈现了元空间,又回到了办法区

这也阐明了官网对“永恒代”的优化从 7 就曾经开始了。

5.3.2 个性

运行时常量池绝对于 class 文件常量池的另外一个个性是具备动态性,Java 语言并不要求常量肯定只有编译器才产生,也就是并非预置入 class 文件中常量池的内容能力进入办法区运行时常量池,运行期间也可能将新的常量放入池中。

String 类的 intern()办法就是采纳了运行时常量池的动态性。当调用 intern 办法时,看池中已蕴含一个等于此 String 对象的字符串:

则返回池中的字符串

将此 String 对象增加到池中,并返回此 String 对象的援用

5.3.3 可能抛出的异样

运行时常量池是办法区的一部分, 所以会受到办法区内存的限度, 因而当常量池无奈再申请到内存时就会抛出 OutOfMemoryError 异样.

咱们个别在一个类中通过 public static final 来申明一个常量。这个类被编译后便生成 Class 文件,这个类的所有信息都存储在这个 class 文件中。

当这个类被 Java 虚拟机加载后,class 文件中的常量就寄存在办法区的运行时常量池中。而且在运行期间,能够向常量池中增加新的常量。如:String 类的 intern()办法就能在运行期间向常量池中增加字符串常量。

当运行时常量池中的某些常量没有被对象援用,同时也没有被变量援用,那么就须要垃圾收集器回收。

6 间接内存(Direct Memory)

间接内存不是虚拟机运行时数据区的一部分, 也不是 JVM 标准中定义的内存区域, 但在 JVM 的理论运行过程中会频繁地应用这块区域. 而且也会抛 OOM

在 JDK 1.4 中退出了 NIO(New Input/Output)类, 引入了一种基于管道和缓冲区的 IO 形式, 它能够应用 Native 函数库间接调配堆外内存, 而后通过一个存储在堆里的 DirectByteBuffer 对象作为这块内存的援用来操作堆外内存中的数据.

这样能在一些场景中显著晋升性能, 因为防止了在 Java 堆和 Native 堆中来回复制数据.

综上看来

程序计数器、Java 虚拟机栈、本地办法栈是线程公有的,即每个线程都领有各自的程序计数器、Java 虚拟机栈、本地办法区。并且他们的生命周期和所属的线程一样。

而堆、办法区是线程共享的,在 Java 虚拟机中只有一个堆、一个办法栈。并在 JVM 启动的时候就创立,JVM 进行才销毁。

7 Metaspace (元空间)

到了 JDK8,元空间的前身 Perm 区(永恒代)被淘汰,在 JDK7 及之前的版本中,只有 Hotspot 才有 Perm 区,它在启动时固定大小,很难进行调优,并且 Full GC 时会挪动类元信息。

在某些场景下,若动静加载类过多,容易产生 Perm 区的 OOM。比方某工程因为性能点较多,运行过程中,要一直动静加载很多类,经常出现谬误:

为解决该问题,须要设定运行参数

-XX:MaxPermSize= l280m

如果部署到新机器上,往往会因为 JVM 参数没有批改导致故障再现。不相熟此利用的人排查问题时都苦不堪言。此外,永恒代在 GC 过程中还存在诸多问题。

所以,JDK8 应用元空间替换永恒代。区别于永恒代,元空间在本地内存中调配。即,

只有本地内存足够,它不会呈现相似永恒代的 java.lang.OutOfMemoryError: PermGen space

对永恒代的设置参数 PermSize 和 MaxPermSize 也会生效。在 JDK8 及以上版本,设定 MaxPermSize 参数,JVM 在启动时并不会报错,但会提醒:

Java HotSpot 64Bit Server VM warning:ignoring option MaxPermSize=2560m; support was removed in 8.0

默认状况下,“元空间”的大小能够动静调整,或者应用新参数 MaxMetaspaceSize 来限度本地内存调配给类元数据的大小。

在 JDK8 里,Perm 区所有内容中

  • 字符串常量移至堆内存
  • 其余内容包含类元信息、字段、动态属性、办法、常量等都挪动至元空间

比方上图中的 Object 类元信息、动态属性 System.out、整型常量 000000 等

图中显示在常量池中的 String,其理论对象是被保留在堆内存中的。

元空间特色

  • 充分利用了 Java 语言标准:类及相干的元数据的生命周期与类加载器的统一
  • 每个类加载器都有它的内存区域 - 元空间
  • 只进行线性调配
  • 不会独自回收某个类(除了重定义类 RedefineClasses 或类加载失败)
  • 没有 GC 扫描或压缩
  • 元空间里的对象不会被转移
  • 如果 GC 发现某个类加载器不再存活,会对整个元空间进行个体回收

GC

  • Full GC 时,指向元数据指针都不必再扫描,缩小了 Full GC 的工夫
  • 很多简单的元数据扫描的代码(尤其是 CMS 外面的那些)都删除了
  • 元空间只有大量的指针指向 Java 堆
  • 这包含:类的元数据中指向 java.lang.Class 实例的指针; 数组类的元数据中,指向 java.lang.Class 汇合的指针。
  • 没有元数据压缩的开销
  • 缩小了 GC Root 的扫描(不再扫描虚拟机外面的已加载类的目录和其它的外部哈希表)
  • G1 中,并发标记阶段实现后就能够进行类的卸载

元空间内存调配模型

  • 绝大多数的类元数据的空间都在本地内存中调配
  • 用来形容类元数据的对象也被移除
  • 为元数据调配了多个映射的虚拟内存空间
  • 为每个类加载器调配一个内存块列表
  • 块的大小取决于类加载器的类型
  • Java 反射的字节码存取器(sun.reflect.DelegatingClassLoader)占用内存更小
  • 闲暇块内存返还给块内存列表
  • 当元空间为空,虚拟内存空间会被回收
  • 缩小了内存碎片

最初, 从线程共享的角度来看

  • 堆和元空间是所有线程共享的
  • 虚拟机栈、本地办法栈、程序计数器是线程外部公有的

从这个角度看一下 Java 内存构造

8 从 GC 角度看 Java 堆

堆和办法区都是线程共享的区域,次要用来寄存对象的相干信息。一个接口中的多个实现类须要的内存可能不一样,一个办法中的多个分支须要的内存也可能不一样,咱们只有在程序运行期间能力晓得会创立哪些对象,因而,这部分的内存和回收都是动静的,垃圾收集器所关注的就是这部分内存(本节后续所说的“内存”调配与回收也仅指这部分内存)。而在 JDK1.7 和 1.8 对这部分内存的调配也有所不同:

Java8 中堆内存调配如下图:

9 JVM 敞开

  • 失常敞开:当最初一个非守护线程完结或调用了 System.exit 或通过其余特定于平台的形式, 比方 ctrl+c。
  • 强制敞开:调用 Runtime.halt 办法,或在操作系统中间接 kill(发送 single 信号)掉 JVM 过程。
  • 异样敞开:运行中遇到 RuntimeException 异样等

在某些状况下,咱们须要在 JVM 敞开时做一些开头的工作,比方删除临时文件、进行日志服务。为此 JVM 提供了敞开钩子(shutdown hocks)来做这些事件。

Runtime 类封装 java 利用运行时的环境,每个 java 应用程序都有一个 Runtime 类实例,应用程序能与其运行环境相连。

敞开钩子实质上是一个线程(也称为 hock 线程),能够通过 Runtime 的 addshutdownhock(Thread hock)向主 jvm 注册一个敞开钩子。hock 线程在 jvm 失常敞开时执行,强制敞开不执行。

对于在 jvm 中注册的多个敞开钩子,他们会并发执行,jvm 并不能保障他们的执行程序。

小结

内存是十分重要的系统资源,是硬盘和 CPU 的两头仓库及桥梁,承载着操作系统和应用程序的实时运行。

JVM 内存布局规定了 Java 在运行过程中内存申请、调配、治理的策略,保障了 JVM 的高效稳固运行。不同的 JVM 对于内存的划分形式和管理机制存在着局部差别。联合 JVM 虚拟机标准,来探讨经典的 JVM 内存布局。

今日份分享已完结,请大家多多包涵和指导!

正文完
 0