Java工程师的进阶之路 JVM篇(一)

·  阅读 887
Java工程师的进阶之路 JVM篇(一)

白菜Java自习室 涵盖核心知识

Java工程师的进阶之路 JVM篇(一)
Java工程师的进阶之路 JVM篇(二)
Java工程师的进阶之路 JVM篇(三)

1. JVM 是什么

JVM 它是 Java Virtual Machine 的缩写,主要是通过在实际计算机模仿各种计算机功能来实现的,组成部分包括堆、方法区、栈、本地方法栈、程序计算器等,其中方法回收堆和方法区是共享区,也就是谁都可以使用,而栈和程序计算器、本地方法栈区是归 JVM 的。Java 能够被称为“一次编译,到处运行”的原因就是 Java 屏蔽了很多的操作系统平台相关信息,使得 Java 只需要生成在 JVM 虚拟机运行的目标代码也就是所说的字节码,就可以在多种平台运行。

2. JVM、JRE、JDK 三者的关系

JDK 是 Java 程序员常用的开发包、目的就是用来编译和调试 Java 程序的。JRE 是指 Java 运行环境,也就是我们的写好的程序必须在 JRE 才能够运行。JVM 是 Java 虚拟机的缩写,是指负责将字节码解释成为特定的机器码进行运行,值得注意的是在运行过程中,Java 源程序需要通过编译器编译为 .class 文件,否则 JVM 不认识。

3. Java 运行时数据区域

3.1 程序计数器

内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成

如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

3.2 虚拟机栈

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧 (Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

局部变量表:存放了编译期可知的各种基本类型 (boolean、byte、char、short、int、float、long、double)、对象引用 (reference 类型) 和 returnAddress 类型(指向了一条字节码指令的地址)。
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

3.3 本地方法栈

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

3.4 Java 堆

对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

3.5 方法区

属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

3.6 运行时常量池

属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。

3.7 直接内存

非虚拟机运行时数据区的部分

在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道 (Channel) 和缓存 (Buffer) 的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。

OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。

4. HotSpot 虚拟机对象探秘

4.1 对象的创建

  1. 遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载。
  2. 类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)。
  3. 前面讲的每个线程在堆中都会有私有的分配缓冲区 (TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。
  4. 内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
  5. 执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成。

4.2 对象的内存布局

在 HotSpot 虚拟机中,分为 3 块区域:对象头 (Header)、实例数据 (Instance Data) 和对齐填充 (Padding)

对象头 (Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。

实例数据 (Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。

对齐填充 (Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。

4.3 对象的访问定位

使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。

通过句柄访问

Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址。详情见图。

使用直接指针访问

reference 中直接存储对象地址

使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动 (GC) 是只改变实例数据指针地址,reference 自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。

Java工程师的进阶之路 JVM篇(一)
Java工程师的进阶之路 JVM篇(二)
Java工程师的进阶之路 JVM篇(三)

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改