前言
能否真正理解JVM的底层实现原理是进阶Java技术的必由之路,Java通过JVM虚拟机的设计使得Java的延拓性更好,平台无关性是其同时兼顾移动端和服务器端开发的重要特性。在本篇文章中,荔枝将会仔细梳理JVM的体系架构和理论知识,希望能帮助到有需要的小伙伴~~~
文章目录
前言
一、JVM的基本概念
1.1 两种线程和生命周期
1.2 JVM的结构体系
类装载器
运行时数据区
执行引擎
1.3 平台无关性的理解
二、JVM的内存结构
2.1 方法区
2.2 Java堆
2.3 VM Stack
2.4 本地方法栈
2.5 程序计数器
2.6 本地方法接口JNI
三、常量池
3.1 编译时常量池
3.2 运行时常量池
3.3 字符串常量池
3.4 常量池的大小限制
总结
一、JVM的基本概念
JVM(Java Virtual Machine)又被称为Java虚拟机,是Java程序的运行环境。我们知道在Java中程序文件.java会被编译器编译成字节码文件(.class文件),并在JVM中利用解释器解释成机器码执行。JVM是Java实现平台无关性的最关键的组件,只要对应的操作系统中有对应的JVM版本,就可以运行Java的字节码文件,实现一次编译、到处运行的场景。荔枝在看了一些资料后发现大多数的书籍和博客都是以HotSpot虚拟机来介绍的,这里荔枝也就随波逐流地来梳理一下:
1.1 两种线程和生命周期
JVM是基于线程的,是线程对应的而不是线程共享的。JVM中的线程主要分为两种:守护线程和普通线程。其中守护线程是JVM自己使用的线程,比如垃圾回收机制(GC)就是一个守护线程;而普通线程就是一般的Java线程,只要有线程在执行那么JVM就不会停止。
那么什么时候虚拟机会结束进程呢?
JVM结束生命周期的四种情况
- 程序正常执行完成后,无普通线程执行
- 程序执行出现异常报错
- 执行了System.exit()方法
- 操作系统出错而导致JVM虚拟机进程终结
1.2 JVM的结构体系
粗略来分,JVM的内部体系结构分为三部分:类装载器、运行时的数据区和执行引擎。
类装载器
类装载器又被称为类加载子系统,在JVM中提供了一套类加载机制。即JVM通过类加载器来将字节码文件加载进入内存空间,获得程序中的类和接口并赋予唯一名称。JVM的类加载器一般来说分为三种:启动类加载器、拓展类加载器和拓展类加载器,通过双亲委派模型来进行类的加载,关于双亲委派模型和类加载机制,这里我们只需要了解有这么一个概念,在荔枝的下一篇文章会详细梳理噢~
除了Java默认提供的三个加载器之外,我们还可以根据自身需求自定义ClassLoader,自定义的类加载器必须继承自 java.lang.ClassLoader 类。
运行时数据区
运行时的数据区又称内存空间,主要包括了方法区、堆、虚拟机栈、本地方法栈和程序计数器这五部分。Java程序被编译成字节码文件后经过JVM的类加载机制就可以得到开发者定义的相关的类、接口、变量信息和代码缓存,并将这些数据注入内存空间进行储存,之后将程序指令编译成机器指令并交给执行引擎即可。
执行引擎
执行引擎的主要职责,就是类加载后得到的程序指令集翻译成硬件所支持的指令集格式,然后执行。 在不同的虚拟机实现中,可能会有两种的执行方式:解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码)。虚拟机可以按自身的需求,采用一种或同时采用多种组合的方式来实现执行引擎。但无论内部怎么实现,都要遵循输入的是字节码文件、处理过程是等效字节码解析过程、输出的是执行结果这个JVM规范要求。
1.3 平台无关性的理解
我们大致了解了JVM的运行原理和架构体系,但对于JVM的平台无关性这一性质还不是特别明确。我们知道Java在网页端和移动端的开发应用十分广泛,这正是其平台无关性的一个具体的体现。简单理解其实JVM作为一个平台对底层的硬件和上层的类对象和API做了隔离,就相当于套接件,使得我们仅需要根据不同的操作系统找到其对应的JVM版本即可完成程序迁移使用,有点开箱即用的感觉哈哈哈。
二、JVM的内存结构
JVM内存结构中不是所有的数据区都是线程隔离的,其中程序计数器、本地方法栈和VM Stack是线程隔离的;而堆、方法区和本地内存是线程共享的。
2.1 方法区
存储类的元数据信息、静态变量、即时编译器编译后的代码缓存等。在HotSpot JVM中,方法区被称为"永久代",在Java 8及之后版本中,它被改为"元空间"(Metaspace)。相比于永久代,元空间的好处是会在运行时根据需要动态调整,只要没有超过当前进程可用的内存上限就不会出现溢出的问题,方法区可以被垃圾回收。
元数据信息
包括类的全名、父类名称、类或接口的标识、类型修饰符、所有父接口全名的列表、类型的字段信息、类型的方法信息、静态类信息、类的引用和常量池的信息。
2.2 Java堆
存储Java对象的内存区域。所有通过new关键字创建的对象都在堆中分配内存。堆区负责存放对象实例,当Java创建一个类的实例对象或者数组时,都会在堆中为新的对象分配内存。需要注意的是:堆空间和方法区是线程共享的,堆的存取是先进先出的,堆内存的大小可以动态分配,并且堆可以由GC机制进行资源回收。
2.3 VM Stack
在Java栈中只保存基础数据类型和对象的引用,注意对象是保存在堆区中的,这里保存的是对象的引用。栈中创建的基本类型数据在超出其作用域之后就会被自动释放掉,不受GC机制的回收管理。当一个线程创建运行的时候,与之对应的栈就创建了,每个栈都是线程隔离滴。每个线程都会建立一个栈,每个栈又包含了若干个栈帧,每个栈帧对应着每个方法的每次调用,栈帧包含了三个部分:局部变量区、操作数栈区和运行环境区。
注意:像String、Integer、Byte、Short、Long、Boolean等等包装类型,它们是存放于堆中的。
2.4 本地方法栈
本地方法栈与虚拟机栈类似,但用于执行本地方法(Native方法),存储的也是本地方法的局部变量表,本地方法的操作数栈等信息。本地方法栈中的数据同样也不会被JVM GC管理,而是在其超出作用域之后自动释放。本地方法栈是在程序调用或JVM调用本地方法接口(Native)时候启用。 本地方法都不是使用Java语言编写的,它们可能由C或其他语言编写,本地方法也不由JVM去运行,所以本地方法的运行不受JVM管理。
注意:HotSpot VM将本地方法栈和JVM栈合并了,本地方法栈也会在深度溢出或扩展失败的时候会分别抛出StackOverflowError 和 OutOfMemoryError 异常。
2.5 程序计数器
程序计数器(Program Counter Register):记录当前线程执行的字节码指令地址,线程切换时,程序计数器也会随之切换,每条线程都会有一个独立的程序计数器。当线程正在执行一个Java方法,程序计数器记录的是正在执行的JVM字节码指令的地址。如果正在执行的是一个Natvie(本地方法),那么这个计数器的值则为空(Underfined)。
2.6 本地方法接口JNI
JNI是Java Native interface的缩写,它提供了若干的API实现了Java和其他语言的通信,对于一下其它语言的库或者是函数功能的移植提供了一定的便捷度。但是需要注意的是,一旦使用了JNI,相当于主动放弃了Java的平台无关性这一特性,同时线程也不再是绝对安全的。
三、常量池
3.1 编译时常量池
Java代码在经过编译器后,会生成一个Class文件,在编译阶段生成的这个常量池储存在Class文件里,它主要存放着 字面量、符号引用等信息,在JVM把Class文件加载完成后,编译时常量池里的数据会存放到运行时常量池中。
3.2 运行时常量池
运行时常量池是在JVM运行时生成的常量池,同时作为方法区(Method Area)的一部分,运行时常量池中存储的是基本类型的数据和对象的引用,与.class文件中的编译时常量池相对应。一般来说,JVM在对字节码文件进行类加载后,会把字节码文件内容里常量池的数据会放入运行时常量池。每一个加载好的Class对象里都会有一个运行时常量池。
3.3 字符串常量池
字符串常量池是常量池的一部分,用于存放字符串字面量。在JDK7之后,字符串常量池从方法区迁移到了堆区,它的底层实现可以理解为是一个HashTable。Java虚拟机中只会存在一份字符串常量池。字符串常量池里,存放的数据可以是引用也可以是对象实例本身。字符串常量池同时也具备运行时常量池动态性的特征,它支持运行期间将新的常量放入池中。
注意:字符串常量池中的字符串是不可变的,这意味着一旦创建了一个字符串对象,它的值就不能被修改。
3.4 常量池的大小限制
- 在JVM规范中,常量池的大小是一个ushort类型(无符号16位整数),因此常量池的最大索引是65535。
- 当常量池中的项超过该限制时,JVM会抛出"Constant pool is too large"的错误。
总结
在这篇文章中,荔枝主要梳理了有关JVM体系结构的知识,明确JVM的组成和基本运行原理。对于类加载机制中的双亲委派模型和GC垃圾回收机制荔枝也会在后续的文章中详细的梳理和总结。希望能帮助到有需要的小伙伴哈哈哈~~~
今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~