博主介绍: ✌博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家✌
💕💕 感兴趣的同学可以收藏关注下 ,不然下次找不到哟💕💕
1、什么是 JVM
JVM(Java虚拟机)是Java编程语言的运行环境。它是Java平台的核心组件之一,负责执行Java字节码(Java编译器生成的中间代码)。JVM提供了内存管理、垃圾回收、安全性等功能,使得Java程序可以在不同的操作系统和硬件平台上运行,实现了“一次编写,到处运行”的特性。
JVM有两个主要的组成部分:
-
类加载器(Class Loader):负责将Java字节码加载到内存中,并将其转换为可执行的形式。类加载器将类的字节码从文件系统、网络或其他来源加载到JVM中。
-
执行引擎(Execution Engine):负责执行已加载的字节码。它将字节码解释成机器指令或直接编译成本地代码来执行。
JVM还包括其他组件,如垃圾回收器(Garbage Collector)用于自动管理内存,即时编译器(Just-In-Time Compiler)用于将热点代码编译成本地机器代码以提高执行效率等。
通过使用JVM,Java程序可以实现跨平台的特性,因为JVM提供了一个统一的运行时环境,屏蔽了底层操作系统和硬件的差异。这使得Java成为一种广泛应用于各种领域的编程语言。
2、运行时数据区
JVM(Java虚拟机)的内存结构包括以下几个主要组成部分:
-
方法区(Method Area):用于存储类的结构信息,包括类的字段、方法、构造函数等。方法区是各个线程共享的内存区域。
-
堆(Heap):用于存储对象实例。所有通过new关键字创建的对象都会被分配到堆中。堆也是各个线程共享的内存区域。
-
栈(Stack):用于存储方法的局部变量、方法参数、返回值等。每个线程在执行方法时都会创建一个对应的栈帧,栈帧中存储了方法的局部变量表、操作数栈、动态链接、方法出口等信息。
-
本地方法栈(Native Method Stack):用于存储本地方法的信息。本地方法是使用其他编程语言(如C、C++)编写的方法,通过JNI(Java Native Interface)调用。
-
PC寄存器(Program Counter Register):用于存储当前线程执行的字节码指令地址。
-
垃圾回收器(Garbage Collector):负责自动管理堆中的对象,回收不再使用的对象。
除了上述主要的内存区域外,JVM还包括运行时常量池(Runtime Constant Pool)、直接内存(Direct Memory)等。运行时常量池存储编译时生成的字面量和符号引用。直接内存是JVM使用的一种特殊内存区域,它不受JVM内存管理的限制,可以直接分配和释放内存。
JVM的内存结构在不同的JVM实现中可能会有所差异,上述描述是一种常见的JVM内存结构。它提供了一个运行时环境,用于执行Java程序并管理内存。
3、程序计数器(线程私有的)
3.1、什么是程序计数器
程序计数器(Program Counter)是一种特殊的寄存器,用于存储当前正在执行的指令在内存中的地址或索引。它是计算机体系结构中的一部分,用于跟踪程序的执行位置。
程序计数器在CPU中起着重要的作用。它用于指示下一条要执行的指令的位置。当CPU执行一条指令时,程序计数器会自动递增,指向下一条要执行的指令。这样,CPU就可以按照程序的顺序依次执行指令。
程序计数器一般是一个固定长度的寄存器,其大小取决于计算机体系结构的设计。在32位体系结构中,程序计数器通常是32位长,而在64位体系结构中,它通常是64位长。
程序计数器的值可以通过跳转指令(如条件分支、无条件跳转、函数调用等)来修改。当发生跳转时,程序计数器会被更新为跳转目标的地址,以便下一条指令的执行。
需要注意的是,程序计数器只存储指令的地址,并不存储指令的内容。具体的指令内容存储在内存中的指令存储区域(如代码段)中。
程序计数器在多线程环境中也起着重要的作用。每个线程都有自己的程序计数器,用于记录线程当前执行的指令位置。这样,在多线程切换时,可以保存和恢复各个线程的执行位置,以实现线程的并发执行。
3.2、程序计数器的特点
程序计数器的特点有以下几个方面:
-
存储当前指令位置:程序计数器用于存储当前正在执行的指令在内存中的地址或索引。它指示了下一条要执行的指令的位置。
-
自动递增:每当CPU执行一条指令时,程序计数器会自动递增,指向下一条要执行的指令。这样,CPU可以按照程序的顺序依次执行指令。
-
跳转指令的更新:当发生跳转指令(如条件分支、无条件跳转、函数调用等)时,程序计数器会被更新为跳转目标的地址,以便下一条指令的执行。
-
固定长度:程序计数器一般是一个固定长度的寄存器,其大小取决于计算机体系结构的设计。在32位体系结构中,程序计数器通常是32位长,而在64位体系结构中,它通常是64位长。
-
存储指令地址而非内容:程序计数器只存储指令的地址,并不存储指令的具体内容。具体的指令内容存储在内存中的指令存储区域(如代码段)中。
-
多线程环境支持:在多线程环境中,每个线程都有自己的程序计数器,用于记录线程当前执行的指令位置。这样,在多线程切换时,可以保存和恢复各个线程的执行位置,以实现线程的并发执行。
总的来说,程序计数器是一种特殊的寄存器,用于存储当前指令的位置,并支持指令的自动递增和跳转。它在计算机体系结构中起着重要的作用,用于控制程序的执行流程。
3.3、程序计数器的作用
程序计数器(Program Counter)是计算机中的一种寄存器,用于存储当前正在执行的指令的地址或索引。它的作用是控制程序的执行流程。
具体来说,程序计数器的作用包括:
-
指示下一条要执行的指令:程序计数器存储着当前指令的地址,它指示了下一条要执行的指令在内存中的位置。当CPU执行完一条指令后,程序计数器会自动递增,指向下一条要执行的指令。
-
实现顺序执行:程序计数器的自动递增机制使得CPU可以按照程序的顺序依次执行指令。通过不断递增程序计数器的值,CPU可以顺序地执行程序中的每一条指令。
-
支持跳转指令:当程序中出现跳转指令(如条件分支、无条件跳转、函数调用等)时,程序计数器会被更新为跳转目标的地址。这样,下一条要执行的指令就会改变,从而实现了程序的跳转功能。
-
控制程序的循环和分支:程序计数器的值可以用于控制程序的循环和分支结构。通过修改程序计数器的值,可以使程序重复执行某一段代码(循环),或者根据条件选择不同的执行路径(分支)。
总的来说,程序计数器在计算机中起着重要的作用,它控制着程序的执行顺序,支持指令的顺序执行和跳转,并控制程序的循环和分支结构。
4、虚拟机栈(线程私有的)
4.1、什么是虚拟机栈
虚拟机栈(Virtual Machine Stack),也称为操作数栈或执行栈,是虚拟机(如Java虚拟机)用于执行程序的一块内存区域。它用于存储方法的局部变量、操作数和中间结果等数据。
虚拟机栈的主要作用是支持方法的调用和执行。每当一个方法被调用时,虚拟机会为该方法创建一个栈帧(Stack Frame),并将栈帧压入虚拟机栈中。栈帧包含了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息。
具体来说,虚拟机栈的功能包括:
- 存储局部变量:每个栈帧都有自己的局部变量表,用于存储方法的参数和局部变量。在方法执行过程中,方法的参数和局部变量都会被存储在局部变量表中。
- 执行方法调用:当一个方法被调用时,虚拟机会创建一个新的栈帧,并将其压入虚拟机栈中。这样,方法的执行就可以在新的栈帧中进行。
- 管理方法的返回:当一个方法执行完成后,虚拟机会从虚拟机栈中弹出该方法对应的栈帧,并将控制权返回给调用该方法的方法。
虚拟机栈的大小是有限的,它通常会在虚拟机启动时预先分配一定的内存。如果虚拟机栈的空间不足,会抛出栈溢出异常。
总结来说,虚拟机栈是虚拟机用于执行程序的一块内存区域,它存储了方法的局部变量、操作数和中间结果等数据。它支持方法的调用和执行,并管理方法的返回。
4.2、什么是栈帧
栈帧(Stack Frame),也称为帧(Frame)或活动记录(Activation Record),是在程序执行过程中用于存储方法调用相关信息的一种数据结构。栈帧是在虚拟机栈(Virtual Machine Stack)中创建的,用于支持方法的调用和执行。
每当一个方法被调用时,虚拟机会为该方法创建一个栈帧,并将其压入虚拟机栈中。栈帧包含了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息。
具体来说,栈帧的主要作用包括:
-
存储局部变量:栈帧中有一个局部变量表,用于存储方法的参数和局部变量。在方法执行过程中,方法的参数和局部变量都会被存储在局部变量表中。
-
执行方法调用:当一个方法被调用时,虚拟机会为该方法创建一个新的栈帧,并将其压入虚拟机栈中。这样,方法的执行就可以在新的栈帧中进行。
-
管理方法的返回:当一个方法执行完成后,虚拟机会从虚拟机栈中弹出该方法对应的栈帧,并将控制权返回给调用该方法的方法。
栈帧的大小是根据方法的需要动态分配的,每个方法都有自己的栈帧。栈帧的创建和销毁是方法调用的过程中自动进行的。
总结来说,栈帧是在虚拟机栈中创建的用于存储方法调用相关信息的数据结构。它包含了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息,支持方法的调用和执行。
4.3、虚拟机栈的运行原理
虚拟机栈(Virtual Machine Stack)是一种用于支持程序执行的数据结构,它采用栈的数据结构形式,用于存储方法调用和执行过程中的相关信息。
虚拟机栈的运行原理如下:
-
栈帧的创建:每当一个方法被调用时,虚拟机会为该方法创建一个栈帧,并将其压入虚拟机栈中。栈帧包含了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息。
-
方法调用:当一个方法被调用时,虚拟机会将当前的栈帧压入虚拟机栈,并为被调用的方法创建一个新的栈帧。这样,方法的执行就可以在新的栈帧中进行。
-
方法执行:在方法执行过程中,虚拟机会使用栈帧中的局部变量表来存储方法的参数和局部变量。操作数栈用于存储方法执行过程中的中间结果和临时变量。动态链接用于支持方法调用的动态性。
-
方法返回:当一个方法执行完成后,虚拟机会从虚拟机栈中弹出该方法对应的栈帧,并将控制权返回给调用该方法的方法。在方法返回时,虚拟机会使用栈帧中保存的返回地址来确定返回的位置。
虚拟机栈的运行原理是基于栈的先进后出(LIFO)的特性。每个方法的执行都在自己的栈帧中进行,保证了方法的局部变量和执行状态的独立性。虚拟机栈的大小是根据方法的需要动态分配的,可以根据实际情况进行调整。
总结来说,虚拟机栈是一种用于支持程序执行的数据结构,采用栈的形式存储方法调用和执行过程中的相关信息。它通过栈帧的创建、方法调用、方法执行和方法返回等步骤,实现了方法的独立执行和控制流的转移。
5、本地方法栈(线程私有的)
5.1、什么是本地方法栈
本地方法栈(Native Method Stack)是Java虚拟机中的一部分,用于支持调用本地方法(Native Method)。本地方法是使用非Java语言编写的方法,通常是使用C或C++等语言编写的。本地方法栈用于存储本地方法的相关信息。
本地方法栈与虚拟机栈类似,但是其作用不同。虚拟机栈用于支持Java方法的调用和执行,而本地方法栈用于支持本地方法的调用和执行。
本地方法栈的运行原理与虚拟机栈类似,也是采用栈的数据结构形式。当一个Java程序调用本地方法时,虚拟机会将当前的栈帧压入本地方法栈,并为本地方法创建一个新的栈帧。本地方法的执行就可以在新的栈帧中进行。
本地方法栈的大小也是根据实际情况进行动态分配的。与虚拟机栈类似,本地方法栈也具有栈的先进后出(LIFO)的特性。
总结来说,本地方法栈是Java虚拟机中的一部分,用于支持调用本地方法。它与虚拟机栈类似,但作用不同。本地方法栈通过栈帧的创建、本地方法调用、本地方法执行和本地方法返回等步骤,实现了本地方法的独立执行和控制流的转移。
5.2、本地方法栈接口
本地方法栈(Native Method Stack)是Java虚拟机中的一部分,用于支持调用本地方法(Native Method)。本地方法是使用非Java语言编写的方法,通常是使用C或C++等语言编写的。本地方法栈用于存储本地方法的相关信息。
在Java虚拟机规范中,并没有明确定义本地方法栈的具体接口。本地方法栈的实现通常由具体的Java虚拟机实现来提供。不同的Java虚拟机实现可能会有不同的本地方法栈接口。
对于Java开发者来说,使用本地方法栈并不需要直接操作其接口。Java虚拟机会负责管理本地方法栈的创建和销毁,并提供相应的调用机制,使得Java程序可以调用和执行本地方法。
总结来说,本地方法栈是Java虚拟机中的一部分,用于支持调用本地方法。具体的本地方法栈接口由Java虚拟机实现提供,对于Java开发者来说,使用本地方法栈通常只需要调用相应的本地方法即可,无需直接操作其接口。
6、堆内存 (线程共享的)
6.1、什么是堆内存
堆内存是计算机中一种用于存储动态分配的数据的内存区域。堆内存是由操作系统分配和管理的,在程序运行时可以动态地分配和释放内存。与栈内存不同,堆内存的大小不会在编译时确定,而是在运行时根据程序的需要进行动态调整。
堆内存通常用于存储动态分配的对象,如动态创建的对象、数组和数据结构等。它提供了灵活的内存管理机制,允许程序在运行时动态地申请和释放内存,从而满足不同场景下的内存需求。
需要注意的是,堆内存的管理需要由程序员负责,包括手动申请和释放内存。如果内存没有被正确释放,会导致内存泄漏的问题,进而影响程序的性能和稳定性。因此,合理地管理堆内存是编程中需要重视的一部分。
6.2、堆内存的划分
堆内存的划分中,新生代、老年代和幸存区是Java虚拟机中用于管理对象的不同区域。
新生代(Young Generation)是指堆内存中用于存放新创建的对象的区域。新生代通常被划分为三个区域:Eden区、幸存区0(Survivor 0)和幸存区1(Survivor 1)。新创建的对象首先会被分配到Eden区,当Eden区满时,会触发一次垃圾回收(Minor GC),将仍然存活的对象复制到幸存区。幸存区也会随着垃圾回收进行对象的复制和清理。经过多次回收后,仍然存活的对象会被移到老年代。
老年代(Old Generation)是指堆内存中存放长时间存活的对象的区域。老年代相对于新生代来说,存放的是更稳定、存活时间更长的对象。当老年代空间不足时,会触发一次完整的垃圾回收(Full GC),对整个堆内存进行清理和整理。
幸存区(Survivor)是新生代中的一部分,用于存放在Eden区和幸存区之间经过一次垃圾回收后仍然存活的对象。幸存区一般有两个,分别称为幸存区0和幸存区1。在垃圾回收时,存活的对象会从Eden区复制到幸存区,经过多次回收后,仍然存活的对象会被移到老年代。
新生代、老年代和幸存区的划分是为了更好地管理内存,提高垃圾回收的效率。通过将对象按照生命周期的不同阶段划分到不同的区域,可以减少垃圾回收的频率和复制的对象数量,提高程序的性能和内存利用率。
6.3、对象在堆内存中的生命周期
对象在堆内存中的生命周期是指对象在内存中被创建、使用和销毁的过程。具体来说,对象的生命周期包括以下几个阶段:
-
创建阶段:对象在堆内存中被创建时,系统会为其分配足够的内存空间,并执行相应的构造函数进行初始化。这个阶段通常发生在使用 new 关键字创建对象时。
-
使用阶段:对象在创建后,可以在程序中被使用。在使用阶段,可以对对象的属性进行读取和修改,调用对象的方法等。
-
销毁阶段:当对象不再被使用时,需要将其从内存中销毁,以释放占用的内存空间。这个阶段通常发生在对象超出作用域、显式调用 delete 关键字或垃圾回收器检测到对象不再被引用时。
需要注意的是,对象的生命周期由程序员负责管理。如果对象没有被正确地销毁或释放,会导致内存泄漏的问题,造成内存资源的浪费。因此,在使用动态分配的对象时,需要确保及时释放不再使用的对象,以避免内存泄漏和性能问题的发生。
6.4、初步了解垃圾回收
垃圾回收是一种自动化的内存管理机制,用于在程序运行过程中自动回收不再使用的内存资源,以避免内存泄漏和内存溢出的问题。垃圾回收通过识别不再被程序引用的对象,并释放其占用的内存空间来实现。
在Java中,垃圾回收是由Java虚拟机(JVM)负责执行的。JVM会周期性地扫描堆内存中的对象,标记那些仍然被引用的对象,并清理掉那些不再被引用的对象。垃圾回收算法的选择和实现可能因不同的JVM而有所不同。
常见的垃圾回收算法包括标记-清除算法、复制算法、标记-整理算法等。标记-清除算法首先标记出所有被引用的对象,然后清理掉那些没有被标记的对象。复制算法将堆内存分为两个区域,每次只使用其中一个区域,将存活的对象复制到另一个区域,并清理掉原来的区域。标记-整理算法则是在标记过程后,将存活的对象向一端移动,然后清理掉边界之外的内存。
垃圾回收的过程中可能会造成一定的性能开销,因此需要根据具体的应用场景和需求来选择适合的垃圾回收策略。同时,开发人员也可以通过手动调用System.gc()方法来建议JVM进行垃圾回收,但并不能保证立即执行。
总的来说,垃圾回收是一项重要的内存管理技术,可以帮助开发人员简化内存管理的工作,提高程序的性能和稳定性。
7、方法区 (线程共享的)
7.1、什么是方法区
方法区是Java虚拟机(JVM)中的一块内存区域,用于存储类的信息、常量、静态变量和编译器编译后的代码等。方法区是线程共享的,它在JVM启动时就被创建,并且在JVM关闭时才会被销毁。
方法区主要用于存储以下内容:
-
类的信息:包括类的名称、父类、接口、字段和方法等。
-
常量池:存放编译器生成的各种字面量和符号引用。
-
静态变量:存放类的静态变量,这些变量在类加载时初始化,并且在整个生命周期内存在。
-
编译器编译后的代码:将Java源代码编译成字节码后存储在方法区中。
在方法区中,由于存储的是类的信息和静态变量等数据,因此它的大小是固定的,不会随着程序的运行而动态改变。在Java 8及之前的版本,方法区是使用永久代(Permanent Generation)实现的。而在Java 8及之后的版本,永久代被元空间(Metaspace)所取代,元空间是使用本地内存来实现的,它的大小不再受限于JVM的内存大小。
需要注意的是,方法区在不同的JVM实现中可能有所不同,具体的实现细节可能会有所差异。但是,无论是永久代还是元空间,方法区的作用都是存储类的相关信息和静态变量等数据。
7.2、方法区的结构
方法区的结构可以分为以下几个部分:
-
类的信息:方法区存储了加载的类的信息,包括类的名称、父类、接口、字段和方法等。这些信息被存储在方法区的类数据结构中,用于在程序运行时进行类的加载、链接和初始化。
-
运行时常量池:方法区中还包含了每个类的常量池,用于存储编译器生成的各种字面量和符号引用。常量池中的内容包括字符串、基本类型的常量值、类和接口的全限定名、字段和方法的名称和描述符等。
-
静态变量:方法区中还存储了类的静态变量,这些变量在类加载时被初始化,并且在整个程序的生命周期内存在。静态变量被存储在类的静态变量表中,可以被所有实例共享。
-
方法和字节码:方法区还存储了类的方法和编译后的字节码。每个方法都包含了方法的名称、参数列表、返回值类型和字节码指令等信息。字节码指令是由编译器生成的,用于在运行时执行方法的具体逻辑。
需要注意的是,方法区的结构在不同的JVM实现中可能有所差异,具体的实现细节可能会有所不同。例如,Java 8及之前的版本使用永久代来实现方法区,而Java 8及之后的版本使用元空间来替代永久代。不同的实现可能会有不同的数据结构和存储方式,但总体上都是用于存储类的相关信息、常量池、静态变量和方法字节码等内容。
7.3、运行时常量池
运行时常量池(Runtime Constant Pool)是方法区中的一部分,用于存储每个类的常量池数据。它是在类加载过程中生成的,并且在程序运行时被使用。
运行时常量池包含了编译器生成的各种字面量和符号引用。字面量包括字符串、基本类型的常量值,符号引用包括类和接口的全限定名、字段和方法的名称和描述符等。
运行时常量池的作用是为了支持在程序运行时进行动态链接和运行时常量池解析。在程序运行时,虚拟机会根据符号引用在运行时常量池中进行查找,找到对应的字面量或符号引用,并进行解析和链接操作。
需要注意的是,运行时常量池是每个类独立拥有的,即每个类都有自己的运行时常量池。它与类的静态变量和方法是独立的,不同实例的对象共享的是类的运行时常量池而不是静态变量。
运行时常量池的具体实现可能会有所不同,不同的JVM实现可能会有不同的数据结构和存储方式。例如,Java 8及之前的版本使用永久代来实现方法区和运行时常量池,而Java 8及之后的版本使用元空间来替代永久代。不同的实现可能会有不同的性能和限制。
7.4、StringTable
StringTable(字符串表)是Java虚拟机中的一部分,属于方法区的一部分,用于存储字符串常量。它是在运行时常量池中的字符串常量被使用时动态生成的。
StringTable的作用是为了提高字符串常量的存储和查找效率。在Java中,字符串常量是不可变的,因此可以被多个对象共享。为了节省内存空间,Java虚拟机会将字符串常量存储在StringTable中,并且通过引用计数的方式来管理字符串的使用情况。
当一个字符串常量被使用时,Java虚拟机会在StringTable中查找该字符串,如果找到则返回对应的引用,如果没有找到则将该字符串添加到StringTable中,并返回新添加的引用。
需要注意的是,StringTable是在JVM中的具体实现,不同的JVM实现可能会有不同的数据结构和存储方式。例如,早期的HotSpot JVM使用哈希表实现StringTable,而后来的版本使用了字典树(Trie)结构来优化字符串的查找效率。不同的实现可能会有不同的性能和限制。
💕💕 本文由激流原创
💕💕喜欢的话记得点赞收藏啊