“5年的学习,我对 JVM 有了更深入的理解”阿里P7架构师分享
在学习Java虚拟机之前,我们要先明白什么是虚拟机!
什么是虚拟机(JVM)?
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
虚拟机(JVM)有什么用?
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用模式Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。--百度百科
初步了解虚拟机(JVM)
这篇文章解释了Java 虚拟机(JVM)的内部架构。下图显示了遵守 Java SE 7 规范的典型的 JVM 核心内部组件。
深入浅出JVM虚拟机.PDF版本
上图显示的组件分两个章节解释。第一章讨论针对每个线程创建的组件,第二章节讨论了线程无关组件。
- 线程
- JVM 系统线程
- 每个线程相关的
- 程序计数器
- 栈
- 本地栈
- 栈限制
- 栈帧
- 局部变量数组
- 操作数栈
- 动态链接
- 线程共享
- 堆
- 内存管理
- 非堆内存
- 即时编译
- 方法区
- 类文件结构
- 类加载器
- 更快的类加载
- 方法区在哪里
- 类加载器参考
- 运行时常量池
- 异常表
- 符号表
- Interned 字符串
线程
这里所说的线程指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。run() 返回时,被处理未捕获异常,原生线程将确认由于它的结束是否要终止 JVM 进程(比如这个线程是最后一个非守护线程)。当线程结束时,会释放原生线程和 Java 线程的所有资源。
JVM 系统线程
如果使用 jconsole 或者其它调试器,你会看到很多线程在后台运行。这些后台线程与触发 public static void main(String[]) 函数的主线程以及主线程创建的其他线程一起运行。Hotspot JVM 后台运行的系统线程主要有下面几个:
每个运行的线程都包含下面这些组件:
程序计数器(PC)
PC 指当前指令(或操作码)的地址,本地指令除外。如果当前方法是 native 方法,那么PC 的值为 undefined。所有的 CPU 都有一个 PC,典型状态下,每执行一条指令 PC 都会自增,因此 PC 存储了指向下一条要被执行的指令地址。JVM 用 PC 来跟踪指令执行的位置,PC 将实际上是指向方法区(Method Area)的一个内存地址。
栈(Stack)
每个线程拥有自己的栈,栈包含每个方法执行的栈帧。栈是一个后进先出(LIFO)的数据结构,因此当前执行的方法在栈的顶部。每次方法调用时,一个新的栈帧创建并压栈到栈顶。当方法正常返回或抛出未捕获的异常时,栈帧就会出栈。除了栈帧的压栈和出栈,栈不能被直接操作。所以可以在堆上分配栈帧,并且不需要连续内存。
Native栈
并非所有的 JVM 实现都支持本地(native)方法,那些提供支持的 JVM 一般都会为每个线程创建本地方法栈。如果 JVM 用 C-linkage 模型实现 JNI(Java Native Invocation),那么本地栈就是一个 C 的栈。在这种情况下,本地方法栈的参数顺序、返回值和典型的 C 程序相同。本地方法一般来说可以(依赖 JVM 的实现)反过来调用 JVM 中的 Java 方法。这种 native 方法调用 Java 会发生在栈(一般是 Java 栈)上;线程将离开本地方法栈,并在 Java 栈上开辟一个新的栈帧。
栈的限制
栈可以是动态分配也可以固定大小。如果线程请求一个超过允许范围的空间,就会抛出一个StackOverflowError。如果线程需要一个新的栈帧,但是没有足够的内存可以分配,就会抛出一个 OutOfMemoryError。
栈帧(Frame)
每次方法调用都会新建一个新的栈帧并把它压栈到栈顶。当方法正常返回或者调用过程中抛出未捕获的异常时,栈帧将出栈。更多关于异常处理的细节,可以参考下面的异常信息表章节。
每个栈帧包含:
- 局部变量数组
- 返回值
- 操作数栈
- 类当前方法的运行时常量池引用
局部变量数组
局部变量数组包含了方法执行过程中的所有变量,包括 this 引用、所有方法参数、其他局部变量。对于类方法(也就是静态方法),方法参数从下标 0 开始,对于对象方法,位置0保留为 this。
有下面这些局部变量:
- boolean
- byte
- char
- long
- short
- int
- float
- double
- reference
- returnAddress
除了 long 和 double 类型以外,所有的变量类型都占用局部变量数组的一个位置。long 和 double 需要占用局部变量数组两个连续的位置,因为它们是 64 位双精度,其它类型都是 32 位单精度。
操作数栈
操作数栈在执行字节码指令过程中被用到,这种方式类似于原生 CPU 寄存器。大部分 JVM 字节码把时间花费在操作数栈的操作上:入栈、出栈、复制、交换、产生消费变量的操作。因此,局部变量数组和操作数栈之间的交换变量指令操作通过字节码频繁执行。比如,一个简单的变量初始化语句将产生两条跟操作数栈交互的字节码。
int i;
被编译成下面的字节码:
0: iconst_0 // Push 0 to top of the operand stack 1: istore_1 // Pop value from top of operand stack and store as local variable 1
更多关于局部变量数组、操作数栈和运行时常量池之间交互的详细信息,可以在类文件结构部分找到。
动态链接
每个栈帧都有一个运行时常量池的引用。这个引用指向栈帧当前运行方法所在类的常量池。通过这个引用支持动态链接(dynamic linking)。
C/C++ 代码一般被编译成对象文件,然后多个对象文件被链接到一起产生可执行文件或者 dll。在链接阶段,每个对象文件的符号引用被替换成了最终执行文件的相对偏移内存地址。在 Java中,链接阶段是运行时动态完成的。
当 Java 类文件编译时,所有变量和方法的引用都被当做符号引用存储在这个类的常量池中。符号引用是一个逻辑引用,实际上并不指向物理内存地址。JVM 可以选择符号引用解析的时机,一种是当类文件加载并校验通过后,这种解析方式被称为饥饿方式。另外一种是符号引用在第一次使用的时候被解析,这种解析方式称为惰性方式。无论如何 ,JVM 必须要在第一次使用符号引用时完成解析并抛出可能发生的解析错误。绑定是将对象域、方法、类的符号引用替换为直接引用的过程。绑定只会发生一次。一旦绑定,符号引用会被完全替换。如果一个类的符号引用还没有被解析,那么就会载入这个类。每个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联的)偏移量。
线程间共享堆
堆被用来在运行时分配类实例、数组。不能在栈上存储数组和对象。因为栈帧被设计为创建以后无法调整大小。栈帧只存储指向堆中对象或数组的引用。与局部变量数组(每个栈帧中的)中的原始类型和引用类型不同,对象总是存储在堆上以便在方法结束时不会被移除。对象只能由垃圾回收器移除。
为了支持垃圾回收机制,堆被分为了下面三个区域:
- 新生代
- 经常被分为 Eden 和 Survivor
- 老年代
- 永久代
内存管理
对象和数组永远不会显式回收,而是由垃圾回收器自动回收。通常,过程是这样的:
- 新的对象和数组被创建并放入老年代。
- Minor垃圾回收将发生在新生代。依旧存活的对象将从 eden 区移到 survivor 区。
- Major垃圾回收一般会导致应用进程暂停,它将在三个区内移动对象。仍然存活的对象将被从新生代移动到老年代。
- 每次进行老年代回收时也会进行永久代回收。它们之中任何一个变满时,都会进行回收。
非堆内存
非堆内存指的是那些逻辑上属于 JVM 一部分对象,但实际上不在堆上创建。
非堆内存包括:
- 永久代,包括:
- 方法区
- 驻留字符串(interned strings)
- 代码缓存(Code Cache):用于编译和存储那些被 JIT 编译器编译成原生代码的方法。
即时编译(JIT)
Java 字节码是解释执行的,但是没有直接在 JVM 宿主执行原生代码快。为了提高性能,Oracle Hotspot 虚拟机会找到执行最频繁的字节码片段并把它们编译成原生机器码。编译出的原生机器码被存储在非堆内存的代码缓存中。通过这种方法,Hotspot 虚拟机将权衡下面两种时间消耗:将字节码编译成本地代码需要的额外时间和解释执行字节码消耗更多的时间。
方法区
方法区存储了每个类的信息,比如:
- Classloader 引用
- 运行时常量池
- 数值型常量
- 字段引用
- 方法引用
- 属性
- 字段数据
- 针对每个字段的信息
- 字段名
- 类型
- 修饰符
- 属性(Attribute)
- 方法数据
- 每个方法
- 方法名
- 返回值类型
- 参数类型(按顺序)
- 修饰符
- 属性
- 方法代码
- 每个方法
- 字节码
- 操作数栈大小
- 局部变量大小
- 局部变量表
- 异常表
- 每个异常处理器
- 开始点
- 结束点
- 异常处理代码的程序计数器(PC)偏移量
- 被捕获的异常类对应的常量池下标
所有线程共享同一个方法区,因此访问方法区数据的和动态链接的进程必须线程安全。如果两个线程试图访问一个还未加载的类的字段或方法,必须只加载一次,而且两个线程必须等它加载完毕才能继续执行。
今天为大家带来的是深入浅出JVM虚拟机.PDF版本
特点
围绕内存管理、执行子系统、编程编译与优化、高效并发等核心内容对JVM进行全面而深入的分析,深刻揭示JVM的工作原理;注重实现,以解决实践中的疑难问题为首要目的,包含大量经典案例和最佳实践。
每一个程序员都应该学习一下
现在免费送给这篇文章的读者朋友,关注+转发+收藏后私信【架构资料】即可免费获取!
深入浅出JVM虚拟机
深入浅出JVM虚拟机
深入浅出JVM虚拟机
我们都知道,Java程序在执行前首先会被编译成字节码文件,然后再由Java虚拟机执行这些字节码文件从而使得Java程序得以执行。事实上,在程序执行过程中,内存的使用和管理一直是值得关注的问题。Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些数据区域都有各自的用途,以及创建和销毁的时间,并且它们可以分为两种类型:线程共享的方法区和堆,线程私有的虚拟机栈、本地方法栈和程序计数器
曾经我也对 JVM 感到很头痛,完全搞不懂应该如何入门 JVM 的学习。但经过了 5 年的学习,我对 JVM 有了更深入的理解。虽然还达不到精通源码的程度,但是对 JVM 各个知识点的理解和联系都形成了自己的体系。
最后:
深入浅出JVM虚拟机.PDF版本是可以免费送给想要学习的小伙伴!
关注+转发+收藏后私信【架构资料】即可免费获取!