JVM(Java虚拟机)是Java编程语言的核心组件之一,它是一个虚拟的计算机环境,用于在各种硬件和操作系统上执行Java字节码。JVM的设计目标是提供一种可移植、安全、高性能的执行环境,使得Java程序能够在不同平台上运行,而不受底层硬件和操作系统的限制。
为什么叫做虚拟机?
JVM被称为虚拟机,是因为它在物理硬件之上提供了一个虚拟的执行环境,它不是一个真实的硬件平台,而是在真实硬件之上的软件模拟。JVM会解释和执行Java程序的字节码,并在内存中模拟出Java程序运行所需要的各种环境和资源。
执行流程:
-
编写Java源代码:程序员编写Java程序的源代码,使用Java编译器将源代码编译成Java字节码文件(.class文件)。
-
类加载:当Java程序被运行时,JVM的类加载器会加载Java字节码文件到内存中,生成对应的类对象。
-
字节码解释和执行:JVM的执行引擎会解释和执行加载到内存中的字节码文件。它会逐条解释字节码指令,并在虚拟机中模拟执行这些指令,从而实现Java程序的功能。
-
即时编译(JIT Compilation):在一些情况下,JVM会将频繁执行的字节码编译成本地机器代码,以提高程序的执行效率。这个过程称为即时编译。
-
内存管理和垃圾回收:JVM负责管理内存的分配和释放,包括堆内存、栈内存、方法区等。它还通过垃圾回收器(Garbage Collector)来自动回收不再使用的内存对象,防止内存泄漏和溢出。
-
与本地代码交互:JVM还提供了本地方法接口(Native Interface),允许Java程序调用本地(即非Java虚拟机)的方法,以实现与底层系统的交互。
JVM内存划分
JVM(Java虚拟机)的内存划分是为了管理Java应用程序的内存使用,它将内存分为不同的区域,每个区域有不同的作用和生命周期。以下是JVM内存划分的主要区域:
-
堆内存(Heap Memory)线程共享:
- 堆内存是Java应用程序运行时的主要内存区域。
- 所有对象实例都存储在堆内存中。
- 堆内存可以动态地增加或减少,用于存储Java对象和数组。
- 堆内存分为新生代(Young Generation)、老年代(Old Generation)和永久代(PermGen,Java 8之前)或元空间(Metaspace,Java 8之后)等部分。
-
栈内存(Stack Memory)线程私有:
- 栈内存用于存储线程执行方法时的局部变量、方法参数、返回值和部分方法调用的信息。
- 每个线程都有自己的栈内存,用于存储线程私有的数据。
- 栈内存中的数据在方法执行结束后会立即释放。
-
方法区(Method Area)线程共享:
- 方法区用于存储类信息、静态变量、常量、编译器优化后的代码等。
- 方法区在JVM启动时被创建,存储在堆内存中。
- 方法区的大小取决于具体的JVM实现和运行时参数设置。
- 在Java 8之前,方法区也被称为永久代(PermGen),而在Java 8及以后的版本中,方法区被替换为元空间(Metaspace)。
-
程序计数器(Program Counter)线程私有:
- 程序计数器是每个线程私有的,用于存储当前线程正在执行的字节码指令的地址。
- 程序计数器在多线程环境中起到了线程隔离的作用。
-
本地方法栈(Native Method Stack)线程私有:
- 本地方法栈用于存储Java程序调用本地方法(即非Java虚拟机中的方法)时的数据。
- 本地方法栈类似于栈内存,但是用于存储本地方法调用时的数据。
JVM类加载
类加载是Java虚拟机(JVM)在运行Java程序时将类文件加载到内存中并生成对应的类对象的过程。类加载是Java语言实现“一次编写,到处运行”的核心机制之一,它负责加载、连接和初始化类文件,以便Java程序能够正确地执行。
当Java虚拟机(JVM)加载类时,通常可以分为以下五个过程:
-
加载(Loading):
- 加载是指查找并加载类文件的过程。在加载阶段,JVM通过类加载器(ClassLoader)根据类的全限定名(Fully Qualified Name)来定位并加载类文件。类加载器会将类文件加载到内存中,并生成对应的类对象。
-
双亲委派模型
-
启动类加载器(Bootstrap ClassLoader):
- 启动类加载器是JVM的内置类加载器,它负责加载Java核心类库(如
java.lang
包中的类)和其他基础类库(如rt.jar
中的类)。 - 启动类加载器是用本地代码实现的,通常不是Java类,因此它不是继承自
java.lang.ClassLoader
。 - 启动类加载器没有父类加载器,它是类加载器层次结构的顶层。
- 启动类加载器是JVM的内置类加载器,它负责加载Java核心类库(如
-
扩展类加载器(Extension ClassLoader):
- 扩展类加载器是Java的标准扩展机制的一部分,它负责加载Java的扩展类库(如
jre/lib/ext
目录下的JAR包中的类)。 - 扩展类加载器是
sun.misc.Launcher$ExtClassLoader
类的实例,它的父类加载器是启动类加载器。
- 扩展类加载器是Java的标准扩展机制的一部分,它负责加载Java的扩展类库(如
-
应用程序类加载器(Application ClassLoader):
- 应用程序类加载器也称为系统类加载器,它负责加载应用程序类路径(Classpath)上的类。
- 应用程序类加载器是
sun.misc.Launcher$AppClassLoader
类的实例,它的父类加载器是扩展类加载器。
-
自定义类加载器:
- 自定义类加载器是由Java程序员编写的,用于加载特定路径下的类或者加载特定格式的类文件。
- 自定义类加载器需要继承自
java.lang.ClassLoader
类,并覆盖findClass
方法实现自定义的类加载逻辑。 - 自定义类加载器可以在运行时动态地加载类,以实现一些特殊的需求,例如动态更新、热部署等。
-
- 在双亲委派模型中,当一个类加载器收到加载类的请求时,它会先检查是否已经加载过这个类,如果没有加载过,就会依次委派给父类加载器去加载,直到顶层的启动类加载器。这样的设计保证了类加载的安全性和稳定性,同时避免了类的重复加载,提高了程序的性能和效率。
-
验证(Verification):
- 验证阶段是确保类文件的正确性和安全性的过程。在这个阶段,JVM会对类文件进行各种验证,包括文件格式验证、字节码验证、符号引用验证和访问权限验证等。目的是防止恶意代码和不合法的类文件对系统造成安全风险。
-
准备(Preparation):
- 准备阶段是为类的静态变量分配内存并设置默认初始值的过程。在准备阶段,JVM会为类的静态变量分配内存空间,并设置默认初始值,例如数值类型的变量设置为0,引用类型的变量设置为null。
-
解析(Resolution):
- 解析阶段是将类中的符号引用转换为直接引用的过程。在解析阶段,JVM会将类、方法和字段等符号引用解析为直接引用,以便在程序运行时能够正确地定位到目标类、方法和字段。
-
初始化(Initialization):
- 初始化阶段是类加载的最后一个阶段,在这个阶段,虚拟机执行类构造器(
<clinit>
方法)的代码,对类的静态变量进行初始化。初始化阶段只有在真正使用类的时候才会触发,例如创建类的实例、调用类的静态方法或访问类的静态变量时。
- 初始化阶段是类加载的最后一个阶段,在这个阶段,虚拟机执行类构造器(
JVM垃圾回收机制
ava的垃圾回收机制是基于自动内存管理的概念,它通过垃圾回收器(Garbage Collector)来自动识别和回收不再使用的内存对象,以避免内存泄漏和溢出。在Java中,垃圾回收器可以使用不同的垃圾回收算法来实现内存回收,以下是一些常见的垃圾回收算法以及它们在Java中的应用:
-
标记-清除算法(Mark and Sweep):
- 标记-清除算法是最基本的垃圾回收算法之一,它分为两个阶段:标记阶段和清除阶段。
- 在标记阶段,垃圾回收器遍历程序的对象图,标记所有可达对象。
- 在清除阶段,垃圾回收器遍历堆内存,清除未被标记的对象,释放它们所占用的内存空间。
- 标记-清除算法的缺点是会产生内存碎片,影响内存的连续分配。
-
复制算法(Copying):
- 复制算法将堆内存分为两个区域:From空间和To空间。
- 在垃圾回收过程中,所有存活的对象都会被复制到To空间,然后将From空间中的对象全部清除。
- 复制算法解决了标记-清除算法中产生的内存碎片问题,但是需要额外的内存空间来进行复制操作。
-
标记-整理算法(Mark and Compact):
- 标记-整理算法结合了标记-清除算法和复制算法的优点,它也分为标记阶段和整理阶段。
- 在标记阶段,垃圾回收器标记所有可达对象。
- 在整理阶段,垃圾回收器将存活的对象向一端移动,然后清除掉未被移动的对象,从而使得堆内存中的存活对象连续排列,不再产生内存碎片。
在Java中,垃圾回收器可以根据应用程序的需求选择不同的垃圾回收算法。Java的垃圾回收机制主要使用复制算法和标记-清除算法的组合,其中新生代内存通常使用复制算法,而老年代内存通常使用标记-清除算法或标记-整理算法。Java的垃圾回收器实现了自适应调节策略,根据应用程序的运行情况动态选择合适的垃圾回收算法和参数,以提高垃圾回收的效率和性能。
在Java的垃圾回收中,内存通常被划分为新生代(Young Generation)和老年代(Old Generation),它们有不同的特点和回收方式。
新生代(Young Generation):
-
特点:
- 新生代主要存放新创建的对象。
- 新生代中的对象通常具有较短的生命周期。
- 新生代通常采用复制算法来进行垃圾回收,将内存分为Eden空间、Survivor From空间和Survivor To空间。
-
回收方式:
- 新生代的垃圾回收通常是通过Minor GC(年轻代垃圾回收)来触发的。
- 在Minor GC过程中,垃圾回收器会扫描Eden空间和Survivor空间,将存活的对象复制到Survivor To空间,并清除Eden空间和Survivor From空间中的垃圾对象。
- 在多次Minor GC后,存活时间较长的对象会被晋升到老年代。
老年代(Old Generation):
-
特点:
- 老年代主要存放存活时间较长的对象。
- 老年代中的对象通常具有较长的生命周期。
- 老年代通常采用标记-清除算法或标记-整理算法来进行垃圾回收。
-
回收方式:
- 老年代的垃圾回收通常是通过Full GC(完全垃圾回收)来触发的。
- 在Full GC过程中,垃圾回收器会扫描整个堆内存,对所有的对象进行标记、清除或整理,以回收内存空间。
- Full GC过程会导致应用程序的停顿时间较长,因此尽量避免频繁触发Full GC是优化Java应用程序性能的重要目标之一。
新生代与老年代的转化方式:
-
晋升(Promotion):
- 在新生代进行多次Minor GC后,存活时间较长的对象会被晋升到老年代。
- 对象晋升到老年代的条件通常是达到一定的年龄阈值,例如通过对象的年龄计数器来判断。
-
直接分配到老年代(Direct Allocation to Old Generation):
- 一些大对象或长期存活的对象可以直接分配到老年代,而不经过新生代的分配和晋升过程。
- 直接分配到老年代的对象可能会增加老年代的内存压力,因此需要谨慎使用。
新生代与老年代的区别在于对象的生命周期和回收方式,新生代通常使用复制算法进行垃圾回收,而老年代通常使用标记-清除算法或标记-整理算法进行垃圾回收。对象在新生代和老年代之间的转化通常是通过晋升或直接分配的方式实现的。