Java虚拟机
JVM的概念
百度百科:java虚拟机
什么是虚拟机?
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。
为什么要有JVM?
- Java设计的初衷是使要建的能在任何平台上运行的程序不需要再在每个单独的平台上由程序员进行重写或重编译。
- Java虚拟机使这个愿望变为可能,因为它能知道每条指令的长度和平台的其他特性。
- JVM的设计目标是提供一个基于抽象规格描述的计算机模型,为解释程序开发人员提供的任何系统上运行。
什么是java虚拟机?
JVM全称Java Virtual Machine
- Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
- 运行所有Java程序的抽象计算机,是Java语言的运行环境
- Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。
- 一旦一个Java虚拟机在给定的平台上运行,任何Java程序(编译之后的程序,称作字节码)都能在这个平台上运行。“一次编写,到处运行”
- Java虚拟机(JVM)可以以一次一条指令的方式来解释字节码(把它映射到实际的处理器指令),或者字节码也可以由实际处理器中称作just-in-time的编译器进行进一步的编译
白话文理解一下:
想象一下,您写了一篇非常特别的文章(Java程序),但是这篇文章需要用一种特殊的“万能语言”(字节码)来书写,这样它才能在任何地方被理解。然而,要读懂这种“万能语言”,人们需要一个特殊的翻译机(JVM)。
这个翻译机不仅能够读懂您的文章,还能把它转换成当地语言(机器指令),以便任何人(任何操作系统和硬件)都能理解。
现在,如果每次有人想读您的文章,都需要自己造一台这样的翻译机,那将是非常麻烦且耗时的。这就是为什么我们需要JVM——一个通用的翻译机,它可以安装在各种不同的设备上。一旦设备上有了这个翻译机,无论何时何地,只要把您的文章放进去,它就能立刻翻译并让大家读懂。
所以,别人想要运行您写的Java程序时,并不需要重新制造一台新的翻译机,只需要确保他们的设备上已经安装了合适的翻译机(即对应版本的JVM)。这样一来,他们就可以轻松运行您的程序,而无需担心底层的细节问题。
总结来说,JVM就像是一个专为Java程序设计的“虚拟计算机”。它允许您编写的程序在任何安装了适当版本JVM的设备上运行,极大地简化了跨平台应用的开发和部署过程。通过这种方式,Java实现了“编写一次,到处运行”的理念。
JVM的组成
类加载器(ClassLoader):
负责加载.class文件中的字节码到内存中,并将这些字节码转换为可以由JVM执行的形式。
类加载器有三个主要类型:
- 启动类加载器(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 应用类加载器(Application ClassLoader)
运行时数据区(Runtime Data Area):
这部分包括了多个关键的数据区域,用于存储程序运行期间需要的数据:
- 方法区(Method Area):存储已被虚拟机加载的类信息、常量、静态变量等。
- 堆(Heap):存放对象实例以及数组,是垃圾回收的主要区域。
- 虚拟机栈(VM Stack):每个线程在创建时都会创建一个私有的栈,用来存储栈帧,栈帧中包含了局部变量表、操作数栈、动态链接等信息。
- 本地方法栈(Native Method Stack):与虚拟机栈类似,但主要是为执行本地方法服务的。
- 程序计数器(Program Counter Register):每个线程都有自己的程序计数器,指向当前线程正在执行的字节码指令地址。
执行引擎(Execution Engine):
执行引擎负责解释或编译字节码并执行它。主要包括以下几部分:
- 解释器(Interpreter):直接解释字节码并执行,虽然速度较慢但能快速启动。
- 即时编译器(Just-In-Time Compiler, JIT):将经常执行的字节码编译成本地机器代码以提高执行效率。
- 垃圾收集器(Garbage Collector, GC):自动管理内存,回收不再使用的对象所占用的内存空间。
本地接口库(Native Interface Library):
提供了一种机制,允许Java代码调用C/C++等其他语言编写的本地库函数,这通过JNI(Java Native Interface)实现。
本地方法库(Native Method Library):
包含了由本地代码(如C/C++)实现的方法库,这些方法可以通过JNI被Java程序调用。
运行时数据区
JVM再执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
分类:
程序计数器(Program Counter Register):
- 每个线程都有一个独立的程序计数器。
- 存储当前线程正在执行的字节码指令的地址。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地方法(Native Method),则计数器的值为空(Undefined)。
虚拟机栈(Java Virtual Machine Stacks):
存储方法调用过程中的局部变量、操作数栈、动态链接和方法出口信息。
每个线程在创建时都会创建一个私有的栈。
栈由多个栈帧(Frame)组成,每个栈帧对应一个方法调用。
- 局部变量表(Local Variable Table):存储方法参数和方法内部定义的局部变量。包括各种基本数据类型、对象引用和返回地址类型。
- 操作数栈(Operand Stack):用于存储计算过程中需要的操作数和结果。Java虚拟机的指令集大多通过操作数栈来进行计算。
- 动态链接(Dynamic Linking):指向运行时常量池中的符号引用,这些引用在类加载阶段解析为直接引用后,可以通过这些引用访问类或接口的方法和字段。
- 方法出口信息(Return Address):存储方法返回地址,即方法退出后应该返回到的位置。
堆(Heap):
- 堆是所有线程共享的一块内存区域,主要用于存储对象实例和数组。
- 堆被划分为新生代(Young Generation)、老年代(Old Generation)等不同区域,以实现高效的垃圾回收机制。
- 新生代通常进一步分为Eden区和两个Survivor区(From和To),用于管理新创建的对象。
注意:
在早期的Java虚拟机(JVM)实现中,存在一个被称为“永久代”(Permanent Generation,简称PermGen)的区域,它用于存储类的元数据。然而,从Java 8开始,JVM的设计发生了变化,永久代被移除,并引入了一个新的区域叫做“元空间”(Metaspace)
永久代是堆的一部分,与永久代不同,元空间并不属于堆内存,而是直接分配在本地内存(Native Memory)中。
方法区(Method Area):
方法区也是所有线程共享的一块内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。
- 类信息:包括类的名称、父类名称、实现的接口列表、字段描述符、方法描述符等。
- 常量池(Constant Pool):存储了类或接口中定义的常量,如字符串常量、整型常量等。
- 静态变量(Static Variables):存储类级别的变量,这些变量属于整个类而非某个实例。
- 即时编译器编译后的代码缓存:存储由JIT编译器生成的机器码。
本地方法栈(Native Method Stack):
- 与虚拟机栈类似,但专门为执行本地方法(Native Method)服务。
- 存储本地方法调用时的相关信息,具体结构和功能依赖于本地方法的具体实现。
类加载器
类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
Java中天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。
类的生命周期:
类的生命周期是指从类被加载到JVM中开始,直到最终被卸载的过程。这个过程可以分为几个主要阶段:加载(Loading)、链接(Linking)、初始化(Initialization)、使用(Using)和卸载(Unloading)。
类的加载阶段必须按顺序执行,而解析阶段不一定。
有四种情况必须立即对类进行“初始化”:
- 使用 new 关键字实例化对象
当你使用 new 关键字创建一个类的实例时,如果该类尚未被初始化,则会触发类的初始化。
- 调用类的静态方法
当你调用一个类的静态方法时,如果该类尚未被初始化,则会触发类的初始化。
- 访问或设置类的静态字段(非final)
当你访问或设置一个类的静态字段(且该字段不是 final 的)时,如果该类尚未被初始化,则会触发类的初始化。
- 使用反射机制
当你通过反射机制来操作类时,某些操作会触发类的初始化。例如,使用 Class.forName(String name) 方法加载类时,如果该类尚未被初始化,则会触发其初始化过程。
1. 加载(Loading)
目标:找到类的字节码文件,并将其加载到内存中。
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 创建类对象,在Java堆中生成一个代表这个类的java.lang.Class对象(实例化),作为方法区这些数据的访问入口。
2. 链接(Linking)
链接阶段又可以细分为三个子阶段:验证(Verification)、准备(Preparation)和解析(Resolution)。
2.1 验证(Verification)
目标:确保类的字节码是正确且安全的。
- 文件格式验证:验证字节码是否符合Java虚拟机规范的文件格式要求。
- 元数据验证:确保类的结构是正确的,例如方法是否有返回值、继承关系是否合法等。
- 字节码验证:防止恶意代码对系统的攻击,确保字节码在运行时是安全的。
- .符号引用验证:确保符号引用能够正确解析为直接引用。(解析阶段发生)
2.2 准备(Preparation)
目标:为类的静态变量分配内存,并设置默认初始值。
- 静态变量初始化:在这个阶段,所有静态变量都会被分配内存空间,并赋予默认值(例如 int 类型的默认值为 0,Object 类型的默认值为 null)。
2.3 解析(Resolution)
目标:将类、接口、字段和方法的符号引用转换为直接引用。
- 符号引用:在编译时生成的对其他类、接口、字段或方法的引用。
- 直接引用:在运行时解析这些符号引用,找到它们实际的内存地址。
3. 初始化(Initialization)
目标:执行类的静态初始化块和静态变量的赋值操作。
- 静态初始化块:如果类中有静态初始化块(static {}),这些块会在初始化阶段被执行。
- 静态变量赋值:静态变量会被赋予程序员指定的初始值(而不是默认值)。
4. 使用(Using)
目标:当类被加载、链接和初始化后,就可以在程序中使用了。
- 实例化对象:通过 new 关键字创建类的实例。
- 调用方法:可以调用类的方法或访问类的字段。
5. 卸载(Unloading)
目标:当类不再被使用时,JVM会进行垃圾回收,释放相关的资源。
- 类卸载条件:只有当类加载器本身也被垃圾回收时,类才会被卸载。具体来说,当没有任何对象引用类加载器时,类加载器及其加载的所有类都可以被垃圾回收。
类生命周期的详细步骤
1.加载(Loading):
- 查找并读取 .class 文件。
- 创建 java.lang.Class 对象表示该类。
2.链接(Linking):
- 验证(Verification):检查字节码的正确性和安全性。
- 准备(Preparation):为静态变量分配内存并赋予默认值。
- 解析(Resolution):将符号引用解析为直接引用。
3.初始化(Initialization):
- 执行静态初始化块和静态变量的赋值操作。
4.使用(Using):
- 实例化对象并调用类的方法。
5.卸载(Unloading):
- 当类加载器不再被引用时,JVM可能会卸载类及其相关资源。
类加载器的层次:
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即时这两个类来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。
启动类加载器(Bootstrap ClassLoader)
- 这是最顶层的类加载器,由本地代码实现(通常是C/C++),不属于Java类层次结构的一部分。
- 负责加载核心Java库中的类,例如 rt.jar 中的类,这些类位于 $JAVA_HOME/jre/lib 目录下。
- 由于它是由本地代码实现的,因此无法通过Java代码直接访问或扩展。
扩展类加载器(Extension ClassLoader)
- 继承自 java.net.URLClassLoader,是 sun.misc.Launcher$ExtClassLoader 的实例。
- 负责加载位于 $JAVA_HOME/jre/lib/ext 目录下的扩展类库,以及任何配置在 java.ext.dirs 系统属性中的路径中的类库。
应用类加载器(Application ClassLoader)
- 继承自 java.net.URLClassLoader,是 sun.misc.Launcher$AppClassLoader 的实例。
- 负责加载应用程序类路径上的类,即 -classpath 或 CLASSPATH 环境变量指定的路径中的类。
自定义类加载器(Custom ClassLoader)
- 用户可以继承 java.lang.ClassLoader 来创建自定义类加载器,以满足特定需求,如从数据库、网络或其他非标准位置加载类。
双亲委派模型
类加载器使用双亲委派模型(Parent Delegation Model),其工作原理如下:
- 当一个类加载器收到加载某个类的请求时,首先不会自己去尝试加载这个类。
- 它会将该请求委派给它的父类加载器,直到启动类加载器。
- 如果父类加载器无法找到或加载该类,则子类加载器才会尝试自己加载该类。
- 如果所有父类加载器都无法加载该类,则最终会抛出 ClassNotFoundException 异常。
垃圾收集器
垃圾收集是什么?
是Java虚拟机(JVM)自动管理内存的一种机制,目的是自动释放不再使用的对象所占用的内存资源。
为什么要垃圾收集?
1.防止内存泄漏:
- 如果程序中存在不再使用的对象但仍然持有对这些对象的引用,那么这些对象将无法被释放,导致内存泄漏。内存泄漏会逐渐耗尽可用内存,最终导致应用程序崩溃或性能下降。
2.简化内存管理:
- 在没有垃圾回收的语言中,开发者需要手动管理内存分配和释放。这不仅增加了开发复杂度,还容易出错。通过垃圾回收,开发者可以专注于业务逻辑,而无需担心内存管理问题。
3.提高系统稳定性:
- 自动化的垃圾回收可以确保在适当的时候释放内存,从而减少因内存不足导致的应用崩溃或性能问题。
4.优化内存使用:
- 垃圾回收器可以通过压缩等方式重新组织堆内存,减少内存碎片化,提高内存利用率。
垃圾收集发生在哪里?
垃圾回收主要发生在堆内存(Heap Memory) 中,因为堆是存储对象实例的地方。具体来说:
- 年轻代(Young Generation):新创建的对象首先被分配到年轻代中的Eden区。当Eden区满时,会触发一次Minor GC(轻量级垃圾回收),清理掉不再使用的对象,并将存活的对象移动到Survivor区。
- 老年代(Old Generation):经过多次Minor GC后仍存活的对象会被晋升到老年代。老年代的空间较大,通常用于存储生命周期较长的对象。当老年代空间不足时,会触发Major GC或Full GC(全量垃圾回收)。
- 永久代/元空间(Permanent Generation/Metaspace):从Java 8开始,永久代被元空间取代。元空间用于存储类的元数据、方法信息等。虽然这部分不属于堆内存,但它也是垃圾回收的一部分,尤其是类卸载时。(逻辑上存在于堆,物理上不存在)
垃圾收集的流程
堆中几乎存放着Java世界中所有的对象实例,垃圾收集器在对堆进行回收前,第一件事就是要确定哪些对象还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。
1.判断对象是否存活
(1)引用计数算法
给对象中添加一个引用计数。每当有一个地方引用它,计数器就加1;当引用失效,计数器值就减1。任何时候,计数器值为0的对象就是不可能被使用的。
注意:Java语言中没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间的相互循环引用的问题。
(2)根搜索算法
在主流的商用程序语言中(Java 和 C#, 甚至包括古老的Lisp),都是采用根搜索算法(GC Roots Tracing)判定对象是否存活。
通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径被称为引用链,当一个对象到GC Roots没有任何的引用链相连(用图论的话来说就是从GC Roots到这个对象不可达),则证明此对象是不可用的。
无论是引用计数算法还是根搜索算法都离不开“引用”:
- 强引用:常规引用,对象不会被回收。
- 软引用:内存不足时可能被回收,用于缓存。
- 弱引用:对象无强引用时立即被回收,用于生命周期较短的对象。
- 虚引用:跟踪对象回收状态,用于清理操作。
2.垃圾收集算法
(1)标记-清除算法
原理
- 标记阶段:从根对象开始,递归地遍历所有可达的对象,并将这些对象标记为存活。
- 清除阶段:扫描整个堆内存,清理未被标记的对象,释放其占用的内存。
优点
- 实现简单,不需要额外的空间。
缺点
- 容易产生内存碎片,影响后续的大对象分配。
- 需要暂停应用程序(Stop-the-world),影响系统的响应时间。
适用场景
- 适用于对停顿时间要求不高、内存碎片化问题不严重的场景。
(2)复制算法
原理
- 将堆内存分为两个区域(Eden区和Survivor区),每次只使用其中一个区域。
- 当一个区域满时,触发GC,将存活的对象复制到另一个区域,然后清空当前区域。
优点
- 不会产生内存碎片,适合处理短期存在的对象。
- 清理过程高效,只需移动存活对象。
缺点
- 需要额外的空间来存储存活的对象,内存利用率较低。
- 每次GC都需要复制对象,增加了开销。
(3)标记-整理算法
原理
- 类似于标记-清除算法,但在清除阶段会将存活的对象向一端移动,从而避免内存碎片。
优点
- 解决了内存碎片问题,适合处理长期存在的对象。
- 内存利用率较高。
缺点
- 移动对象的过程较为耗时,可能导致暂停时间较长。
适用场景
- 适用于老年代(Old Generation)的垃圾回收,特别是当内存碎片化成为问题时。
(4)分代收集算法
原理
- 根据对象的生命周期将其分为年轻代和老年代。
- 年轻代采用复制算法,老年代采用标记-清除或标记-整理算法。
- 通过这种分代策略,可以更高效地管理不同生命周期的对象。
优点
- 提高了垃圾回收的效率,减少了不必要的扫描。
- 针对不同生命周期的对象采用了不同的回收策略。
缺点
- 实现复杂,需要维护多个区域的状态。
适用场景
- 适用于大多数现代JVM,默认配置通常使用分代收集算法。
内存分配与回收策略
内存分配
1. 堆内存划分
JVM的堆内存通常被划分为以下几个区域:
年轻代(Young Generation):新创建的对象首先被分配到年轻代。
- Eden区:新对象通常首先被分配到这里。
- Survivor区(S0 和 S1):用于存放从Eden区存活下来的对象。
老年代(Old Generation):
- 经过多次Minor GC后仍存活的对象会被晋升到老年代。
永久代/元空间(Permanent Generation/Metaspace):
- 存储类的元数据、方法信息等(从Java 8开始,永久代被元空间取代)。
2. 对象分配
年轻代分配:
- 新对象通常首先被分配到Eden区。
- 如果Eden区空间不足,会触发一次Minor GC,清理掉不再使用的对象,并将存活的对象移动到Survivor区。
老年代分配:
- 经过多次Minor GC后仍存活的对象会被晋升到老年代。
- 对于大对象(超过一定阈值),可以直接分配到老年代(通过-XX:PretenureSizeThreshold参数设置阈值)。
3. TLAB(Thread Local Allocation Buffer)
为了提高多线程环境下的对象分配效率,JVM为每个线程分配了一个本地缓冲区(TLAB)。当线程需要分配对象时,优先从TLAB中分配内存,减少了线程间的竞争。
回收策略
1. Minor GC(轻量级垃圾回收)
触发条件:****Eden区满时触发。
过程:
- 标记Eden区和Survivor区中的存活对象。
- 将存活对象复制到另一个Survivor区(例如从S0复制到S1)。
- 清理Eden区和原来的Survivor区。
- 如果某个对象经过多次Minor GC仍然存活,则将其晋升到老年代。
2. Major GC(重量级垃圾回收)
触发条件:****老年代空间不足时触发。
过程:
- 标记老年代中的存活对象。
- 清理未被标记的对象,释放其占用的内存。
- 可能会进行内存整理,减少碎片化。
3. Full GC(全量垃圾回收)
触发条件:****整个堆内存(包括年轻代和老年代)空间不足时触发。
过程:
- 标记并清理年轻代和老年代中的所有对象。
- 清理元空间中的无效类信息(如果使用了G1或ZGC等收集器)。
执行引擎
JVM(Java虚拟机)的执行引擎是负责执行字节码的核心组件。它将编译器生成的Java字节码转换为具体平台上的机器码,并在运行时管理这些代码的执行。执行引擎的主要职责包括解释字节码、即时编译(JIT)、垃圾回收等。以下是JVM执行引擎的关键组成部分和工作机制。
基本组成
解释器(Interpreter)
功能:逐条读取并解释执行Java字节码。
优点:
- 启动速度快,因为不需要预编译。
- 适合短生命周期的应用程序。
缺点:
- 执行效率较低,因为每次都需要重新解释字节码。
即时编译器(Just-In-Time Compiler, JIT)
功能:将频繁执行的字节码片段编译成本地机器码,以提高执行效率。
工作原理:
- 在程序运行过程中,JIT会识别出“热点”方法(即经常被调用的方法),并将它们编译成本地机器码。
- 编译后的机器码可以直接由CPU执行,减少了解释执行的开销。
优点:
- 提高了频繁执行代码段的性能。
- 动态优化,适应不同的运行环境。
缺点:
- 初始启动时间较长,因为需要进行编译。
- 需要占用额外的内存来存储编译后的机器码。
垃圾回收器(Garbage Collector, GC)
功能:自动管理内存,回收不再使用的对象,释放内存资源。
常见GC算法:
- Serial收集器:单线程执行,适用于单核CPU和对响应时间要求不高的应用。
- Parallel收集器:多线程并行执行,适用于吞吐量优先的应用。
- CMS(Concurrent Mark-Sweep)收集器:并发执行,减少停顿时间,适用于对响应时间要求较高的应用。
- G1收集器:面向服务端应用,可设置停顿时间目标,适合需要低延迟的应用。
- ZGC收集器:极低的暂停时间,适合大规模应用和大堆内存。
工作流程
1.加载字节码:
- 类加载器(ClassLoader)将 .class 文件加载到JVM中,并将其转换为方法区中的类数据结构。
2. 链接(Linking):
- 验证(Verification):确保加载的字节码符合JVM规范,防止恶意代码或错误代码的执行。
- 准备(Preparation):为类的静态变量分配内存,并设置默认初始值。
- 解析(Resolution):将类、接口、字段和方法的符号引用转换为直接引用。
3.初始化(Initialization):
- 执行类构造器
方法,为静态变量赋初始值,并执行静态初始化块中的代码。
4.执行字节码:
- 解释器逐条解释执行字节码指令。
- 对于频繁执行的代码段,JIT编译器将其编译成本地机器码,以提高执行效率。
5.垃圾回收:
- 当堆内存不足时,垃圾回收器会触发垃圾回收,清理不再使用的对象,释放内存资源。
优化技术
VM执行引擎的优化技术旨在提升Java应用程序的执行效率和性能。这些技术包括但不限于:
- 分层编译:结合解释器和JIT的优势,提供渐进式的编译优化。
- 内联:将方法调用替换为方法体本身,减少方法调用的开销。
- 逃逸分析:分析对象的作用域,决定是否栈上分配以减少垃圾回收开销。
- 锁优化:减少锁竞争,提高并发性能。
- 分支预测:提高条件语句的执行效率。
- 方法内联缓存:提高动态方法调用的效率。
- 常量折叠:在编译期间计算常量表达式的结果。
- 死代码消除:移除不会被执行的代码段。
- 循环展开:通过增加每次迭代处理的数据量,减少循环控制的开销。
补充 :
JVM调优:JVM调优
JMM(内存模型):Java内存模型JMM
注意:
1.本篇文章中的图片来源于百度(侵删)
2.如有错误,欢迎指正。