【JVM】JVM内存模型(JVMMM)

0、前言

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来。
——《深入理解Java虚拟机-JVM高级特性与最佳实践》

Java作为一种高级编程语言,得以大行其道的一个重要因素就是所依赖的运行环境Java虚拟机(JVM)的良好发展。JVM使得Java程序具有了平台无关性的优势,在PC端和各种移动终端都有良好的运行表现。

所谓平台无关性,是指JVM保证了Java程序能在多平台间进行无缝移植,即编译一次,处处运行。其原理是:JVM屏蔽了具体平台相关的信息,使Java语言编译程序只需要生成目标字节码(.class),由 JVM 把字节码解释成具体平台上的机器指令,就可以在多种平台上不加修改地运行,而非直接在硬件系统的 CPU 上执行。

说白了就是,JVM把Java程序解释成JVM能懂的语言(字节码),JVM在运行时负责把这一套语言解释成物理计算机器能懂的语言(具体的机器指令)。

JVM作为Java程序运行的载体,其重要特征就是自动的内存管理,内存管理主要包括内存分配(内存模型)和垃圾收集技术两部分。

1、什么是JVM内存模型

JVM的内存模型又叫JVM运行时数据区域。

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的运行时数据区域。这些区域用途各异,生命周期也各不相同。

2、JVM的内存模型构成

JVM的内存模型主要包括:堆内存、方法区(包括运行时常量池)、栈内存(包括虚拟机栈和本地方法栈)、程序计数器。

从共享范围上来划分可分为两类:线程共享区域(堆和方法区);线程私有区域(虚拟机栈、本地方法栈和程序计数器)。

从堆概念来划分可分为两类:堆内存和非堆内存。

JVM运行时数据区:
在这里插入图片描述
JVM内存的拓扑结构图:
在这里插入图片描述

PS:

在JDK8之前,永久代(permanent generation)是HotSpot JVM特有的概念,是方法区(JVM规范)的异一种实现。永久代是一片连续的堆空间。JDK8以后,JVM不再有永久代(PermGen)。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)

1.1、堆

1.1.1、什么是堆

这里所说的堆要区别与数据结构中的堆(完全二叉树),这里所说的堆是JVM所管理的最大的一块内存区域,是被所有线程共享。

1.1.2、堆的作用

Java作为一门面向对象的编程语言,其程序运行的过程,是对象实例从创建到消亡的过程。堆就是用来存储Java对象和数组的内存区域。

1.1.3、堆的特点

  • 堆内存被所有线程共享;
  • 堆在JVM启动时就被创建了;
  • 垃圾回收就是回收此区域的内存;
  • new 出来的对象都存在堆中;
  • 因为堆的线程共享特点,在给对象和数组分配内存时需要加锁。

1.1.4、堆内存的分区

堆内存从组成上来说主要划分为新生代和老年代,其中新生代又分为Eden区、From Survivor区和To Survivor区。

  • 新生代(年轻代)
    • Eden区
      • 对象会优先在新生代 Eden 区中进行分配,当 Eden 区空间不足时,虚拟机会使用复制算法发起一次 Minor GC(Young GC),清除掉垃圾对象。之后,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(From 区内存不足时,直接进入 Old 区)
    • Survivor区:From Survivor区和To Survivor区
      • Survivor 区相当于是 Eden 区和 Old 区的一个缓冲区。如果没有Survivor 区域,Old区将很快被填满,就会触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。
      • Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(To 区内存不足时,直接进入 Old 区)。
      • Survivor 分为2个区是为了解决内存碎片化的问题。Minor GC 执行后,Eden 区会清空,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。这时候JVM要使用标记清除算法去清除垃圾对象,而标记清除算法最大的问题就是内存碎片,由于在Eden区中有很多对象是“朝生夕死”的,所以必然会让内存产生严重的碎片化。Survivor 有2个区域,每次 Minor GC时,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,再将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。这样一来,总有一个Survivor区域是空闲的。这样就解决了内存碎片的问题。
  • 老年代
    • Old区据着2/3的堆内存空间,当对象从新生代中存活下来,就会被拷贝到这里。Major GC 会清理Old区中的对象,每次Major GC 都会触发“Stop-The-World”。内存越大,执行的时间也就越长。由于老年代中对象存活率很高,采用复制算法效率很低,所以老年代垃圾收集采用的是标记整理算法。

1.1.5、堆内存的分配与回收

1.1.5.1、堆内存分配策略

分配策略:

  1. 对象的内存分配,大方向上讲,就是在堆上分配;
  2. 如果启动了本地线程分配缓冲TLAB,将按线程优先在TLAB上分配;
  3. 对象主要分配在新生代的Eden区,少数情况下也可能直接分配在老年代(大对象直接进入老年代);
  4. 大对象直接进入老年代;
  5. 长期存活的对象将进入老年代;
1.1.5.2、堆内存申请

分配过程:

  1. 对于新创建的对象,JVM会试图为该Java对象在Eden中初始化一块内存区域;
  2. 当Eden空间足够时,内存申请结束。否则到下一步
  3. 当Eden空间不足时,JVM会发起一次Minor GC,试图释放在Eden中所有不活跃的对象,经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。
    1. 说明1:对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置;
    2. 说明2:当发生 Minor GC 时,Eden 区和 from 指向的 Survivor 区中的存活对象会被复制到 to 指向的 Survivor 区中,然后交换 from 和 to 指针,以保证下一次 Minor GC 时,to 指向的 Survivor 区还是空的。
  4. 当老年代空间不够时,JVM会在老年代进行一次完全的垃圾收集(Major GC/Full GC);
  5. 完全垃圾收集后,若Survivor及老年代仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory”错误。
1.1.5.3、堆内存回收

主要依靠JVM的垃圾回收机制,下文阐述

1.1.6、堆内存的配置参数整理

  • -Xms:指定JVM的初始堆内存,默认是物理内存的1/64;
  • -Xmx:指定JVM的最大堆内存,默认是物理内存的1/4。
    • 默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
    • 空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。
    • 因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。
    • 说明:如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try…catch捕捉。

PS:非堆内存的分配:

-XX:PermSize:设置非堆内存初始值,默认是物理内存的 1/64;

XX:MaxPermSize:设置最大非堆内存的大小,默认是物理内存的 1/4.

1.2、方法区

1.2.1、什么是方法区

方法区与Java堆一样是所有线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。该区域也是线程共享的。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap非堆),目的是与Java堆区分开。与方法区联系密切的一个概念是"永久代"。

  • 永久代(Permanent Generation),可以理解为 JDK 1.8 之前 HotSpot 虚拟机对《Java 虚拟机规范》中"方法区"的实现;
  • JDK1.7中,将1.6中永久代的字符串常量池和静态变量等移到了堆中;
  • JDK1.8中,则完全废弃了"永久代",类型信息改用了在本地内存中实现的"元空间(Metaspace)",将 JDK 1.7 中永久代剩余的部分(主要是类型信息)移到了元空间。

1.2.2、方法区的作用

用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

1.2.3、方法区的特点

  • 永久代;
    • 方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,我们把方法区称为永久代。
  • 线程共享;
    • 方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的。整个虚拟机中只有一个方法区。
  • 内存回收效率低 ;
  • Java虚拟机规范对方法区的要求比较宽松;
  • 运行时常量池是方法区的一部分,用于存放编译期生成的字面常量和符号引用
  • 根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

1.2.4、方法区的相关参数

  • -XX:PermSize=64MB 最小尺寸,初始分配
  • -XX:MaxPermSize=256MB 最大允许分配尺寸,按需分配
  • XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled 设置垃圾不回收默认大小
  • -server选项下默认MaxPermSize为64m
  • -client选项下默认MaxPermSize为32m

1.2、JVM虚拟机栈

1.2.1、什么是JVM虚拟机栈

Java虚拟机栈是描述Java方法运行过程的内存模型。
Java虚拟机栈会为每一个即将运行的Java方法创建一块叫做“栈帧”的区域,这块区域用于存储该方法在运行过程中所需要的一些信息,这些信息包括:

  1. 局部变量表
    1. 存放基本数据类型变量
    2. 引用类型的变量
    3. returnAddress类型的变量
    4. 局部变量表所需内存空间在编译期完成分配,当进入一个方法时,该方法需要在栈帧中分配多大的局部变量空间是完全确定的,运行期间不会改变其大小
  2. 操作数栈
    1. 出栈和入栈
  3. 动态链接
    1. 常量池中查找父类引用指向子类对象的真实实例对象。
  4. 方法出口信息
    1. eturn、try catch 等控制程序

具体执行和使用过程:

当一个方法即将被运行时,Java虚拟机栈首先会在Java虚拟机栈中为该方法创建一块“栈帧”,栈帧中包含局部变量表、操作数栈、动态链接、方法出口信息等。当方法在运行过程中需要创建局部变量时,就将局部变量的值存入栈帧的局部变量表中。
当这个方法执行完毕后,这个方法所对应的栈帧将会出栈,并释放内存空间。

1.2.2、JVM虚拟机栈特点

  • 线程私有;
  • 局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建;
  • 生命周期与线程相同;
  • 两类异常
    • 线程请求的栈深度大于虚拟机所允许的深度时抛出 StackOverflowError 异常;
    • 栈扩展时无法申请到足够的内存时抛出 OutOfMemoryError 异常。

1.3、本地方法栈

  1. 本地方法栈(Native Method Stacks)与 Java 虚拟机栈作用类似,只不过是Native 方法执行的线程内存模型。
  2. 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
  3. 异常类型与 Java 虚拟机栈相同。

1.4、程序计数器

1.4.1、什么是程序计数器

程序计数器是一块较小的内存空间,可以把它看作当前线程正在执行的字节码的行号指示器。也就是说,程序计数器里面记录的是当前线程正在执行的那一条字节码指令的地址。
**注:**但是,如果当前线程正在执行的是一个本地方法,那么此时程序计数器为空。

1.4.2、程序计数器的作用

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

1.4.3、程序计数器的特点

  • 线程私有;
  • 命周期随着线程的创建而创建,随着线程的结束而死亡;
  • 占用内存空间较小;
  • 若线程执行的是 Java 方法,记录的是虚拟机字节码指令地址;
  • 若执行的是本地(Native)方法,则为空(Undefined);
  • 该区域是唯一一个在《Java 虚拟机规范》中规定无任何 OutOfMemoryError 的区域。

1.5、直接内存

  1. 直接内存(Direct Memory)并非虚拟机运行时数据区的一部分,也非《Java 虚拟机规范》定义的内存区域。
  2. 直接内存的大小不受Java虚拟机控制,但当内存不足时就会抛出OOM(OutOfMemoryError)异常。
  3. 在NIO中引入了一种基于通道和缓冲的IO方式。它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象直接操作该内存,而无需先将外面内存中的数据复制到堆中再操作,从而提升了数据操作的效率。

2、类加载的全过程

类加载全过程分析

3、在JVM中Java对象创建的全过程

Java对象创建过程分析

4、垃圾回收机制

TODO

发布了11 篇原创文章 · 获赞 5 · 访问量 6886

猜你喜欢

转载自blog.csdn.net/C1248770189/article/details/105017844