java虚拟机
# 基础
# 为什么会有JVM
JVM(Java虚拟机)是Java平台的核心组成部分之一,它是一个在计算机上运行Java字节码的虚拟机。JVM充当了Java应用程序和底层操作系统之间的中间层,提供了跨平台的特性,使得Java程序可以在不同的操作系统和硬件上运行。JVM的主要功能和特点包括字节码执行、内存管理、即时编译、异常处理和类加载和运行时环境等。
JVM的跨平台特性是Java语言的重要优势之一。由于Java程序在不同的操作系统上运行的时候都依赖于JVM,因此只需编写一次Java程序,就可以在多个平台上运行,无需对代码进行修改。这使得Java成为了广泛应用于各种领域的开发语言之一。
# 内存管理
# Java内存区域结构
JVM内存区域主要分为程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存。其中程序计数器、Java虚拟机栈和本地方法栈属于线程隔离,即他们都有自己的线程归属,其他属于线程共享的¹。
- 程序计数器:是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码行号指示器。
- Java虚拟机栈:每个线程私有,里面装的多个栈帧,每个栈帧对于的一个方法。里面存储的是Java方法的内存模型。
- 本地方法栈:线程私有,和上一个Java虚拟机栈作用相似,Java虚拟机栈是为Java方法服务,本地方法栈是为Native服务。
- Java堆:Java虚拟机管理最大的一块,线程共享,存放对象实例和数组。分新生代(1/3)和老年代 (2/3),新生代还可以分Eden(8/10)、From Survivor(1/10) 、To Survivor(1/10),是主要根据垃圾清理来分的。
- 方法区:线程共享,主要存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存。在Java 8中,永久代(PermGen)被移除,取而代之的是元空间(Metaspace)。元空间不在JVM内存中,而是使用本地内存。方法区(Method Area)这个概念仍然存在,但它的实现方式发生了变化。在Java 8及更高版本中,方法区的实现是元空间。
# 怎么理解本地方法
本地方法栈为本地方法(Native Method)提供服务。本地方法通常是用C或C++等其他语言实现的,它们不是由Java语言编写的。本地方法可以访问操作系统底层资源,例如文件系统、硬件设备等。
举个例子,Java类库中的System
类提供了一个静态方法arraycopy
,它可以快速地将一个数组中的元素复制到另一个数组中。这个方法是用C语言实现的,并且被声明为native
,这意味着它是一个本地方法。当Java程序调用这个方法时,JVM会调用底层的C语言实现来执行这个操作。
本地方法栈就是用来支持本地方法调用的。当Java程序调用一个本地方法时,JVM会在本地方法栈中创建一个栈帧来存储该方法的状态信息,包括参数、局部变量和返回值等。当本地方法执行完毕后,它的栈帧就会从本地方法栈中弹出,控制权返回给Java程序。
# 为什么不用永久代了,改用元空间了呢
在Java 8中,永久代(PermGen)被移除,取而代之的是元空间(Metaspace)。这一改变主要是为了解决永久代中的一些问题。
永久代的大小是固定的,如果程序运行时需要存储更多的类或方法信息,就会导致永久代溢出(OutOfMemoryError: PermGen space)。这种情况在使用一些动态生成类的框架时尤为常见。为了避免这种情况,开发人员需要手动调整永久代的大小,但这并不总是有效。
元空间使用本地内存,它的大小可以根据程序运行时的需要动态调整。这样就避免了永久代溢出的问题。此外,元空间还提供了更好的内存管理和垃圾回收机制。
总之,使用元空间代替永久代可以提高JVM的性能和可靠性。
# 一个对象在JVM的创建过程
当Java程序创建一个新的对象时,JVM会执行以下步骤:
检查:JVM会检查字节码指令,以确定要创建的对象的类是否已经被加载、解析和初始化。如果没有,JVM会先执行类加载过程。
分配内存:接下来,JVM会在堆内存中为新对象分配内存空间。这一步骤的具体实现方式取决于JVM的内存分配策略和垃圾回收器的配置。
初始化零值:JVM会将分配给对象的内存空间初始化为零值,以确保对象的所有属性都有一个初始值。
设置对象头:JVM会设置新对象的对象头,包括对象的类元数据信息、哈希码、GC分代年龄等信息。
执行构造函数:最后,JVM会执行对象的构造函数,以完成对象的初始化。构造函数会根据程序员的设定来初始化对象的属性,并执行其他初始化逻辑。
完成上述步骤后,新对象就被成功创建,并且可以被Java程序使用了。
# 类的加载过程
类的加载过程包括以下几个步骤:
加载:首先,JVM会通过类加载器(ClassLoader)来加载类的二进制数据,并将其存储在方法区中。类的二进制数据通常来自于类文件,但也可以通过其他方式获取,例如网络传输或动态生成。
验证:接下来,JVM会对类的二进制数据进行验证,以确保它符合Java语言规范。这一步骤包括文件格式验证、元数据验证、字节码验证和符号引用验证等。
准备:在准备阶段,JVM会为类的静态变量分配内存,并设置初始值。初始值通常为零值,但也可以是由final关键字修饰的常量表达式。
解析:解析阶段主要是将类的符号引用替换为直接引用。符号引用是一种抽象的引用方式,它通过名称和描述符来标识目标。直接引用则是一种具体的指针或偏移量,它可以直接指向目标。
初始化:最后,在初始化阶段,JVM会执行类的初始化方法(
<clinit>
)。初始化方法由编译器自动生成,它包含了静态变量的赋值语句和静态代码块。初始化方法会按照源代码中的顺序来执行。
完成上述步骤后,类就被成功加载并初始化了。之后,Java程序就可以使用这个类来创建对象或调用静态方法了。
# 内存溢出 与 内存泄漏之间的区别
内存溢出(Out of Memory)和内存泄漏(Memory Leak)是两个不同的概念。
内存溢出指的是程序在运行过程中,需要的内存超过了系统能够分配的最大内存,导致程序无法继续运行。例如,在Java中,如果程序需要分配的堆内存超过了JVM配置的最大堆内存,就会抛出OutOfMemoryError
异常。
内存泄漏指的是程序在运行过程中,无法释放不再使用的内存,导致可用内存逐渐减少。内存泄漏通常是由于程序中存在逻辑错误或设计缺陷导致的。例如,在Java中,如果程序中存在循环引用或未关闭的资源,就可能导致内存泄漏。
内存溢出和内存泄漏之间存在一定的关联。长时间的内存泄漏可能会导致可用内存不足,从而引发内存溢出。但它们本质上是两个不同的问题,需要通过不同的方法来解决。
# 如何判断一个对象是否存活
JVM中判断对象是否存活主要依靠垃圾回收器(Garbage Collector, GC)的算法。常用的算法有引用计数法和可达性分析法。
引用计数法是一种简单的算法,它给每个对象分配一个引用计数器,用来记录该对象被引用的次数。当一个对象被引用时,它的引用计数器加1;当一个引用失效时,它的引用计数器减1。当一个对象的引用计数器为0时,说明该对象不再被任何其他对象引用,即不再存活。
可达性分析法是一种更复杂的算法,它通过遍历对象图来判断对象是否存活。在可达性分析中,JVM会从一组称为GC Roots的根节点开始遍历,沿着对象之间的引用关系向下搜索。如果一个对象可以从GC Roots到达,说明该对象仍然存活;否则,说明该对象不再存活。
两种算法都有各自的优缺点。引用计数法实现简单,但无法处理循环引用的情况;可达性分析法可以处理循环引用,但实现复杂且需要暂停程序运行。目前,主流的JVM实现都采用可达性分析法来判断对象是否存活。
# Java中可作为GC Roots的对象
在Java中,可以作为GC Roots的对象主要有以下几种:
- 虚拟机栈中的引用对象:局部变量、方法参数等。
- 方法区中的静态变量引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
这些对象都是垃圾回收器在进行可达性分析时的起始点。垃圾回收器会从这些GC Roots开始遍历,沿着对象之间的引用关系向下搜索,找到所有可以从GC Roots到达的对象。这些对象被认为是存活的,而其他无法到达的对象则被认为是垃圾,可以被回收。
# JVM中的对象引用
在JVM中,对象之间的引用关系可以分为四种类型:强引用、软引用、弱引用和虚引用。
强引用(Strong Reference):强引用是最常见的一种引用类型,它是指在程序代码中直接通过变量名访问对象的引用。只要存在强引用,垃圾回收器就不会回收被引用的对象。
软引用(Soft Reference):软引用是一种相对弱化的引用类型,它可以通过
java.lang.ref.SoftReference
类来实现。当内存空间不足时,垃圾回收器会回收软引用所指向的对象。软引用通常用于实现内存敏感的缓存。弱引用(Weak Reference):弱引用比软引用更弱,它可以通过
java.lang.ref.WeakReference
类来实现。当垃圾回收器扫描到弱引用时,无论内存空间是否充足,都会回收弱引用所指向的对象。弱引用通常用于实现规范化映射(Canonicalizing Mapping)等数据结构。虚引用(Phantom Reference):虚引用是最弱的一种引用类型,它可以通过
java.lang.ref.PhantomReference
类来实现。虚引用无法通过引用来访问对象,它主要用于跟踪对象被垃圾回收器回收的活动。虚引用通常与引用队列(Reference Queue)一起使用,以实现更精细的内存管理。
这四种类型的引用强度依次递减。在进行垃圾回收时,垃圾回收器会根据当前内存情况和对象的引用类型来决定是否回收对象。
# 为什么会有finalize()方法啊
finalize()
方法是Java对象的一个特殊方法,它在垃圾回收器回收对象之前被调用。finalize()
方法提供了一种机制,允许开发人员在对象被回收之前执行一些清理操作,例如释放非Java资源(如文件句柄、数据库连接等)。
finalize()
方法的使用需要谨慎。首先,它的执行时间是不确定的,因为垃圾回收器的运行时间是不确定的。其次,finalize()
方法可能会影响垃圾回收的性能,因为它需要在回收对象之前执行额外的操作。此外,如果finalize()
方法抛出异常,异常信息将不会被传递给调用者,而是被忽略。
因此,在实际开发中,应尽量避免使用finalize()
方法。如果需要在对象被回收之前执行清理操作,可以使用其他机制来实现,例如try-with-resources
语句或AutoCloseable
接口等。
# 垃圾回收算法有哪些啊
垃圾回收算法是垃圾回收器(Garbage Collector, GC)用来回收不再使用的对象的算法。常用的垃圾回收算法有标记-清除算法、复制算法、标记-整理算法和分代收集算法等。
标记-清除算法(Mark-Sweep):标记-清除算法分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器会遍历所有可达对象,并将它们标记为存活;在清除阶段,垃圾回收器会遍历整个堆内存,回收未被标记的对象。标记-清除算法简单易实现,但会产生内存碎片。
复制算法(Copying):复制算法将堆内存分为两个相等的区域,每次只使用其中一个区域。当进行垃圾回收时,垃圾回收器会遍历当前使用的区域中的所有可达对象,并将它们复制到另一个区域中。复制完成后,当前使用的区域中的所有对象都可以被回收。复制算法不会产生内存碎片,但会浪费一半的内存空间。
标记-整理算法(Mark-Compact):标记-整理算法结合了标记-清除算法和复制算法的优点。它在标记阶段和标记-清除算法相同,在清除阶段则不是直接回收未被标记的对象,而是将所有存活对象向一端移动,然后直接清理边界以外的内存。标记-整理算法不会产生内存碎片,也不会浪费内存空间。
分代收集算法(Generational Collection):分代收集算法根据对象的存活时间将堆内存划分为不同的区域,一般分为新生代和老年代。新生代中的对象通常存活时间较短,老年代中的对象通常存活时间较长。垃圾回收器会根据不同区域中对象的特点采用不同的垃圾回收算法。
这些垃圾回收算法各有优缺点,可以根据具体情况选择适当的算法来实现垃圾回收器。目前,主流的JVM实现都采用分代收集算法来进行垃圾回收。
# 为什么要有minor gc
Minor GC(也称为Young GC)是指在新生代中进行的垃圾回收。它是分代收集算法中的一个重要组成部分。
在分代收集算法中,堆内存被划分为新生代和老年代两个区域。新生代中的对象通常存活时间较短,大部分对象在经过一次Minor GC后就可以被回收。因此,Minor GC可以快速回收新生代中的垃圾对象,释放出空间供新对象使用。
Minor GC通常采用复制算法来进行垃圾回收。复制算法将新生代划分为两个相等的区域,每次只使用其中一个区域。当进行Minor GC时,垃圾回收器会遍历当前使用的区域中的所有可达对象,并将它们复制到另一个区域中。复制完成后,当前使用的区域中的所有对象都可以被回收。
由于新生代中的对象存活时间较短,且可达对象数量较少,因此Minor GC通常可以在较短时间内完成。这样就可以保证程序运行时有足够的内存空间来分配新对象,同时避免了频繁进行Full GC(即对整个堆内存进行垃圾回收)所带来的性能开销。
总之,Minor GC是一种针对新生代中垃圾对象的高效垃圾回收方式。它可以快速释放出内存空间,保证程序运行时有足够的内存来分配新对象。
# 为什么需要Full GC
Full GC(也称为Major GC)是指对整个堆内存(包括新生代和老年代)进行的垃圾回收。它是分代收集算法中的一个重要组成部分。
在分代收集算法中,堆内存被划分为新生代和老年代两个区域。新生代中的对象通常存活时间较短,大部分对象在经过一次Minor GC后就可以被回收。但是,也有一些对象经过多次Minor GC后仍然存活,这些对象会被晋升到老年代中。
随着程序运行时间的增加,老年代中的对象会逐渐增多。当老年代中的可用空间不足时,就需要进行Full GC来回收老年代中的垃圾对象。Full GC通常采用标记-清除算法或标记-整理算法来进行垃圾回收。这些算法会遍历整个堆内存中的所有可达对象,并将它们标记为存活。然后,垃圾回收器会回收未被标记的对象,或者将所有存活对象向一端移动,然后清理边界以外的内存。
由于Full GC需要遍历整个堆内存中的所有对象,因此它的执行时间通常比Minor GC要长。但是,Full GC可以回收整个堆内存中的垃圾对象,释放出更多的空间供程序使用。
总之,Full GC是一种针对整个堆内存中垃圾对象的垃圾回收方式。它可以释放出更多的内存空间,保证程序运行时有足够的内存来分配新对象。
# 垃圾回收器
JVM中有几种常用的垃圾回收器,它们分别针对新生代和老年代进行垃圾回收。常用的垃圾回收器有:
Serial收集器:这是一个单线程收集器,它在进行垃圾回收时会暂停所有其他工作线程,直到垃圾回收完成。它适用于单CPU环境,且对暂停时间没有特别高的要求。
ParNew收集器:这是Serial收集器的多线程版本,除了使用多线程进行垃圾回收外,其他行为与Serial收集器相同。它适用于多CPU环境,且对暂停时间有一定要求。
Parallel Scavenge收集器:这是一个新生代垃圾回收器,它关注的是吞吐量而不是暂停时间。它适用于后台计算型任务,且对暂停时间没有特别高的要求。
Serial Old收集器:这是一个老年代垃圾回收器,它使用单线程进行垃圾回收。它可以作为CMS收集器的后备预案,在CMS出现Concurrent Mode Failure时使用。
Parallel Old收集器:这是一个老年代垃圾回收器,它使用多线程进行垃圾回收。它可以与Parallel Scavenge收集器配合使用,以达到“吞吐量优先”的目标。
CMS(Concurrent Mark Sweep)收集器:这是一个老年代垃圾回收器,它关注的是最短回收停顿时间。它适用于与用户交互较多的场景,且希望系统停顿时间最短。
G1(Garbage-First)收集器:这是一个面向整个堆内存的垃圾回收器,它旨在实现可预测的暂停时间,并且能够与应用程序并发执行。
以上就是JVM中常用的几种垃圾回收器。不同的垃圾回收器有不同的特点和适用场景,在实际应用中可以根据具体情况选择合适的垃圾回收器。
# stop the world
"Stop the world"是指在进行垃圾回收时,JVM会暂停所有的应用程序线程,直到垃圾回收完成。这是因为垃圾回收需要对堆内存中的对象进行遍历和移动,如果应用程序线程在此期间继续运行,可能会修改对象的引用关系,导致垃圾回收出现错误。
"Stop the world"事件通常发生在垃圾回收开始时。具体时间取决于垃圾回收器的类型和配置。例如,在使用标记-清除算法的垃圾回收器中,"Stop the world"事件通常发生在标记阶段开始时;而在使用复制算法的垃圾回收器中,"Stop the world"事件通常发生在复制阶段开始时。
"Stop the world"事件会导致应用程序暂停响应,因此需要尽量减少其发生的次数和持续时间。不同的垃圾回收器有不同的优化策略,可以根据具体情况选择合适的垃圾回收器来减少"Stop the world"事件的影响。
# 为什么会出现CMS啊
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的垃圾收集器。它的出现是为了解决传统垃圾收集器在进行垃圾回收时会导致应用程序暂停响应的问题。
在传统的垃圾收集器中,垃圾回收需要暂停所有的应用程序线程,直到垃圾回收完成。这会导致应用程序暂停响应,影响用户体验。为了解决这个问题,CMS收集器采用了并发标记和并发清除的方式来进行垃圾回收。
在CMS收集器中,垃圾回收分为四个阶段:初始标记、并发标记、重新标记和并发清除。其中,初始标记和重新标记阶段需要暂停应用程序线程,但这两个阶段所需时间较短;而并发标记和并发清除阶段则可以与应用程序线程并发执行,不需要暂停应用程序线程。
通过这种方式,CMS收集器可以在减少应用程序暂停时间的同时完成垃圾回收。它适用于对响应时间有较高要求的场景,例如Web应用、B/S系统等。
总之,CMS收集器的出现是为了解决传统垃圾收集器在进行垃圾回收时会导致应用程序暂停响应的问题。它采用了并发标记和并发清除的方式来进行垃圾回收,可以减少应用程序暂停时间,提高用户体验。
# CMS垃圾回收过程
# 既然有了CMS 为什么还要有G1
尽管CMS垃圾回收器在减少应用程序暂停时间方面取得了很大的成功,但它仍然存在一些问题。例如,它使用的标记-清除算法会导致内存碎片,可能会影响程序的长期运行性能;它对CPU资源的占用较高,可能会影响应用程序的运行效率;它对堆内存的大小和并发线程数也有一定的限制。
为了解决这些问题,G1(Garbage-First)垃圾回收器应运而生。G1垃圾回收器是一种面向整个堆内存的垃圾回收器,它旨在实现可预测的暂停时间,并且能够与应用程序并发执行。
G1垃圾回收器将堆内存划分为多个小块,每个小块都可以作为新生代或老年代使用。在进行垃圾回收时,G1垃圾回收器会根据每个小块中垃圾对象的数量和回收成本来选择性地进行回收。这样可以避免一次性对整个堆内存进行垃圾回收,从而减少应用程序暂停时间。
此外,G1垃圾回收器还采用了并发标记和压缩算法来进行垃圾回收。这样可以避免内存碎片的产生,提高程序的长期运行性能。它对CPU资源的占用也较低,不会影响应用程序的运行效率。它对堆内存的大小和并发线程数也没有特别严格的限制。
总之,G1垃圾回收器是为了解决CMS垃圾回收器存在的问题而出现的。它采用了创新的算法和技术来进行垃圾回收,可以实现可预测的暂停时间,并且能够与应用程序并发执行。
# G1垃圾回收过程
# 虚拟机执行
# 类的生命周期
在JVM中,类的生命周期大致可以分为五个阶段:加载、连接、初始化、使用和卸载。
- 加载:查找并加载类的二进制数据到方法区中,并在堆中实例化一个Class对象。
- 连接:做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析。
- 初始化:对类的静态变量,静态代码块执行初始化操作。
- 使用:类的使用包括主动引用和被动引用。
- 卸载:当一个类不再被任何地方引用时,它就会被卸载。
# 类加载器有哪些
Java中内置了三个重要的类加载器:
- 引导类加载器(Bootstrap ClassLoader):它是最顶层的类加载器,由C++实现,负责加载JDK内部的核心类库(如%JAVA_HOME%/lib目录下的rt.jar、resources.jar、charsets.jar等)以及被-Xbootclasspath参数指定的路径下的所有类。
- 扩展类加载器(Extension ClassLoader):负责加载%JRE_HOME%/lib/ext目录下的jar包和类以及被java.ext.dirs系统变量所指定的路径下的所有类。
- 系统类加载器(System ClassLoader):也称为应用程序类加载器,负责加载当前应用classpath下的所有jar包和类。
除了这三个内置的类加载器之外,开发人员还可以通过继承java.lang.ClassLoader类来实现自定义的类加载器,以满足一些特殊需求。
# 为什么要进行双亲委派
不同等级的类加载器之间存在父子关系,它们之间遵循双亲委派模型。当一个类加载器收到类加载请求时,它会先将这个请求委托给父类加载器去完成,直到最顶层的引导类加载器。如果父类加载器无法完成这个加载请求(即在它的搜索范围内没有找到对应的类),子类加载器才会尝试自己去加载。
这种设计的目的是为了保证Java核心库的类型安全。所有的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到Java虚拟机中;如果这个过程是由Java应用自己的类加载器所完成,那么很可能会在JVM中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容的,相互不可见的(正是命名空间在发挥着作用)。借助于双亲委派机制,Java核心库中的类的加载工作都是由引导类加载器来统一完成,从而确保了Java应用所使用的都是同一个版本的Java核心库,它们之间是相互兼容的。
# 什么是双亲委派机制
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。