1、说一下 JVM 的主要组成部分及其作用?
JVM(Java虚拟机)是Java程序运行的核心组件,它负责将Java字节码翻译成底层操作系统能够执行的指令。JVM由以下几个主要组成部分构成:
-
类加载器(Class Loader):负责将类的字节码加载到内存中,并进行验证、准备和解析等操作。类加载器按照一定的搜索规则查找类文件,并将其加载到内存中,使得程序可以使用这些类。类加载器还支持类的动态加载和卸载。
-
执行引擎(Execution Engine):负责执行加载到内存中的字节码指令。执行引擎可以逐行解释执行字节码,也可以将字节码编译成本地机器码再执行。常见的执行引擎有解释器和即时编译器(Just-In-Time Compiler,JIT)。
-
运行时数据区(Runtime Data Area)
:是JVM在运行过程中管理数据的区域,包括方法区、堆、栈、程序计数器和本地方法栈等。其中:- 方法区(Method Area)用于存储类的结构信息、常量池、静态变量等。
- 堆(Heap)用于存储对象实例和数组等动态分配的内存。
- 栈(Stack)用于存储线程执行方法时的局部变量、操作数栈、方法出口等。
- 程序计数器(Program Counter)用于记录线程当前执行的字节码指令位置。
- 本地方法栈(Native Method Stack)用于支持Java以外的语言调用。
-
垃圾收集器(Garbage Collector):负责自动管理和回收堆内存中不再使用的对象。垃圾收集器通过识别不可达对象,并释放它们的内存空间,从而防止内存泄漏和提高系统性能。
-
JIT编译器(Just-In-Time Compiler):JIT编译器将热点代码(经常执行的代码块)从解释器转换为本地机器码,以提高程序的执行效率。JIT编译器可以根据程序的实际执行情况进行优化,使得程序在运行过程中逐渐变得更快。
这些组成部分共同协作,使得Java程序能够跨平台运行,并提供了内存管理、安全性、性能优化等功能,使Java成为一种广泛应用的编程语言。
其实可以一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入
到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个
java.lang.Class对象,用来封装类在方法区内的数据结构。
2、说一下 JVM 运行时数据区
JVM(Java虚拟机)的运行时数据区是指在JVM运行过程中用来存储数据的不同区域,每个区域都有特定的作用和存储内容。以下是JVM主要的运行时数据区及其作用:
-
方法区(Method Area):用于存储类的结构信息、常量池、静态变量等。每个类对应一个Class对象,Class对象包含了类的结构信息,如字段、方法、父类、接口等。方法区是线程共享的区域。
-
堆(Heap):用于存储对象实例和数组等动态分配的内存。所有线程共享堆内存。堆内存可以通过-Xms和-Xmx参数来指定初始大小和最大大小。
-
栈(Stack):每个线程在执行方法时会创建一个栈帧,栈帧包含了局部变量表、操作数栈、方法出口等信息。栈内存用于存储方法的局部变量、方法参数、中间计算结果等。栈内存大小可以通过-Xss参数来指定。
-
程序计数器(Program Counter):记录当前线程执行的字节码指令位置,是线程私有的数据区域。当线程执行Java方法时,程序计数器指向下一条将要执行的指令地址。
-
本地方法栈(Native Method Stack):用于支持Java调用本地方法(Native Method),即使用JNI(Java Native Interface)调用其他编程语言编写的库。本地方法栈与Java虚拟机栈类似,区别在于本地方法栈执行的是本地方法。
这些运行时数据区域共同组成了JVM的内存模型,负责管理程序运行时所需的数据。合理管理这些区域的内存分配和释放对于程序的性能和稳定性具有重要影响。
3、深拷贝和浅拷贝
深拷贝和浅拷贝是编程中处理对象复制操作时经常遇到的概念,特别是在使用引用数据类型(如对象和数组)时。
浅拷贝(Shallow Copy):
- 浅拷贝指的是复制一个对象的时候,仅仅复制对象本身及其包含的引用,但不复制引用指向的对象本身。
- 在浅拷贝操作中,如果对象的属性是基本数据类型(如字符串、数字、布尔值等),则直接复制这些属性的值;如果属性是引用数据类型(如对象、数组等),则复制的是这些引用数据类型的引用,也就是说,原始对象和浅拷贝对象中的引用类型属性实际上指向同一块内存区域。
- 因此,如果原始对象或浅拷贝对象中的一个修改了引用类型的属性,另一个也会受到影响。
深拷贝(Deep Copy):
- 深拷贝指的是复制一个对象以及这个对象所有的引用指向的对象的复制。
- 在深拷贝操作中,无论对象的属性是基本数据类型还是引用数据类型,都会复制一份新的出来。对于引用数据类型,会递归地创建一个新的对象或数组,并将原始对象的引用类型属性的内容复制到新对象中。
- 因此,原始对象和深拷贝对象之间完全独立,一个对象的修改不会影响另一个对象。
使用场景:
- 浅拷贝通常用于创建一个对象的备份,但不需要复制嵌套对象,或者嵌套对象不会被修改的情况。
- 深拷贝通常用于需要完全分离原始对象和复制对象的情况,特别是在嵌套对象需要被独立修改时。
4、说一下堆栈的区别?
堆(Heap)和栈(Stack)是在计算机内存管理中常见的两种数据结构,它们在用途、分配方式、存储内容以及管理方式等方面有着明显的区别。
-
用途:
- 堆:用于存储动态分配的内存空间,主要用于存储对象实例和数组等。
- 栈:用于存储方法的局部变量、方法参数、中间计算结果以及方法调用的栈帧等。
-
分配方式:
- 堆:由程序员手动申请和释放内存,通常通过new关键字分配堆内存,由Java虚拟机的垃圾回收器自动回收不再使用的对象。
- 栈:由编译器自动分配和释放内存,遵循"先进后出"的原则,方法调用时会在栈上创建一个栈帧,方法执行结束时会弹出栈帧。
-
存储内容:
- 堆:存储对象的实例和数组,对象在堆上分配内存,但实际对象本身可能位于堆上或者栈上。
- 栈:存储方法的局部变量、方法参数、中间计算结果等,以及方法调用的相关信息。
-
管理方式:
- 堆:由垃圾回收器自动管理内存,负责回收不再使用的对象以及内存碎片整理等操作。
- 栈:由编译器和运行时系统自动管理,保证方法的局部变量等数据的正确分配和释放。
总的来说,堆和栈在内存管理中扮演着不同的角色。堆用于存储动态分配的内存,主要用于存储对象实例和数组;而栈用于存储方法调用的局部变量和方法调用信息。在Java中,对象实例通常存储在堆上,而方法调用的局部变量等数据则存储在栈上。
5、队列和栈是什么?有什么区别?
队列(Queue)和栈(Stack)是计算机科学中两种常见的数据结构,它们在元素存储和访问方面有着明显的区别。
-
队列(Queue):
- 队列是一种先进先出(First In First Out, FIFO)的数据结构,类似于现实生活中排队的概念。在队列中,新的元素被添加到队尾,而从队列中移除元素时则是从队头开始移除。这样保证了最先进入队列的元素会最先被取出。
- 常见的队列实现包括普通队列、双端队列和优先级队列等。
-
栈(Stack):
- 栈是一种后进先出(Last In First Out, LIFO)的数据结构,类似于现实生活中堆放盘子的概念。在栈中,新的元素被压入栈顶,而从栈中移除元素时也是从栈顶开始移除。这样保证了最后压入栈的元素会最先被弹出。
- 常见的栈实现包括普通栈和双端栈等。
-
区别:
- 主要区别在于元素的存储和访问顺序。队列是先进先出的,而栈是后进先出的。
- 队列适用于需要按照先后顺序处理元素的场景,例如任务调度、消息传递等;栈适用于需要反向处理元素的场景,例如表达式求值、函数调用等。
- 在实际应用中,队列和栈都有各自的优点和用途,具体选择取决于所需的数据操作方式和算法需求。
总的来说,队列和栈是两种常见的数据结构,它们在元素存储和访问顺序上有明显的区别,分别适用于不同的应用场景和算法需求。
6、HotSpot虚拟机对象探秘
在HotSpot虚拟机中,当遇到一条new指令时,会按照以下步骤来处理对象的创建和内存分配过程:
-
检查常量池和类加载:
- 虚拟机首先检查常量池中是否已经加载了相应的类。如果没有加载,就需要执行类加载过程,包括加载、验证、准备、解析和初始化等步骤。
-
内存分配:
- 根据Java堆中内存的规整性,选择合适的分配方式:
- 如果Java堆内存是绝对规整的,可以使用“指针碰撞”方式分配内存,即在堆内存中找到合适大小的内存块进行分配。
- 如果Java堆内存不规整,就需要从空闲列表中分配内存,这种方式称为“空闲列表”方式。
- 根据Java堆中内存的规整性,选择合适的分配方式:
-
并发处理:
- 在内存分配过程中,需要考虑并发情况,HotSpot虚拟机提供了两种并发处理方式:
- CAS同步处理:使用Compare and Swap(CAS)等原子操作来保证多线程并发访问的正确性。
- Thread Local Allocation Buffer(TLAB):为每个线程在Eden区分配一个本地线程缓冲区,避免多线程竞争分配内存时的同步问题。
- 在内存分配过程中,需要考虑并发情况,HotSpot虚拟机提供了两种并发处理方式:
-
内存空间初始化:
- 内存空间分配后,需要进行初始化操作,包括对象头信息、哈希码等的设置。
-
对象设置:
- 执行必要的对象设置,如设置对象的元信息、哈希码等。
-
执行方法:
- 最后执行对象的构造方法,完成对象的初始化。
通过以上步骤,HotSpot虚拟机能够有效地处理对象的创建和内存分配过程,同时考虑到内存的规整性、并发处理以及对象初始化等方面的问题,确保程序的正确性和性能。
7、Java会存在内存泄漏吗?请简单描述
Java确实存在内存泄漏的情况。内存泄漏指的是不再被使用的对象或变量一直占据在内存中。尽管Java拥有垃圾回收机制,理论上不再被使用的对象会被自动回收,但在某些情况下,Java程序仍然可能出现内存泄漏。
主要原因包括:
1、长生命周期对象持有短生命周期对象的引用:当一个长生命周期的对象持有一个短生命周期对象的引用时,即使短生命周期对象不再需要,它也无法被垃圾回收器回收。例如,将一个对象放入全局变量中,即使后续不再使用,由于全局变量的生命周期很长,这个对象也无法被回收。
2、内部类与外部类的引用关系:如果一个外部类的实例方法返回了一个内部类的实例对象,并且这个内部类对象被长期引用,即使外部类实例不再被使用,由于内部类持有外部类的实例引用,外部类实例也无法被垃圾回收。
3、集合类未正确清理元素:在使用集合类如ArrayList时,如果仅通过调用remove()方法删除元素,而没有将对应的引用设置为null,那么这些元素仍然可被访问,导致无法被垃圾回收。
4、静态集合类不当使用:静态集合类如HashMap、ArrayList等如果被用来存储与生命周期较长的静态变量相关的对象,这些对象即使不再需要,也难以被回收。
5、资源未关闭:未正确关闭的资源如数据库连接、文件流等,它们持有的对象也无法被回收。
6、 finalize方法滥用:过度依赖对象的finalize()方法来释放资源也可能导致内存泄漏。
总之,尽管Java拥有垃圾回收机制,但不当的编程习惯仍然可能导致内存泄漏。因此,编写代码时需要注意避免长生命周期对象持有短生命周期对象的引用,并确保及时清理不再需要的对象。
8、简述Java垃圾回收机制
Java垃圾回收机制是Java虚拟机提供的一种自动内存管理功能,用于在运行时动态回收不再被使用的对象占用的内存空间。其核心目标是通过自动管理内存,减轻程序员手动管理内存的负担,并提高程序的健壮性和稳定性。
Java垃圾回收机制主要具有以下特点:
1、自动管理:Java虚拟机会自动执行垃圾回收,程序员无需手动释放内存。
2、不可预测性:垃圾回收的触发时间和执行时间是不可预测的,程序员无法精确控制。
3、回收范围:垃圾回收主要针对Java堆内存中的对象空间,不涉及栈内存、寄存器等。
4、引用计数:垃圾回收器会跟踪对象的引用数量,当对象引用数量为0时,判定为可回收对象。
5、标记-清除:主流的垃圾回收算法包括标记-清除、复制、标记-整理等,各有优缺点。
6、finalize方法:在对象被回收前,垃圾回收器会调用对象的finalize方法,但该方法并不保证一定会被调用。
7、GC通知:程序员可以通过System.gc()来建议垃圾回收器执行垃圾回收,但具体是否执行由垃圾回收器决定。
8、性能开销:垃圾回收会占用CPU时间,影响程序性能,因此需要合理配置垃圾回收器参数。
总的来说,Java垃圾回收机制极大地简化了内存管理,但同时也存在一定的不确定性,程序员需要了解其工作原理,并采取一些措施来提高垃圾回收的效率。
9、GC是什么?为什么要GC
GC是Garbage Collection的缩写,中文称为“垃圾回收”。它是编程语言中一种自动内存管理的形式,旨在回收不再使用的内存空间,以便这些空间可以被重新分配给其他对象使用。
为什么要GC?
1、避免内存泄漏:如果没有垃圾回收机制,程序员需要手动管理内存的分配和释放。这很容易导致内存泄漏,即不再需要的内存没有被释放,随着程序运行时间的增长,内存占用会越来越高。
2、简化内存管理:垃圾回收机制使得程序员不必担心内存的分配和释放,从而可以将更多的精力集中在业务逻辑的实现上。
3、提高开发效率:手动管理内存是一个繁琐且容易出错的过程。垃圾回收机制可以自动完成这一工作,提高开发效率。
4、保证程序稳定性:内存管理不当可能导致程序崩溃或产生不可预知的行为。垃圾回收机制有助于减少这类问题。
5、适应动态内存需求:现代应用程序经常需要动态地创建和销毁对象,垃圾回收可以有效地管理这种动态内存需求。
6、优化资源使用:垃圾回收器可以根据当前系统的资源使用情况来调整回收策略,从而优化内存使用。
7、减少停顿时间:虽然垃圾回收会占用CPU时间,但现代垃圾回收器设计的目标是尽量减少停顿时间(Stop-The-World),使得垃圾回收对程序运行的影响降到最低。
垃圾回收机制在大多数现代编程语言中都是不可或缺的部分,它使得内存管理更加高效和可靠。然而,垃圾回收也带来了一些挑战,比如需要选择合适的垃圾回收策略、调整垃圾回收器参数以适应不同的应用场景,以及处理垃圾回收可能带来的性能开销。
10、垃圾回收的优点和原理。并考虑2种回收机制
垃圾回收的优点和原理:
垃圾回收是自动内存管理的一种形式,它的主要目的是自动释放程序不再使用的内存。以下是垃圾回收的一些优点:
1、避免内存泄漏:垃圾回收可以自动释放不再使用的内存,避免手动管理内存时可能出现的内存泄漏问题。
2、简化内存管理:程序员无需手动管理内存的分配和释放,可以更专注于业务逻辑的实现。
3、提高程序稳定性:垃圾回收可以减少因内存管理不当导致的程序崩溃或不可预知行为。
4、适应动态内存需求:垃圾回收可以有效地管理程序运行时动态创建和销毁的对象。
垃圾回收的原理主要是通过跟踪对象的引用来判定哪些对象是“活”的,哪些是“死”的。当一个对象没有任何“活”的引用时,它就被认为是“死”的,可以被回收。垃圾回收器会定期执行垃圾回收,释放“死”对象的内存空间。
考虑两种常见的垃圾回收机制:
标记-清除(Mark-Sweep)算法:
1、标记阶段:垃圾回收器会从一组根对象开始,标记所有从这些根对象可达的对象。
2、清除阶段:垃圾回收器会遍历堆内存,标记那些在标记阶段未被标记的对象,这些对象被视为垃圾,可以被回收。
3、回收阶段:垃圾回收器会释放那些被标记为垃圾的对象所占用的内存空间。
复制(Copying)算法:
1、复制阶段:垃圾回收器会将活着的对象从当前内存区域复制到一个新的内存区域。
2、清除阶段:垃圾回收器会清除当前内存区域,释放其内存空间。
3、交换阶段:垃圾回收器会将新内存区域设置为当前内存区域,并重复使用旧的内存区域。
标记-清除算法的优点是避免了内存的频繁复制,适合老年代对象;缺点是回收过程中会产生内存碎片。复制算法的优点是可以快速回收,并且回收后内存是连续的,没有碎片;缺点是需要额外的内存来复制对象,适合新生代对象。
11、垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
垃圾回收器的基本原理是通过检测和识别不再被程序引用的对象,然后释放它们所占用的内存空间,以便供程序后续使用。这样可以避免内存泄漏和提高内存利用率。
垃圾回收器不能马上回收内存。它会周期性地或者在系统空闲时执行垃圾回收操作。具体的回收时机由虚拟机决定,一般会受到当前系统资源的影响和垃圾回收算法的选择。
主动通知虚拟机进行垃圾回收的方式包括:
- System.gc()方法:通过调用
System.gc()
方法,程序可以建议虚拟机执行垃圾回收操作,但并不能强制要求。 - Runtime.getRuntime().gc()方法:同样地,通过调用
Runtime.getRuntime().gc()
方法也可以建议虚拟机执行垃圾回收操作。
需要注意的是,虚拟机并不一定会立即响应这些垃圾回收的建议,而是根据自身的策略和当前的系统资源情况来决定是否执行垃圾回收操作。
12、Java 中都有哪些引用类型?
在Java中,主要有四种引用类型,它们分别是:
-
强引用(Strong Reference):强引用是最常见的引用类型。当一个对象被强引用关联时,它不会被垃圾回收器回收,即使系统内存不足时也不会被回收。例如:
Object obj = new Object();
-
软引用(Soft Reference):软引用用来描述一些还有用但非必需的对象。在系统将要发生内存溢出之前,这些对象会被回收。软引用可以通过
java.lang.ref.SoftReference
类来实现。 -
弱引用(Weak Reference):弱引用是用来描述非必需对象的引用。如果一个对象只被弱引用关联,那么它在下一次垃圾回收时就会被回收。弱引用可以通过
java.lang.ref.WeakReference
类来实现。 -
虚引用(Phantom Reference):虚引用是最弱的一种引用类型。一个对象如果只被虚引用关联,那么它在任何时候都可能被垃圾回收器回收。虚引用可以通过
java.lang.ref.PhantomReference
类来实现。
这些引用类型在Java中提供了灵活的内存管理方式,使得开发人员可以更好地控制对象的生命周期和内存占用。
13、怎么判断对象是否可以被回收?
在Java中,判断一个对象是否可以被回收主要依赖于垃圾回收器使用的可达性分析算法。以下是一些常见的可达性分析算法:
1、引用计数算法:
- 每个对象都有一个引用计数器,用于记录该对象被引用的次数。
- 当一个对象被创建并初始化时,其引用计数为1。
- 当有一个新的引用指向该对象时,引用计数加1。
- 当一个引用失效时(例如,一个局部变量离开了作用域),引用计数减1。
- 当对象的引用计数变为0时,表示该对象不再被任何其他对象或变量所引用,因此可以被垃圾回收器回收。
GC Root可达性分析算法:
- 这是根搜索算法的一种扩展,不仅考虑直接从根对象可达的对象,还考虑那些通过一系列引用链可达的对象。
- 垃圾回收器会定义一系列的“GC Roots”,包括但不限于根对象,并递归地遍历这些GC Roots,标记所有可达的对象。
- 不可达的对象将被视为垃圾,可以被垃圾回收器回收。
- 在Java中,垃圾回收器会定期执行这些算法来判断哪些对象是“活”的,哪些是“死”的。当一个对象被判定为“死”时,它就可以被垃圾回收器回收,从而释放其占用的内存空间。
14、在Java中,对象什么时候可以被垃圾回收
在Java中,对象可以被垃圾回收的条件是它不再被任何“活”的线程或“活”的GC Roots所引用。以下是一些常见的条件,当满足这些条件时,对象就可以被垃圾回收:
1、对象引用计数为0:在引用计数算法中,当对象的引用计数变为0时,表示没有任何“活”的对象或变量引用该对象,因此可以被垃圾回收。
2、对象不可达:在根搜索算法中,当对象从GC Roots不可达时,表示没有任何“活”的线程或GC Roots引用该对象,因此可以被垃圾回收。
3、对象被显式设置为null:当对象的最后一个引用被显式设置为null时,该对象就不再被引用,可以被垃圾回收。
4、对象生命周期结束:当对象的生命周期结束时,例如,一个局部变量离开了作用域,它所引用的对象就不再被引用,可以被垃圾回收。
5、对象所在类被卸载:当一个类被卸载时,该类的所有实例对象都会被垃圾回收。
6、对象所在线程结束:当一个线程结束时,该线程栈中的对象都会被垃圾回收。
7、显式调用System.gc():虽然这只是一个建议,垃圾回收器可能会在调用System.gc()后执行垃圾回收,但不保证一定会回收对象。
需要注意的是,垃圾回收是Java虚拟机的一项自动管理功能,程序员无法精确控制垃圾回收的时机。垃圾回收器会根据一定的策略和算法,在适当的时机自动执行垃圾回收。
15、JVM中的永久代中会发生垃圾回收吗
在Java虚拟机(JVM)中,永久代(PermGen)是方法区的一种实现,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Java 8之前,永久代是JVM内存结构的一部分,而在Java 8及以后的版本中,永久代被元数据区(Metaspace)所取代,直接使用本地内存(Native Memory)。
对于Java 8及以后的版本,垃圾回收不会发生在元数据区,因为它是直接使用本地内存的。当元数据区满了或者超过了临界值时,会触发完全垃圾回收(Full GC),但这主要是为了清理堆内存中的对象,而元数据区的清理则由元数据区的内存管理机制来处理。
对于Java 8之前的版本,永久代中的垃圾回收主要回收两部分内容:
1、常量池中废弃的常量:如果常量池中的常量没有被任何地方引用,那么它可以被垃圾回收。
2、不再使用的类型(类):判断一个类是否不再被使用需要同时满足以下三个条件:
- 该类的所有实例都已经被回收,即堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收,这个条件通常是很难达到的。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
16、说一下 JVM 有哪些垃圾回收算法?
JVM 中主要的垃圾回收算法包括以下几种:
1、标记-清除(Mark-Sweep)算法:
标记阶段:标记所有活动的对象。
清除阶段:回收未被标记的对象所占用的空间。
优点:实现简单,不需要额外的内存空间。
缺点:会产生内存碎片,可能导致后续分配大对象时出现问题。
2、复制(Copying)算法:
将内存分为两块,每次只使用其中一块。
在垃圾回收时,将活动的对象复制到另一块内存区域,然后清理掉旧的内存区域。
优点:没有内存碎片,回收速度快。
缺点:内存利用率低,需要额外的内存空间。
3、标记-整理(Mark-Compact)算法:
标记阶段:标记所有活动的对象。
整理阶段:将所有活动的对象移动到内存的一端,然后清理掉边界以外的内存。
优点:没有内存碎片。
缺点:需要移动对象,回收速度较慢。
4、分代收集算法:
根据对象的生命周期将内存划分为不同的区域,如新生代和老年代。
新生代使用复制算法,老年代使用标记-整理或标记-清除算法。
优点:可以提高垃圾回收的效率。
17、说一下 JVM 有哪些垃圾回收器?
Java虚拟机(JVM)中常见的垃圾回收器包括以下几种:
1、串行垃圾回收器(Serial GC):
新生代使用标记-复制(mark-copy)算法。
老年代使用标记-清除-整理(mark-sweep-compact)算法。
优点:简单高效,停顿时间短。
缺点:在垃圾回收时会暂停其他所有工作线程(Stop-The-World)。
2、并行垃圾回收器(Parallel GC):
新生代使用标记-复制(mark-copy)算法。
老年代使用标记-清除-整理(mark-sweep-compact)算法。
优点:在多核CPU环境下,可以充分利用多个CPU并行处理垃圾回收,提高应用程序的吞吐量。
缺点:在垃圾回收时会暂停其他所有工作线程(Stop-The-World)。
3、并发标记清除垃圾回收器(CMS GC):
老年代使用标记-清除(mark-sweep)算法。
优点:在垃圾回收时允许应用程序的线程与垃圾回收线程并发执行,减少应用程序的停顿时间。
缺点:因为并发执行,垃圾回收的总体吞吐量会降低,同时会产生内存碎片。
4、G1(Garbage-First)垃圾回收器:
使用标记-清除-整理(mark-sweep-compact)算法。
优点:可以预测停顿时间,适合要求低延迟的大堆内存应用,同时避免内存碎片。
缺点:因为要跟踪每个区域的垃圾回收价值,所以其内部数据结构比其他回收器复杂,性能开销较大。
5、ZGC(Z Garbage Collector):
使用标记-整理(mark-compact)算法。
优点:低延迟,高并发,适合多核心机器,停顿时间不会随着堆内存的增加而增加。
缺点:目前还在持续优化中,可能会影响应用程序的性能。
18、详细介绍一下 CMS 垃圾回收器?
CMS(Concurrent Mark Sweep)垃圾回收器是Java虚拟机(JVM)中的一种垃圾回收器,主要用于回收老年代(Tenured Generation)的内存。CMS垃圾回收器的设计目标是减少应用程序的停顿时间,特别是在多线程应用程序中。以下是对CMS垃圾回收器的详细介绍:
工作原理
CMS垃圾回收器使用标记-清除(Mark-Sweep)算法,该算法包括以下几个主要阶段:
1、初始标记(Initial Mark):
这个阶段会暂停所有的用户线程(Stop-The-World事件)。
CMS垃圾回收器会标记所有从GC Roots直接可达的对象。
2、并发标记(Concurrent Mark):
在这个阶段,CMS垃圾回收器与应用程序线程并发执行。
它会标记所有从GC Roots间接可达的对象。
3、预清理(Preclean):
这个阶段也是并发执行的,它会尝试处理在并发标记阶段因为应用程序线程运行而发生变化的对象。
4、可终止的预清理(Concurrent Preclean):
这个阶段同样是并发执行的,它会再次尝试处理在并发标记和预清理阶段因为应用程序线程运行而发生变化的对象。
这个阶段可能会重复多次,直到满足某些终止条件。
5、重新标记(Final Remark):
这个阶段会再次暂停所有的用户线程(Stop-The-World事件)。
CMS垃圾回收器会重新扫描应用程序的根集合,并标记在并发标记阶段发生变化的对象。
6、并发清除(Concurrent Sweep):
在这个阶段,CMS垃圾回收器与应用程序线程并发执行,清除所有未被标记为活动的对象。
优点
1、低停顿时间:CMS垃圾回收器在并发标记和并发清除阶段与应用程序线程同时运行,减少了应用程序的停顿时间。
2、适用于多线程应用程序:CMS垃圾回收器适用于对响应时间要求较高的多线程应用程序,如Web服务器。
缺点
1、垃圾回收吞吐量较低:由于CMS垃圾回收器需要与应用程序线程并发执行,这可能会降低应用程序的总体吞吐量。
2、内存碎片:标记-清除算法不移动对象,可能导致内存碎片化,这可能会影响后续的内存分配。
3、浮动垃圾:在并发清除阶段,应用程序可能会产生新的垃圾,这些垃圾只能在后续的垃圾回收周期中被清除。
JVM参数
要启用CMS垃圾回收器,可以使用以下JVM参数:
-XX:+UseConcMarkSweepGC
此外,还可以通过其他参数来调整CMS垃圾回收器的行为,例如设置堆内存大小、触发垃圾回收的阈值等。
注意事项
CMS垃圾回收器不适用于新生代(Young Generation)的内存回收,它通常与ParNew垃圾回收器(一种新生代垃圾回收器)一起使用。
从Java 9开始,CMS垃圾回收器被标记为“deprecated”,并在Java 14中被完全移除,取而代之的是G1垃圾回收器。
CMS垃圾回收器在Java 8及之前的版本中是一种流行的垃圾回收器选择,尤其适用于需要低延迟的应用程序。然而,随着G1和ZGC等更先进的垃圾回收器的出现,CMS垃圾回收器的使用已经逐渐减少。
19、新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
新生代和老年代垃圾回收器在Java虚拟机中各有不同的实现,它们在对象生命周期、垃圾回收策略等方面存在一些区别:
新生代垃圾回收器:
1、Serial GC:单线程执行,使用复制算法,适用于单核CPU环境,简单高效但会暂停其他线程。
2、ParNew GC:Serial GC的多线程版本,适用于多核CPU环境,同样使用复制算法,但暂停时间可能更长。
3、Parallel GC:与ParNew类似,适用于多核CPU环境,使用复制算法,提供更高的吞吐量,但也会暂停其他线程。
老年代垃圾回收器:
1、Serial Old GC:Serial GC的老年代版本,单线程执行,使用标记-整理算法。
2、Parallel Old GC:Parallel GC的老年代版本,多线程执行,使用标记-整理算法,适用于多核CPU环境,提供高吞吐量。
3、Concurrent Mark Sweep (CMS) GC:以最短回收停顿时间为目标,使用标记-清除算法,回收老年代对象,但可能会产生内存碎片。
4、Garbage-First (G1) GC:将堆内存分割为多个大小相等的独立区域,根据各个区域垃圾回收的价值和成本进行优先级排序,旨在提供可控的停顿时间。
区别:
对象生命周期:新生代中的对象一般存活时间较短,而老年代中的对象存活时间较长。
垃圾回收算法:新生代垃圾回收器通常采用复制算法,而老年代垃圾回收器采用标记-整理或标记-清除算法。
停顿时间:新生代垃圾回收器的停顿时间较短,而老年代垃圾回收器的停顿时间较长。
内存碎片:新生代垃圾回收器由于采用复制算法,不会产生内存碎片,而老年代垃圾回收器可能产生内存碎片。
回收频率:新生代垃圾回收器通常更频繁地进行垃圾回收,而老年代垃圾回收器的回收频率较低。
20、简述分代垃圾回收器是怎么工作的?
分代垃圾回收器是基于这样一个观察:不同的对象有不同的生命周期,因此可以将对象根据其生命周期分成不同的代,然后对每一代使用最合适的垃圾回收策略。以下是对分代垃圾回收器工作原理的简要概述:
对象分类:在Java堆内存中,对象被分为三个代:新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation,Java 8之前)或元数据区(Metaspace,Java 8及以后)。新生代中的对象生命周期较短,而老年代中的对象生命周期较长。
新生代垃圾回收:新生代垃圾回收器(如Serial GC、ParNew GC、Parallel GC)通常使用复制算法。新生代被进一步划分为一个或多个Eden区和两个Survivor区(From和To)。新创建的对象首先在Eden区分配。当Eden区满时,进行Minor GC,将Eden区和From区中存活的对象复制到To区,然后清空Eden区和From区。To区和From区互换角色。经过多次Minor GC后,仍然存活的对象会被晋升到老年代。
老年代垃圾回收:老年代垃圾回收器(如Serial Old GC、Parallel Old GC、CMS GC、G1 GC)通常使用标记-清除或标记-整理算法。当老年代的空间不足以分配新对象时,进行Major GC(也称为Full GC),标记所有存活的对象,然后清除未被标记的对象。在某些情况下,为了减少内存碎片,会进行标记-整理,将所有存活的对象移动到老年代的一端。
永久代/元数据区垃圾回收:永久代(Java 8之前)或元数据区(Java 8及以后)用于存储类信息、常量池等数据。这部分区域的垃圾回收通常与老年代的垃圾回收一起发生,主要是清理不再使用的类信息和常量池。
分代垃圾回收器通过将对象按生命周期分类,并对每一代使用最适合的垃圾回收策略,从而提高了垃圾回收的效率和性能。新生代的Minor GC通常比老年代的Major GC更频繁,但停顿时间更短。
注意:元数据区的垃圾回收
在Java 8及以后的版本中,永久代(Permanent Generation)被元数据区(Metaspace)所取代。元数据区直接使用本地内存(Native Memory),而不是Java堆内存。它的主要作用是存储类的元数据,包括类信息、常量池、方法信息等。
关于元数据区的垃圾回收,有以下几点需要注意:
类卸载:当类的Class对象不再被引用,并且满足一定的卸载条件时(如类的所有实例都已被回收,类加载器也被回收,且没有任何地方通过反射访问该类的方法),该类的元数据可以被卸载,从而释放元数据区中的内存。
常量池清理:类似于类卸载,当常量池中的常量不再被任何地方引用时,它们也可以被清理掉。
元数据区内存管理:与Java堆内存不同,元数据区不使用传统的垃圾回收算法(如标记-清除或复制算法)。相反,它依赖于内部的内存管理机制来处理类的卸载和常量池的清理。
Full GC影响:当发生Full GC时,虽然主要是为了清理堆内存中的对象,但也会影响到元数据区。Full GC会触发元数据区的清理工作,例如卸载不再使用的类。
参数控制:元数据区的最大和最小大小可以通过JVM参数来控制,例如-XX:MaxMetaspaceSize和-XX:CompressedClassSpaceSize。
综上所述,元数据区确实会发生垃圾回收,但它是通过类卸载和常量池清理的方式来进行的,而不是使用传统的堆内存垃圾回收算法
。
21、简述java内存分配与回收策略以及Minor GC和Major GC
Java内存分配与回收策略主要包括堆内存的划分、对象的内存分配和垃圾回收算法。Java将内存划分为堆内存(Heap)和方法区(Method Area),其中堆内存用于存储对象实例,而方法区用于存储类的结构信息、静态变量、常量池等。
在堆内存中,通常将其划分为年轻代(Young Generation)、老年代(Old Generation)和永久代(在Java 8之前的版本中存在)。对象首先被分配到年轻代的Eden区,经过一段时间的存活后会被移到幸存者区(Survivor Space)。在经过多次垃圾回收后,仍然存活的对象会被晋升到老年代。
Java的垃圾回收主要通过Minor GC和Major GC来完成内存回收。
-
Minor GC:也称为新生代GC,指的是对年轻代(包括Eden区和幸存者区)的垃圾回收。在Minor GC中,会清理年轻代中的无用对象,发生频率比较高。通常采用复制算法进行垃圾回收,即将存活对象复制到幸存者区,清理Eden区和幸存者区中的无用对象。
-
Major GC:也称为老年代GC,指的是对老年代的垃圾回收。在Major GC中,会清理老年代中的无用对象,发生频率相对较低。通常采用标记-清除或标记-整理算法进行垃圾回收,以清理老年代中较大的对象。
通过这种分代的内存管理和垃圾回收策略,Java可以更高效地管理内存,减少内存碎片化问题,并提高垃圾回收的性能。同时,Java虚拟机还提供了不同的垃圾收集器(如Serial GC、Parallel GC、CMS GC、G1 GC等),可以根据应用场景和需求选择合适的垃圾收集器来优化性能。
22、简述java类加载机制?
Java类加载机制是指在程序运行过程中,将类的字节码文件加载到内存中,并转换为运行时数据结构的过程。Java类加载机制主要包括以下几个步骤:
-
加载(Loading):加载是指通过类加载器将类的字节码文件加载到内存中。在加载阶段,类加载器会根据类的全限定名(fully qualified name)查找类文件,并读取其二进制数据流。
-
验证(Verification):验证阶段对加载的类进行验证,确保其符合Java虚拟机规范和安全规范,以防止恶意代码的执行。
-
准备(Preparation):准备阶段是为类的静态变量分配内存并设置默认初始值(零值),这些变量会在类加载的初始化阶段被显式赋值。
-
解析(Resolution):解析阶段是将类、接口、字段和方法的符号引用解析为直接引用的过程,以便于后续的访问和使用。
-
初始化(Initialization):初始化阶段是类加载过程的最后一步,会执行类构造器()方法,对类的静态变量进行显示赋值等操作。
需要注意的是,类加载过程中的验证、准备和解析并不是必需的,而是根据需要来执行的。此外,Java类加载机制采用双亲委派模型,即除了启动类加载器(Bootstrap ClassLoader)外,每个类加载器在加载类时都会先委托给其父类加载器进行加载,只有在父类加载器无法加载时才由子类加载器自行加载。
通过类加载机制,Java实现了动态加载和运行时多态性,使得程序可以根据需要动态加载类,并在运行时适应不同的环境和需求。
23、描述一下JVM加载Class文件的原理机制
JVM加载Class文件的原理机制如下:
1、类加载器(ClassLoader):JVM使用类加载器来加载Class文件。类加载器本身也是一个类,它的主要职责是将Class文件从文件系统或网络等位置加载到JVM内存中。
2、加载过程:当程序首次使用一个类时,类加载器会进行加载。加载过程包括读取Class文件并解析,将其转换为JVM能够识别的数据结构,然后将这些数据结构存放在方法区(Method Area)中。
3、连接过程:连接过程包括验证、准备和解析(可选)。
验证:确保Class文件的字节流符合JVM规范,没有安全方面的问题。
准备:为类变量分配内存,并设置默认初始值。
解析:将类、接口、字段和方法的符号引用转换为直接引用。
4、初始化:初始化阶段是执行类构造器()方法的过程,该方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。这个阶段会执行静态变量赋值和静态代码块。
5、使用:当初始化完成后,Class对象可以被用来创建实例、调用方法等。
6、卸载:当Class对象不再被引用,即无法在任何地方通过反射访问该类的方法,并且满足一定的卸载条件时(如类的所有实例都已被回收,类加载器也被回收),该类的Class对象和元数据会被卸载,从而释放方法区中的内存。
类加载器层次结构:Java中的类加载器有三个,分别是Bootstrap Loader、ExtClassLoader和AppClassLoader。它们各自负责不同类型的类的加载:
Bootstrap Loader:负责加载JDK的核心库,如rt.jar。
ExtClassLoader:负责加载JDK的扩展库,如jre/lib/ext目录下的库。
AppClassLoader:负责加载应用程序自身的类路径(Classpath)下的类库。
委托机制:当类加载器需要加载一个类时,它会首先委托给其父类加载器进行加载。只有当父类加载器无法加载该类时,才由自己来加载。
通过以上机制,JVM能够动态地加载和卸载类,实现了Java程序的运行时灵活性。
24、什么是类加载器,类加载器有哪些?
类加载器(ClassLoader)是Java虚拟机(JVM)的一个重要组成部分,负责将类的字节码文件加载到内存中并生成对应的Class对象。类加载器通过将类的字节码文件转换为运行时的Java类,并在程序运行时动态加载和链接类,实现了Java的动态性和灵活性。
在Java中,类加载器主要分为以下几种类型:
-
启动类加载器(Bootstrap ClassLoader):负责加载Java核心类库(如rt.jar),是JVM的一部分,并不继承自java.lang.ClassLoader类。
-
扩展类加载器(Extension ClassLoader):负责加载Java的扩展类库(如jre/lib/ext目录下的jar包)。
-
应用程序类加载器(Application ClassLoader):也称为系统类加载器,负责加载应用程序类路径(Classpath)上指定的类库。
-
自定义类加载器(Custom ClassLoader):开发者可以根据需要自定义类加载器,继承自java.lang.ClassLoader类,实现自定义的类加载策略。
除了以上常见的类加载器外,还有一些特殊的类加载器,如Web应用程序中的WebAppClassLoader、OSGi中的BundleClassLoader等。
类加载器按照双亲委派模型进行工作,即每个类加载器在加载类时都会先委托给其父类加载器进行加载,只有在父类加载器无法加载时才由子类加载器自行加载。这种层次结构保证了类的唯一性和安全性,避免同一个类被不同的类加载器加载多次,同时也提高了类的可移植性和共享性。
通过合理使用类加载器,可以实现类的动态加载、版本隔离、热部署等功能,为Java语言的灵活性和扩展性提供了良好的支持。
25、说一下类装载的执行过程?
类加载的执行过程可以大致分为以下几个步骤:
-
加载(Loading):当程序需要使用某个类时,类加载器会首先检查该类是否已经被加载。如果尚未加载,则类加载器会根据类的全限定名(包括包路径)在文件系统或网络中查找对应的.class文件,然后读取该文件的字节码数据。
-
链接(Linking):链接阶段包括验证、准备和解析三个步骤。
- 验证(Verification):验证阶段确保类的字节码符合JVM规范,不会危害虚拟机的安全。
- 准备(Preparation):在准备阶段,为类的静态字段分配内存并设置初始值(通常为零值,如int类型初始化为0)。
- 解析(Resolution):解析阶段将常量池中的符号引用转换为直接引用,例如将方法调用转换为实际的内存地址。
-
初始化(Initialization):在初始化阶段,JVM负责执行类构造器()方法,该方法主要包括对静态字段的显示赋值和静态代码块的执行。这一阶段标志着类加载过程的完成,类被正式初始化为可用状态。
需要注意的是,在链接和初始化阶段,类加载过程可能会触发对其他类的加载,从而形成类加载的递归调用。此外,类加载过程中还涉及到双亲委派模型的机制,即类加载器在加载类时会先委托给其父类加载器进行加载,只有在父类加载器无法加载时才由子类加载器自行加载。
通过类加载的执行过程,Java实现了动态加载和多态性的特性,使得程序可以按需加载类,并在运行时适应不同的环境和需求。
26、什么是双亲委派模型?
双亲委派模型(又称双亲委派机制)是Java类加载器的一种工作原则,用于保证类的唯一性和安全性。该模型规定了类加载器在加载类时首先委托给父类加载器进行加载,只有在父类加载器无法加载时才由子类加载器自行加载。
具体来说,当一个类加载器收到加载类的请求时,它会先检查自身是否已经加载了这个类。如果没有,则会将加载请求委托给父类加载器。父类加载器也会按照同样的方式继续向上委托,直至达到最顶层的启动类加载器。只有当所有父类加载器都无法加载该类时,当前类加载器才会尝试自行加载。
双亲委派模型的优势在于确保每个类被加载且仅被加载一次,避免了类的重复加载,提高了系统的安全性和稳定性。同时,该模型也使得Java核心类库能够被启动类加载器优先加载,确保了核心类库的一致性和统一性。
通过双亲委派模型,Java类加载器实现了类加载的层次化管理,有效地解决了类加载过程中可能出现的类冲突和安全性问题,为Java语言的可移植性和跨平台性提供了良好的支持。
27、说一下 JVM 调优的工具?常用的 JVM 调优的参数都有哪些?JVM调优是优化Java应用程序性能和资源利用的重要手段之一。以下是一些常用的JVM调优工具和参数:
JVM调优工具:
-
VisualVM:VisualVM是JDK自带的一款图形化监控和分析工具,可以监控应用程序的性能数据、堆栈信息等,并提供基于JMX的远程监控功能。
-
JConsole:JConsole是JDK自带的一个监控和管理工具,可以实时监控Java应用程序的资源利用情况、线程状态等,并进行相关操作。
-
JVisualVM:JVisualVM是VisualVM的前身,提供了更多的插件和扩展功能,可以进行堆内存分析、线程分析等。
-
Java Mission Control:Java Mission Control是Oracle JDK附带的一款高级监控工具,提供了更丰富的性能分析和故障诊断功能。
常用的JVM调优参数:
-
堆内存设置:
-Xms<size>
:初始堆大小-Xmx<size>
:最大堆大小
-
GC相关参数:
-XX:+UseG1GC
:启用G1垃圾收集器-XX:MaxGCPauseMillis=<time>
:指定最大GC停顿时间-XX:NewSize=<size>
:新生代大小-XX:SurvivorRatio=<ratio>
:Eden区与Survivor区的比例
-
调优日志:
-XX:+PrintGC
:打印GC日志-XX:+PrintGCDetails
:打印更详细的GC日志-Xloggc:<file>
:将GC日志输出到指定文件
-
性能监控:
-Dcom.sun.management.jmxremote
:启用JMX远程监控-Dcom.sun.management.jmxremote.port=<port>
:指定JMX端口
-
类加载相关:
-XX:+TraceClassLoading
:跟踪类加载过程-XX:+TraceClassUnloading
:跟踪类卸载过程
-
编译优化:
-XX:+AggressiveOpts
:启用更激进的编译优化