目录
1.程序计数器(Program Counter Register)
6.运行时常量池(Runtime Constant Pool):
一.JVM是什么
JVM(Java Virtual Machine)是Java虚拟机的缩写,它是Java编程语言的关键部分之一。JVM是一个虚拟的计算机,它是在物理计算机上模拟的计算机,用于执行Java字节码指令。
当你编写Java程序时,Java源代码首先被编译成字节码(.class文件),然后由JVM解释和执行这些字节码指令。JVM负责管理内存、执行垃圾回收、加载类、执行字节码等任务,从而实现Java程序的跨平台特性。
JVM的存在使得Java程序具有跨平台性,即你编写的Java程序可以在任何安装了JVM的操作系统上运行,而不需要对程序进行重新编译。这种特性极大地简化了Java程序的开发和部署过程。
二.JVM 运行的基本流程
1. 编写:Java源代码开发者使用Java语言编写源代码,保存为.java
文件。
2. 编译源代码:使用Java编译器(如javac
)将.java
文件编译成Java字节码,存储在.class
文件中。字节码是一种中间代码,它与具体的硬件和操作系统无关。
3. 类加载:当Java程序运行时,JVM通过类加载器(Class Loader)加载这些.class
文件。类加载器按照以下步骤执行:
- 加载:读取硬盘上的.class文件,将数据转化为方法区内的数据结构。
- 链接:验证加载的类信息,准备并解析符号引用到直接引用。
- 初始化:对类变量进行初始化,执行静态代码块。
4. 执行:类加载完成后,JVM将字节码提交给执行引擎。执行引擎可以通过解释器逐条解释执行字节码,也可以通过即时编译器(JIT)将部分字节码转换成本地机器码以提高效率。
5. 运行时数据区域:JVM在运行过程中,会使用到以下几个主要的内存区域:
- 程序计数器:每个线程有一个程序计数器,是线程私有的。
- Java栈:每个线程运行时都会创建一个Java栈,用于存放帧。
- 本地方法栈:为执行本地方法服务。
- 堆:几乎所有的对象实例都在这里分配内存。
- 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量等信息。
6. 垃圾回收:JVM在堆内存中管理应用程序创建的所有对象实例。当对象不再被引用时,垃圾回收器将清理这些对象占用的内存,以确保内存的有效利用。
7. 退出:程序执行完毕后,或者遇到未捕获的异常或错误导致退出,JVM将终止程序并释放所有资源。
三.JVM 运行时数据区
当JVM 把字节码(class文件)通过类加载器(ClassLoader)加载时,文件会被加载到内存中的运行时数据区(Runtime Data Area)
JVM的运行时数据区包括以下几个主要的部分:
1.程序计数器(Program Counter Register)
程序计数器是一个非常关键的组件,主要用来存储当前线程执行的字节码的行号指示器。以下是程序计数器的几个主要特点:
线程私有性:程序计数器是线程私有的,这意味着每个线程都有自己独立的程序计数器,线程之间的计数器互不影响。这种设计是为了线程切换后能恢复到正确的执行位置。
执行追踪:程序计数器的主要功能是指示线程当前正在执行的Java字节码的具体位置。如果执行的是Java方法,程序计数器记录的是正在执行的字节码指令的地址;如果执行的是Native方法,则程序计数器的值为空(Undefined)。
内存需求小:由于程序计数器仅仅存储线程执行的代码位置,它的内存需求通常比较小。
垃圾回收无关:程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。也就是说,它对垃圾回收过程没有直接影响。
程序计数器的存在使得Java虚拟机可以支持多线程环境中的线程切换,而不会发生执行状态混乱的问题。这也是Java虚拟机能够支持同时执行多段代码逻辑的关键技术之一。在Java多线程编程中,程序计数器为每个线程的独立运行提供了保障,确保了程序执行的正确性和效率。
2.堆(Heap)
堆(Heap)是Java虚拟机中最大的一块内存区域,用于存储对象实例和数组。堆是所有线程共享的,而且在Java虚拟机启动时就会被创建。以下是堆的一些主要特点和作用:
对象实例和数组存储: 堆用于存储Java程序中创建的对象实例和数组。当程序通过关键字
new
创建一个对象或数组时,这些对象和数组就会被存储在堆中。动态分配和回收: 堆内存的大小可以在程序运行时动态分配和调整。Java虚拟机会根据程序运行的需要动态地分配堆内存,并且在对象不再被引用时,由垃圾回收器负责回收堆中的内存空间。
垃圾回收器管理: 堆中的内存由Java虚拟机的垃圾回收器负责管理。垃圾回收器会定期扫描堆内存中的对象,识别出不再被引用的对象,并将其回收释放内存,以便其他对象可以使用。
分代结构: 堆内存通常被划分为几个不同的代(Generation),例如新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation,JDK 8及更高版本中为元空间Metaspace)。不同代的内存分配和回收策略不同,以提高垃圾回收效率。
内存分配和对象存储: 当Java程序需要创建一个新的对象实例或数组时,Java虚拟机会在堆内存中分配一块连续的内存空间,并将对象实例或数组的数据存储在其中。堆内存的分配是由Java虚拟机的内存管理器负责的。
堆是Java程序中存储对象实例和数组的主要内存区域,它的动态分配和垃圾回收特性使得Java程序能够灵活地管理内存,并且提供了良好的性能和可靠性。
3.Java虚拟机栈(JVM Stack)
Java虚拟机栈(JVM Stack)是Java虚拟机内存中的一块区域,用于存储方法的局部变量、操作数栈、动态链接、方法出口等信息。每个线程在执行Java程序时都会拥有自己独立的Java虚拟机栈。以下是Java虚拟机栈的主要特点和作用:
线程私有: 每个线程在执行时都会有自己独立的Java虚拟机栈。这样可以保证线程在执行方法时,局部变量的访问不会被其他线程所影响。
方法调用: Java虚拟机栈主要用于存储方法的调用信息。每当一个方法被调用时,Java虚拟机会为该方法创建一个栈帧(Stack Frame),栈帧包含了方法的局部变量、操作数栈、动态链接、方法出口等信息。
局部变量存储: 每个栈帧中包含一个局部变量表(Local Variable Table),用于存储方法的局部变量。局部变量表中存储的是方法参数、临时变量等数据。
操作数栈: 每个栈帧中还包含一个操作数栈(Operand Stack),用于存储方法执行过程中的操作数。操作数栈用于执行方法中的算术运算、逻辑运算等操作。
动态链接: 每个栈帧中还包含一个指向运行时常量池(Runtime Constant Pool)中方法引用的指针,用于支持方法的动态链接。
方法出口: 栈帧中还包含一个方法出口(Return Address),用于存储方法执行完毕后的返回地址,以便方法执行完毕后能够正确返回到调用者。
栈帧的压栈与弹栈: 当方法被调用时,Java虚拟机会为该方法创建一个栈帧,并将其压入Java虚拟机栈中;当方法执行完毕时,对应的栈帧会被弹出,释放栈空间。
Java虚拟机栈的大小可以通过启动参数进行调整,不过栈空间的大小是有限制的,当栈空间不足时会抛出 StackOverflowError
错误。因此,合理地设置栈空间大小对于程序的性能和稳定性都是非常重要的。
4.本地方法栈(Native Method Stack)
本地方法栈与Java虚拟机栈类似,不同的是本地方法栈为执行Native方法(使用C或C++编写的方法)服务。
5.方法区(Method Area)
方法区(Method Area)是Java虚拟机内存中的一块区域,用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区是所有线程共享的内存区域,在Java虚拟机启动时就会被创建。以下是方法区的主要特点和作用:
存储类信息: 方法区主要用于存储已加载的类信息,包括类的结构信息(如类的字段、方法、父类、接口等)、静态变量、常量池等。
常量池: 方法区中包含了每个类的常量池,用于存储类中的常量。常量池中包含了字面量(如字符串、数值)、符号引用(如类和接口的全限定名、字段和方法的名称和描述符等)等。
静态变量: 方法区还用于存储类的静态变量,这些变量在类加载时被初始化,并且在整个程序的生命周期内都存在。
即时编译器编译后的代码: 方法区还用于存储即时编译器(Just-In-Time Compiler,JIT)编译后的代码,这些代码通常是将Java字节码编译成本地机器码的过程中生成的。
运行时常量池: 方法区中还包含每个类的运行时常量池,用于存储类加载后解析后的符号引用。运行时常量池与类的常量池相对应,但是它保存的内容不一定与常量池中的内容完全一致,因为常量池中的数据可能会被解析为直接引用。
GC Roots扫描: 垃圾回收器在进行垃圾回收时需要扫描方法区中的对象,以确定哪些对象是可达的。为了进行扫描,垃圾回收器会从一组称为GC Roots的对象开始,然后逐步遍历方法区中的对象。
需要注意的是,方法区的大小是有限制的,并且在某些情况下可能会导致 OutOfMemoryError
错误。另外,从Java 8开始,方法区被移除,取而代之的是元空间(Metaspace),它是方法区的一种实现方式,具有更好的性能和可靠性。
6.运行时常量池(Runtime Constant Pool):
运行时常量池(Runtime Constant Pool)是Java虚拟机方法区中的一部分,用于存储每个类或接口的常量池所解析出来的符号引用。运行时常量池是在类加载解析阶段生成的,并且与类的常量池相对应,但其中保存的内容并不完全一致,因为常量池中的数据可能会被解析为直接引用。
以下是关于运行时常量池的详细介绍:
符号引用和直接引用: 在Java代码中,一些符号引用(Symbolic Reference)如类和接口的全限定名、字段和方法的名称和描述符等,需要在类加载解析阶段转换为直接引用(Direct Reference),以便在程序运行时快速访问。运行时常量池中存储的就是这些解析后的直接引用。
解析过程: 在类加载的解析阶段,Java虚拟机会将类或接口的符号引用解析为直接引用,并将解析后的结果存储在运行时常量池中。这样在程序运行时,就可以直接使用这些直接引用来访问类、字段和方法,而不需要重新进行解析。
方法调用: 在Java程序运行时,通过运行时常量池中存储的直接引用,可以快速地进行方法调用,字段访问等操作,从而提高程序的执行效率。
动态性: 运行时常量池具有一定的动态性,因为在程序运行过程中,可能会动态地加载类、修改类的结构等,这些都可能导致运行时常量池中的内容发生变化。
内存占用: 运行时常量池占用的内存空间是有限制的,因此需要合理地设置方法区大小,避免出现
OutOfMemoryError
错误。
运行时常量池在Java虚拟机中扮演着重要的角色,它存储了类加载解析阶段生成的直接引用,为Java程序的运行提供了高效的访问方式。在程序设计和性能优化时,了解和合理利用运行时常量池是非常重要的。
四.JVM 类加载
类的生命周期是其中一个重要的组成部分,它包括了以下几个阶段:
加载(Loading): 加载阶段是指将类的.class文件字节码内容加载到JVM中的方法区。这个过程可以通过类加载器来完成,类加载器负责从文件系统、网络中加载类的二进制数据,并创建一个Class对象。
连接(Linking): 连接阶段又包括了三个子阶段:
- 验证(Verification): 确保被加载的类的正确性,包括文件格式的验证、元数据的验证、字节码的验证等。
- 准备(Preparation): 为类的静态变量分配内存,并设置默认初始值。
- 解析(Resolution): 将常量池中的符号引用转换为直接引用。
初始化(Initialization): 在初始化阶段,JVM会执行类构造器
<clinit>()
方法,该方法由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。在程序运行过程中,如果发现这个类还没有被初始化,则触发其初始化阶段。使用(Using): 在类初始化完成后,可以通过创建对象或者调用类的静态方法等方式来使用这个类。
卸载(Unloading): 当一个类不再被引用,且没有任何实例存在时,JVM 可能会对其进行卸载操作,释放相应的内存空间
双亲委派模型
双亲委派模型是Java类加载器的一种重要机制,用于保证Java应用程序的稳定性和安全性。它是一种层次化的类加载器体系,将类加载的责任委派给了父类加载器,只有在父类加载器无法完成加载时,才由子类加载器尝试加载。这个模型通常用于解决类的命名冲突、安全性和代码隔离等问题。
下面是关于双亲委派模型的一些关键概念:
层次化结构:在双亲委派模型中,Java类加载器形成了一个层次化的结构,每个类加载器都有一个父加载器。这种层次化结构保证了类加载器之间的有序性。
委派机制:当一个类加载器收到加载类的请求时,它会首先将这个请求委派给父类加载器。父加载器会继续将请求传递给自己的父加载器,直到达到顶层的启动类加载器(Bootstrap ClassLoader)为止。如果顶层加载器无法加载这个类,就会逐级向下委派,直到某个类加载器能够加载成功或者加载失败。
避免重复加载:双亲委派模型可以避免类的重复加载,当一个类加载器加载了一个类后,它会将加载成功的类缓存起来,下次如果再次加载同一个类,就可以直接返回缓存中的类,而不需要重新加载。
安全性和代码隔离:通过双亲委派模型,Java运行时环境可以确保核心类库不会被恶意代码替换,同时也可以实现不同模块之间的代码隔离,防止类的命名冲突和版本冲突。
注意:不是所有的类都严格遵循双亲委派模型。尽管Java的标准类加载器(如Bootstrap ClassLoader、Extension ClassLoader和Application ClassLoader)都严格遵循双亲委派模型,但是在一些特殊情况下,开发者可以自定义类加载器,并且可以选择是否遵循双亲委派模型。
五.垃圾回收
1.死亡对象的判断算法
JVM判断对象是否死亡的算法使用的是可达性分析算法
可达性分析算法是一种用于确定动态分配的内存是否可被程序中的任何指针访问到的算法。它是大多数现代垃圾回收器(包括 Java 的垃圾回收器)使用的基本原理。可达性分析算法基于一个简单的概念:如果一个对象可以通过一系列的引用关系从根对象(如全局变量、活跃线程的栈中的变量等)访问到,那么这个对象就是“可达”的,否则就是“不可达”的。
下面是可达性分析算法的基本过程:
根搜索:可达性分析算法从一组称为“根”的对象开始,如全局变量、活跃线程的栈中的变量等。这些根对象被认为是程序中正在使用的对象,因此它们是可达的。
遍历对象图:从根对象开始,可达性分析算法递归地遍历对象之间的引用关系。如果一个对象引用了另一个对象,那么这个对象也会被视为可达的。算法会持续遍历并标记所有可以被访问到的对象,直到无法再找到新的可达对象为止。
标记阶段:在遍历过程中,已经被访问过的对象将被标记为“被访问过”,以避免重复访问同一个对象。
清除阶段:在标记阶段完成后,所有未被标记为“被访问过”的对象将被认为是“不可达”的,即垃圾对象。这些垃圾对象占用的内存将被释放,并标记为可用内存。
可达性分析算法的优点在于它能够准确地识别出程序中实际可达的对象,避免了将仍然被使用的对象误判为垃圾对象的情况。然而,这种算法也存在一些缺点,例如可能导致停顿时间过长、频繁的扫描整个堆内存等问题。因此,在实际的垃圾回收实现中,通常会结合其他技术和优化手段来降低这些缺点的影响。
2.垃圾回收算法
在 JVM(Java 虚拟机)中,垃圾回收算法是用来识别和清理不再被程序使用的内存,从而避免内存泄漏和提高程序性能的重要组成部分。JVM 中常见的垃圾回收算法包括以下几种:
标记-清除算法(Mark and Sweep):
- 这是最基本的垃圾回收算法之一。它通过两个阶段来执行垃圾回收:标记阶段和清除阶段。
- 在标记阶段,算法会从根对象开始遍历所有可达对象,并标记它们。未被标记的对象被视为垃圾对象。
- 在清除阶段,算法会清理所有未被标记的垃圾对象,并回收它们占用的内存空间。
复制算法(Copying):
- 复制算法主要用于处理年轻代的垃圾回收。它将堆内存分为两个相等大小的区域,通常称为“from”区域和“to”区域。
- 在垃圾回收时,所有存活的对象都会被复制到“to”区域,而未被复制的对象则被视为垃圾对象。
- 之后,“from”区域被清空,而“to”区域则成为新的存活对象的容器。
标记-整理算法(Mark and Compact):
- 标记-整理算法通常用于老年代的垃圾回收。它结合了标记-清除和移动对象的步骤。
- 首先,与标记-清除算法类似,算法会标记所有可达对象,并清理掉未被标记的垃圾对象。
- 然后,算法会将所有存活对象向一端移动,以便在垃圾回收后获得一块连续的内存空间。
分代算法(Generational):
- 分代算法是一种综合性的垃圾回收策略,它将堆内存分为不同的代(Generation),通常包括年轻代、老年代和永久代(或元空间)等。
- 基于分代假设:大部分对象的生命周期很短,而少数对象会存活更长时间。因此,采用不同的垃圾回收算法和频率来处理不同代的对象,可以更有效地管理内存。
并发标记-清除算法(Concurrent Mark and Sweep):
- 并发标记-清除算法是一种针对停顿时间的优化算法。它允许垃圾回收器在程序运行的同时执行标记和清除操作,从而减少程序的停顿时间。
- 虽然并发标记-清除算法可以减少停顿时间,但其引入了额外的复杂性和性能开销,因此需要在实际应用中进行权衡。
这些垃圾回收算法在 JVM 中通常会结合使用,以便在不同情况下达到最佳的性能和效果。例如,通常会将年轻代使用复制算法,老年代使用标记-整理算法,并且可能结合分代假设来优化垃圾回收策略。