目录
25. Minor GC/Young GC、Major GC/Old GC、Mixed GC? 对象什么时候会进入老年代?
39. JVM内存为什么要分成新生代、老年代、持久代?新生代中为什么要分为Eden和Survivor?
41. JVM内存模型相关知识点。重排序?内存屏障?happen-before?主内存与工作内存?
69. String、StringBuffer、StringBuilder区别
1. 说说你了解的JVM内存模型
JVM内存模型简称JMM,它是Java语言规范中定义的用于管理多线程环境下数据访问的一套规则和机制。包括可见性、原子性和有序性等特点。
2. 简单说下你对JVM的了解
JVM是Java虚拟机,它实现了“一次编写,到处运行”的特点,Java源代码编译的字节码文件可以在任何支持JVM的平台运行,包括类加载器、运行时数据区、执行引擎、本地接口库,提供了跨平台的能力。
3. 说说类加载机制
类加载器负责把编写的类加载到JVM中
加载:找到类的字节码文件,把加载到内存中
链接:包括验证、准备、解析这几个阶段。验证阶段的目的是确保类的字节码文件是正确的且安全的;准备阶段是为类的静态变量分配内存并设置默认初始值;解析阶段将类、接口、字段和方法的符号引用转换成直接引用
初始化:执行静态初始化代码块和为静态变量赋值
使用:当类被加载、链接、初始化后,就可以在类中使用了
销毁:当类不再使用时,JVM会进行垃圾回收,释放相关资源
4. 说说对象的实例化过程
类加载器检查:当程序使用new关键字新建一个实例对象时,类加载器先看JVM中有没有加载这个类,如果没有加载,那么类加载器就需要加载当前类(加载、链接、初始化);如果已经加载,就不需要经过这些阶段了
分配内存:已经确定类已经被加载,接下来就要为新对象分配内存(根据内存是否规整分配,规整采用指针碰撞,不规整采用空闲列表)。
初始化零值:此时内存分配完毕,JVM会将对象的内存空间都初始化为零值(不包括对象头)。
设置对象头信息
执行构造方法来对对象进行实例化
引用指向新对象:此时实例化对象就可以使用了
5. 说说JVM的双亲委派模型
双亲委派模型是JVM中的一种类加载机制。
JVM的类加载器分为应用程序加载器(Application)、扩展类加载器(Extension)、启动类加载器(Bootstrap)。
当一个类加载器收到加载请求时,它首先不会自行加载,而是遵循一定的层级关系会将类的加载请求传递给父类,让自己的父类加载器去加载,如果父类都加载不了,才会自己加载当前类。
6. 说说JVM调优思路
首先要明确需求,是要增加吞吐量、提高响应速度还是降低延迟
对当前JVM进行性能分析:通过工具分析当前JVM的情况,如CPU使用率、内存占用情况、垃圾回收的频率等;查看GC的日志文件,看看垃圾回收方式以及垃圾回收器的种类
调整堆内存:堆的初始堆大小和最大堆大小 以及 永久代和新生代的占比
选择合适的垃圾收集器
性能测试并不断完善,持续监控JVM的性能
7. 项目中有没有实际的JVM调优经验?
7.1 CPU飙升
当CPU使用率异常提升时,通过工具或者指令进行分析。
对于计算密集型的任务,考虑是否可以采用并发或并行处理的方式来分散负载。
如果发现某些方法被频繁调用且耗时较长,可以尝试缓存结果或减少不必要的调用。
对于锁争用问题,可以尝试缩小锁的作用范围,或者使用更细粒度的锁机制,如读写锁、无锁数据结构等。
7.2 GC调优
选择合适的垃圾收集器:不同的应用场景适合不同的垃圾收集器。例如,对于延迟敏感的应用,CMS或G1垃圾收集器可能是更好的选择;而对于吞吐量优先的应用,则可能更适合Parallel GC。
调整堆大小:合理设置初始堆大小(-Xms)和最大堆大小(-Xmx),避免频繁的堆扩展操作,同时确保有足够的内存空间来支持应用运行。
新生代与老年代比例:根据应用对象生命周期的特点调整新生代(Young Generation)与老年代(Old Generation)的比例。如果年轻代过小,可能会导致频繁的Minor GC;如果过大,则可能导致Full GC的时间过长。
分析GC日志:开启GC日志(可以通过参数如-XX:+PrintGCDetails -XX:+PrintGCDateStamps),然后使用工具(如GCViewer)分析日志,找出GC行为中的模式和问题点。
优化对象分配:尽量减少临时对象的创建,尤其是大对象,因为它们会直接进入老年代,增加Full GC的压力。
8. 内存溢出和内存泄漏是什么意思?内存泄漏的原因?
内存溢出:指的是程序在申请内存时没有足够的内存分配给它,这通常会导致程序崩溃或者操作系统强制终止该程序以保护系统的稳定性。
内存泄漏:程序中已动态分配的堆内存未被释放或无法释放,造成系统内存的浪费。虽然内存被程序占用,但是已经不再使用了。
内存泄漏的原因:
1.未关闭资源,如文件驶入输出流、数据库资源等
2.长生命时期的对象持有短生命时期对象的引用,比如说静态变量集合中存放大量对象,但是这些对象被其他程序认为可以回收
3.缓存长时间不清理
9. 如何判断对象仍然存活?
有两种算法:
引用计数算法:
给对象中添加一个引用计数。每当有一个地方引用它,计数器就加1;当引用失效,计数器值就减1。任何时候,计数器值为0的对象就是不可能被使用的。
根搜索算法:
通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径被称为引用链,当一个对象到GC Roots没有任何的引用链相连(用图论的话来说就是从GC Roots到这个对象不可达),则证明此对象是不可用的。
10. JVM中一次完整的GC流程是怎样的
1.标记阶段:通过根搜索算法标记对象的存活情况。
2.对象移动阶段:根据不同垃圾收集器采用不同的垃圾收集策略对对象进行移动(标记清除算法、复制算法、标记整理算法、分代收集算法)
3.重新标记阶段:修正因为并发标记阶段因为程序运行导致的标记变化
4.整理阶段(某些垃圾收集器会进行内存碎片整理,有些则不会)
5.清理阶段:清理掉那些已经确定为不可达且不再使用的对象所占用的空间
11. 如何避免Full GC?
FullGC通常是因为堆内存满了:
1.调整堆大小:增大堆内存
2.优化堆内存占比:比如说增大新生代的占比,避免对象过早的进入老年代
3.定期检查内存泄漏,修正内存泄漏
4.使用合适的垃圾收集器
12. 说说JVM的垃圾回收机制
首先要检测对象是否存活,有两种算法:引用计数算法和根搜索算法,引用计数无法解决相互依赖引用的问题,在Java中采用的是根搜索算法。
垃圾回收算法有复制算法、标记清除算法、标记整理算法、分代收集算法。根据不同的垃圾收集器采用不同的垃圾回收策略
在进行垃圾回收时,当eden区满时会触发MinorGC(轻GC),当老年代满时就会触发Major GC/Old GC(重GC),如果堆内存都满了(新生代+老年代都满了),就会触发FullGC(全GC)
13. 说说GC的可达性分析算法
通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径被称为引用链,当一个对象到GC Roots没有任何的引用链相连(用图论的话来说就是从GC Roots到这个对象不可达),则证明此对象是不可用的。
14. 说说JVM的垃圾回收算法
标记清除算法:从根对象开始,递归地遍历所有可达的对象,并将这些对象标记为存活。扫描整个堆内存,清理未被标记的对象,释放其占用的内存。
复制算法:将堆内存分为两个区域(Eden区和Survivor区),每次只使用其中一个区域。当一个区域满时,触发GC,将存活的对象复制到另一个区域,然后清空当前区域。
标记整理算法:类似于标记-清除算法,但在清除阶段会将存活的对象向一端移动,从而避免内存碎片。
分代收集算法:年轻代采用复制算法,老年代采用标记-清除或标记-整理算法。
15. 说说七个垃圾收集器
Serial收集器:单线程的垃圾收集器,在进行垃圾收集时会暂停所有应用程序的线程。
ParNew收集器:Serial的多线程版本的垃圾收集器,能够并行执行垃圾收集操作。
Parallel Scavenge收集器:专注于通过多线程提高吞吐量的收集器,用于新生代的垃圾回收。
Serial Old收集器:Serial收集器的老年代版本,使用标记-整理算法。
Parallel Old收集器:Parallel Scavenge收集器的老年代版本,采用多线程和标记-整理算法。
CMS(Concurrent Mark Sweep)收集器:旨在最小化停顿时间,主要用于老年代的垃圾回收,采用标记-清除算法,并尽可能地与应用程序并发执行。
G1(Garbage First)收集器:面向服务端设计,将堆划分为多个区域,根据优先级来选择部分区域进行垃圾回收,既可以应用于新生代也可以应用于老年代。
16. 请你讲下CMS(并发标记清除)回收器
为了解决应用程序响应时间敏感的需求而设计的一种老年代垃圾收集器。它的主要目标是减少停顿时间,特别是尽量减少用户线程的暂停时间。
初始标记(Initial Mark):这个阶段会短暂地暂停所有应用程序线程,仅标记直接从GC Roots可达的对象。由于只需处理根对象,这个阶段非常快。
并发标记(Concurrent Mark):在这个阶段,CMS与应用程序线程并发运行,遍历对象图并标记出所有可访问的对象。因为这个过程不需要暂停应用线程,所以对系统的性能影响较小。
重新标记(Remark):这是另一个需要暂停应用线程的短暂阶段,目的是修正并发标记期间由于程序继续运行而导致的对象状态变化。此阶段使用了卡片标记技术来加速标记过程。
并发清除(Concurrent Sweep):与应用程序线程并发执行,清理掉那些未被标记的对象,释放它们占用的空间。
17. 请你讲下G1垃圾优先回收器
为现代多核处理器设计的一种高效、低延迟的垃圾收集器,旨在替代传统的CMS收集器。它特别适合于具有大堆内存的应用程序,并且能够在满足用户定义的暂停时间目标的同时提供高吞吐量。
G1的垃圾回收过程大致可分为以下几个阶段:
初始标记(Initial Mark):短暂暂停应用线程,标记直接从GC Roots可达的对象。
根区域扫描(Root Region Scan):扫描Survivor区域中的对象引用到的老年代对象,并标记它们。此阶段与应用程序并发执行。
并发标记(Concurrent Mark):与应用程序并发执行,遍历对象图并标记所有可访问的对象。
最终标记(Remark):短暂暂停应用线程,完成标记过程,包括处理并发标记期间产生的变化。
筛选回收(Cleanup and Copy):根据用户设定的停顿时间目标选择若干个垃圾最多的区域进行清理。在这个过程中,存活的对象会被复制到新的区域中,旧区域则被清理出来供后续分配使用。这个阶段也可以与应用程序并发执行部分任务。
18. 什么是JVM?
JVM(Java虚拟机)是Java语言的核心执行环境,它使得Java程序能够在任何安装有JVM的设备上运行,实现了“编写一次,到处运行”的理念。JVM负责加载、验证、执行字节码指令,并通过内存管理和垃圾回收机制自动处理对象的生命周期,同时提供了安全的执行环境以保护系统资源不受恶意代码侵害。此外,JVM还采用了多种优化技术来提升应用程序的性能。简而言之,JVM是一个抽象化的计算机,它在实际的硬件和操作系统之上提供了一个运行Java程序的平台。
19. JVM的组织架构?
JVM(Java虚拟机)的组织架构主要包括类加载器子系统、运行时数据区、执行引擎和本地接口库四大组件。
类加载器子系统负责加载编译后的字节码文件;
运行时数据区则包含了方法区、堆、虚拟机栈、本地方法栈和程序计数器,用于存储程序运行期间的数据和信息;
执行引擎承担解释和执行字节码的任务,并通过即时编译器优化性能,同时垃圾收集器管理内存回收;
本地接口库支持与非Java代码的交互。
这一架构确保了Java应用能够高效、安全地在任何支持JVM的平台上运行。
20. JVM的内存区域是什么?
JVM的内存区域主要分为五个部分:程序计数器,为每个线程私有,记录当前线程执行字节码指令的位置;Java虚拟机栈,同样线程私有,存储方法执行时的局部变量、操作数栈等信息;本地方法栈,服务于native方法执行;堆,所有线程共享,用于存储对象实例和数组,是垃圾回收的主要区域;方法区,也是共享区域,存储已被加载的类信息、常量、静态变量等内容。这些区域共同支持Java应用的运行时需求。
21. 堆和栈的区别是什么?
堆和栈是JVM中的两种重要内存区域,它们有着本质的区别:
栈(Java虚拟机栈)是线程私有的,主要用于存储方法执行期间的局部变量、操作数栈等数据,生命周期与方法调用周期一致;
堆则是所有线程共享的,用于存储对象实例和数组,负责动态分配内存,其管理主要通过垃圾回收机制来实现。
简而言之,栈管理的是方法执行的上下文及局部数据,堆则专注于对象的存储及其内存的自动管理。
22. JDK1.6、1.7、1.8内存区域的变化?
从JDK 1.6到JDK 1.8,Java内存区域经历了显著的变化,主要集中在方法区的实现上。
在JDK 1.6中,方法区通过永久代(PermGen)实现,用于存储类元数据、常量和静态变量,但其大小固定且容易导致内存溢出。
JDK 1.7开始逐步移除永久代的部分功能,例如将字符串内部缓存转移至堆中,为后续版本铺路。
到了JDK 1.8,永久代被移除,取而代之的是元空间(Metaspace),它使用本地内存,允许类元数据根据需要动态扩展,从而解决了永久代的诸多限制,如固定的内存大小和频繁的Full GC问题。这些变化改善了Java应用的性能和稳定性。
23. Java堆的内存分区了解吗?
Java堆内存主要分为两个逻辑分区:新生代(Young Generation)和老年代(Old Generation)。新生代专为新对象分配内存,进一步细分为一个Eden区和两个Survivor区(通常标记为From和To),大多数新对象首先在Eden区分配,经过Minor GC后仍存活的对象会被移动到其中一个Survivor区,之后在Survivor区间来回移动直至达到一定年龄阈值后晋升到老年代。老年代则存储生命周期较长的对象以及从新生代晋升过来的对象,当老年代空间不足时会触发Major GC或Full GC进行清理。这种分区策略有助于优化垃圾回收过程,提高系统的整体性能。
25. Minor GC/Young GC、Major GC/Old GC、Mixed GC? 对象什么时候会进入老年代?
Minor GC/Young GC主要针对新生代进行垃圾回收,当Eden区满时触发,用于清理不再使用的短期对象,通常执行速度较快;、
Major GC/Old GC专注于老年代的垃圾回收,当老年代空间不足以容纳新晋升的对象或自身空间不足时触发,由于需要处理生命周期较长的对象,其执行时间较长且可能导致较长时间的停顿;
Mixed GC则是一种结合了新生代和老年代垃圾回收的方式,尤其在使用G1收集器时,它会选择性地回收部分新生代和老年代的空间,旨在减少Full GC的频率并控制停顿时间,从而优化整体应用性能。这些不同的GC类型共同作用以维持Java应用高效、稳定地运行。
对象通常在以下情况下会进入老年代:当新生代中的对象经过多次Minor GC后依然存活,并且其年龄达到设定的阈值(默认15,可通过JVM参数调整)时,这些对象会被晋升到老年代;如果Survivor区空间不足以容纳从Eden区过来的存活对象,这些无法安置的对象也会直接进入老年代;此外,通过设置特定参数,可以让超过一定大小的大对象直接分配到老年代,以避免频繁的内存复制操作。这些机制共同决定了对象何时由新生代晋升至老年代。
26. 垃圾收集器的作用?
垃圾收集器的主要作用是自动管理Java应用程序的内存,通过识别和回收不再使用的对象所占用的内存空间,防止内存泄漏并优化内存使用效率,从而确保程序的稳定性和性能。它能够在尽量不影响或最小化影响程序运行的前提下,有效地进行内存清理和碎片整理,支持应用程序高效、安全地执行。垃圾收集器的存在极大地简化了开发者的内存管理工作,并有助于提高应用的整体性能和响应速度。
27. CMS垃圾收集器的垃圾收集过程?
初始标记(Initial Mark):这个阶段会短暂地暂停所有应用程序线程,仅标记直接从GC Roots可达的对象。由于只需处理根对象,这个阶段非常快。
并发标记(Concurrent Mark):在这个阶段,CMS与应用程序线程并发运行,遍历对象图并标记出所有可访问的对象。因为这个过程不需要暂停应用线程,所以对系统的性能影响较小。
重新标记(Remark):这是另一个需要暂停应用线程的短暂阶段,目的是修正并发标记期间由于程序继续运行而导致的对象状态变化。此阶段使用了卡片标记技术来加速标记过程。
并发清除(Concurrent Sweep):与应用程序线程并发执行,清理掉那些未被标记的对象,释放它们占用的空间。
29. 有了CMS,为什么还要引入G1?
尽管CMS(Concurrent Mark Sweep)垃圾收集器能有效减少停顿时间,但它存在内存碎片化问题且在处理大堆内存时性能下降,同时无法提供可预测的停顿时间。G1(Garbage First)通过采用分区回收和标记-整理算法,解决了内存碎片问题,并允许设定最大GC停顿时间目标,提供了更高效的内存管理和更可预测的停顿时间,特别适合需要处理大堆内存及对延迟敏感的应用场景。因此,引入G1旨在克服CMS的局限性,提供更加灵活、高效的垃圾回收解决方案。
30. 说说解释执行和编译执行的区别?
解释执行:在解释执行中,Java源代码首先被编译成字节码(.class文件)。然后,Java虚拟机(JVM)在程序运行时逐行解释这些字节码,并将其转换为机器码立即执行。
编译执行:编译执行首先将源代码(或字节码)通过编译器全部转换成本地机器码,生成可以直接由操作系统执行的二进制文件。这种做法的优点是可以提供更高的执行效率,因为编译后的机器码直接执行,无需额外的翻译步骤;缺点是初始编译时间较长,且生成的可执行文件依赖于特定的操作系统和硬件架构。适用于性能要求高的生产环境。
33. 类加载器有哪些?
启动类加载器(Bootstrap Class Loader):负责加载$JAVA_HOME/lib目录下的核心库。
扩展类加载器(Extension Class Loader):负责加载$JAVA_HOME/lib/ext目录下的扩展库。
应用程序类加载器(Application Class Loader):也称为系统类加载器,负责加载用户路径(classpath)下的类和包。
35. 为什么要用双亲委派机制?
双亲委派机制的主要目的是保证类的唯一性和安全性:
保证类的唯一性:所有委托给父类加载器加载的类都只会被加载一次,这样可以避免同一类被多次加载的情况。
保证Java平台的安全性:防止用户自定义的类冒充核心类库中的类。例如,阻止恶意代码伪装成java.lang.String类。
36. 如何破坏双亲委派机制?
虽然双亲委派机制有其重要性,但在某些情况下可能需要“破坏”它,比如实现热部署、模块化等高级功能。可以通过以下方式实现:
自定义类加载器:继承ClassLoader类并重写loadClass()方法,改变默认的双亲委派逻辑。
利用线程上下文类加载器:在某些框架中(如OSGi),可能会使用线程上下文类加载器来临时改变类加载的行为,从而绕过双亲委派模型。
37. 什么情况下会发生栈内存溢出
递归过深:当一个方法递归调用自身而没有适当的终止条件时,每次递归调用都会在栈中创建一个新的栈帧。如果递归层次过深,会导致栈空间耗尽,从而抛出StackOverflowError。
局部变量过多或过大:如果在一个方法内部定义了大量的局部变量或创建了非常大的对象(例如大数组),这些数据需要存储在该方法对应的栈帧中。如果栈帧所需的内存超出了允许的最大值,也会导致栈溢出。
无限循环中的方法调用:如果存在无限循环并且在循环体内有方法调用,这将不断消耗栈空间,最终导致栈溢出。
39. JVM内存为什么要分成新生代、老年代、持久代?新生代中为什么要分为Eden和Survivor?
JVM内存分为新生代、老年代和持久代(或元空间),以优化垃圾回收效率和管理不同类型数据。新生代进一步分为Eden区和两个Survivor区,旨在高效处理大量短命对象,通过Minor GC快速回收Eden区中的对象,并仅将存活的对象移动到Survivor区或晋升到老年代,减少不必要的对象复制和内存碎片,从而提高整体性能和内存使用效率。这种分区策略适应了对象的不同生命周期特征,确保了垃圾回收机制的灵活性和高效性。
41. JVM内存模型相关知识点。重排序?内存屏障?happen-before?主内存与工作内存?
JVM内存模型通过定义主内存与工作内存的交互规则来确保多线程程序的正确性,其中主内存存放所有线程共享的数据,而每个线程有自己的工作内存用于存储私有数据副本;重排序优化了指令执行顺序以提高性能,但可能导致多线程间的可见性和有序性问题;内存屏障用来防止特定类型的重排序,确保关键操作的顺序性;happen-before原则则提供了一组规则,明确了哪些操作先发生并对其他操作可见,从而保证了即使存在重排序,程序的行为仍然是预期的。这些机制共同作用,确保了Java程序在并发环境下的正确性和高效性。
42. 简单说说类加载器,可以打破双亲委派么?
类加载器负责在JVM中加载类文件,采用双亲委派模型以确保类的唯一性和安全性:当一个类加载请求出现时,加载器首先将请求委派给父类加载器,依次向上直到启动类加载器,这种机制避免了重复加载和潜在的安全风险。然而,在某些高级场景如模块化系统(OSGi)或自定义类加载策略中,可以通过自定义类加载器重写loadClass方法来打破双亲委派机制,实现更灵活的类加载逻辑,例如支持热部署或特定的类隔离需求。这种方式虽然提供了更大的灵活性,但也需要谨慎使用以避免破坏应用的稳定性和安全性。
43. JVM主要参数有哪些?
JVM提供了丰富的启动参数来调整其行为,以满足不同的应用需求。以下是一些常用的JVM参数:
堆内存设置:
-Xms<size>:设置JVM初始堆大小。
-Xmx<size>:设置JVM最大堆大小。
新生代设置:
-Xmn<size>:设置年轻代的大小。
-XX:NewRatio=<ratio>:设置年轻代和老年代的比例。
垃圾回收相关:
-XX:+UseG1GC:使用G1垃圾收集器。
-XX:+UseConcMarkSweepGC:使用CMS垃圾收集器。
-XX:MaxGCPauseMillis=<ms>:设置最大GC停顿时间目标(适用于G1等)。
性能调优:
-XX:PermSize=<size> 和 -XX:MaxPermSize=<size>:设置永久代大小(适用于JDK 7及之前版本;JDK 8中已被元空间取代)。
-XX:MetaspaceSize=<size> 和 -XX:MaxMetaspaceSize=<size>:设置元空间大小(JDK 8及以上版本)。
其他:
-XX:+PrintGCDetails:打印详细的GC日志信息。
-XX:+HeapDumpOnOutOfMemoryError:在发生内存溢出错误时生成堆转储文件。
44. 怎么打出线程栈信息?
要获取Java进程的线程栈信息,可以使用以下几种方法:
使用jstack工具:jstack <pid> 命令可以直接打印给定进程ID(PID)的Java进程的所有线程的栈跟踪信息。这对于分析死锁、高CPU使用率等问题非常有用。
通过JVM自带的ThreadMXBean:可以编写代码利用java.lang.management.ThreadMXBean来获取当前JVM实例中的所有线程及其状态信息,并打印出来。
发送信号(仅限Unix/Linux系统):向运行中的Java进程发送SIGQUIT信号(通常通过kill -3 <pid>),这会导致JVM将线程栈信息输出到标准输出或日志文件中。
45. 强引用、软引用、弱引用、虚引用的区别?
强引用(Strong Reference):最常见的引用类型,当一个对象通过强引用被访问时,它不会被垃圾收集器回收。只要还有强引用指向该对象,对象就不会被回收。
软引用(Soft Reference):软引用指向的对象会在内存不足时被垃圾收集器回收。通常用于实现内存敏感的缓存,只有当JVM需要确定释放一些内存以维持正常运行时,才会清除这些引用。
弱引用(Weak Reference):弱引用指向的对象一旦没有其他强引用指向它,在下一次垃圾回收时就会被回收。常用于构建弱引用集合,如WeakHashMap,自动清理不再使用的映射项。
虚引用(Phantom Reference):虚引用主要用于管理对象被回收后的资源清理工作。虚引用不会影响对象的生命周期,也无法直接通过虚引用来获取对象。通常与引用队列(ReferenceQueue)一起使用,当虚引用所关联的对象即将被垃圾收集器回收时,会将该引用加入到关联的引用队列中,从而允许开发者执行特定的清理操作。
48. 深拷贝和浅拷贝
浅拷贝(Shallow Copy):创建一个新对象,然后将现有对象的非静态字段复制给这个新对象。如果字段是基本类型,则直接复制其值;如果字段是引用类型,则复制的是引用地址,而不是引用指向的对象本身。这意味着原对象和副本对象共享引用类型的成员变量,任何一方对这些共享对象的修改都会影响到另一方。
深拷贝(Deep Copy):除了创建一个新的对象并复制所有基本类型的字段外,还会递归地复制所有引用类型的字段,即为引用类型的成员变量创建新的实例。这样,原对象和副本对象完全独立,对其中一个对象中的引用类型成员变量所做的任何更改都不会影响到另一个对象。
49. 堆栈的区别?
堆(Heap):
所有对象及其数组都在堆上分配。
堆内存由JVM自动管理,通过垃圾回收机制自动清理不再使用的对象。
堆是所有线程共享的,因此需要同步机制来保证数据的一致性。
堆的大小相对较大,且可以动态扩展。
栈(Stack):
栈内存用于存储方法调用期间的状态信息,包括局部变量、操作数栈等。
每个线程都有自己的私有栈,栈帧随着方法调用而创建或销毁。
栈的生命周期与线程相同,栈空间通常较小且固定,不适合存储大量数据。
栈的操作速度比堆快,因为栈的分配和释放只需要简单的移动指针即可完成。
50. 队列和栈的区别?
队列(Queue):
遵循先进先出(FIFO, First In First Out)原则,最早进入队列的元素最先被移除。
常见操作包括enqueue(入队)、dequeue(出队)以及查看队首元素等。
可以有阻塞队列实现,支持多线程环境下的安全访问。
栈(Stack):
遵循后进先出(LIFO, Last In First Out)原则,最后进入栈的元素最先被移除。
主要操作包括push(压栈)、pop(弹栈)以及查看栈顶元素等。
栈常用于解决诸如表达式求值、括号匹配等问题。
51. 对象的创建方式和步骤
1.通过new关键字:
类加载检查:虚拟机首先会检查这个类是否已经被加载、解析和初始化过;如果没有,则先执行相应的类加载过程。
分配内存:根据对象的大小为新对象分配足够的内存空间。分配方式有两种:指针碰撞(适用于堆内存规整的情况)和空闲列表(适用于堆内存不规整的情况)。
初始化零值:内存分配完成后,虚拟机会将分配到的内存空间都初始化为零值(不包括对象头),这一步保证了对象即使没有显式初始化也能使用默认值。
设置对象头信息:包括类的元数据指针、对象的哈希码、GC分代年龄等信息。
执行构造方法:完成上述步骤后,接着执行<init>方法,即构造函数,进行对象的初始化操作。
2.通过反射机制:
可以使用java.lang.reflect.Constructor中的newInstance()方法来创建对象实例。
3.通过序列化机制:
如果一个对象实现了Serializable接口,可以通过反序列化的方式从流中恢复对象状态。
4.通过克隆机制:
实现Cloneable接口的对象可以通过调用clone()方法创建自身的副本。
52. 对象内存分配与并发安全处理
对象的内存分配主要发生在堆上,且为了支持高并发环境下的内存分配,JVM采取了一些策略来确保安全性:
CAS(Compare And Swap)+ 失败重试:用于处理多线程环境下的内存分配竞争问题。当多个线程同时尝试分配内存时,采用CAS操作加上失败重试机制来保证原子性。
TLAB(Thread Local Allocation Buffer):为了减少同步开销,JVM为每个线程预先分配一小块私有的内存区域(TLAB)。线程需要分配对象时,优先在其TLAB内进行分配。如果TLAB已满,则需要重新申请新的TLAB或者直接在Eden区进行分配。
锁机制:对于一些特殊情况,如TLAB耗尽或大对象分配,可能仍需使用锁来保护分配过程,避免并发冲突。
53. 对象的访问定位方式
在Java中,对象的访问定位主要依赖于两种方式:
使用句柄访问:Java堆中会划分出一块区域作为句柄池,句柄包含了对象实例数据的指针和对象类型数据的指针。这种方式的优点是引用中存储的是稳定的句柄地址,对象被移动(垃圾收集时可能发生)时只需更新句柄池中的实例数据指针,而引用本身不需要修改。
直接指针访问:直接指针访问方式下,引用中直接存储了对象实例数据的地址。这种方式的优点是访问速度更快,因为无需经过额外的间接寻址步骤。现代JVM大多采用这种访问方式,因为它减少了指针解引用次数,提高了性能。
54. Java内存泄漏排查
使用JVM自带工具:
jvisualvm:提供了一个图形界面来监控和分析JVM的性能,包括内存使用情况、线程状态等。
jmap:可以生成堆转储文件(heap dump),用于分析对象分布和内存泄漏问题。
jstack:打印线程栈信息,有助于发现死锁或长时间运行的线程。
jstat:实时查看JVM的各种统计信息,如GC活动。
分析堆转储文件:
使用工具如Eclipse MAT(Memory Analyzer Tool)或VisualVM加载堆转储文件,查找哪些对象占用了大量内存,并分析它们的引用链以确定是否存在不应存在的引用。
监控GC日志:
开启GC日志(通过JVM参数如-XX:+PrintGCDetails),观察GC行为,如果频繁发生Full GC且未能有效释放内存,可能是内存泄漏的一个信号。
代码审查:
检查代码中可能导致内存泄漏的地方,如静态集合类未正确清理、监听器未移除、缓存未设置合理的过期策略等。
56. Java引用类型有哪些?
强引用(Strong Reference):最常见的引用类型,当一个对象通过强引用被访问时,它不会被垃圾收集器回收。只要还有强引用指向该对象,对象就不会被回收。
软引用(Soft Reference):软引用指向的对象会在内存不足时被垃圾收集器回收。通常用于实现内存敏感的缓存,只有当JVM需要确定释放一些内存以维持正常运行时,才会清除这些引用。
弱引用(Weak Reference):弱引用指向的对象一旦没有其他强引用指向它,在下一次垃圾回收时就会被回收。常用于构建弱引用集合,如WeakHashMap,自动清理不再使用的映射项。
虚引用(Phantom Reference):虚引用主要用于管理对象被回收后的资源清理工作。虚引用不会影响对象的生命周期,也无法直接通过虚引用来获取对象。通常与引用队列(ReferenceQueue)一起使用,当虚引用所关联的对象即将被垃圾收集器回收时,会将该引用加入到关联的引用队列中,从而允许开发者执行特定的清理操作。
57. 分代垃圾回收器的工作原理
工作流程:
Minor GC:主要针对新生代进行垃圾回收,速度快,因为大多数对象都是短命的。
Major GC/Old GC:针对老年代的垃圾回收,频率较低但耗时较长。
Full GC:同时清理新生代、老年代和元空间,是最耗时的操作,应尽量避免频繁发生。
63. JVM调优工具
JVM提供了多种工具来帮助进行性能监控和调优:
jstat:用于监控JVM的垃圾回收、类加载等统计数据,可以帮助分析GC频率和效率。
jstack:生成虚拟机当前时刻的线程快照(即threaddump),用于定位线程死锁、长时间运行的线程等问题。
jmap:生成堆转储快照(heap dump),有助于分析内存泄漏和对象占用情况。
jinfo:实时查看和调整正在运行的Java应用程序的JVM参数配置。
jconsole:基于JMX的可视化监控工具,可以监控内存使用、线程状态、类加载数量等。
VisualVM:集成了多个JDK命令行工具的功能,提供了一个图形化的界面,支持性能分析、内存泄漏检测等功能。
GC日志:通过设置相应的JVM参数(如-XX:+PrintGCDetails),可以记录详细的GC活动日志,便于后续分析。
65. G1收集器的阶段划分
G1(Garbage First)收集器是为了解决传统分代垃圾收集器的一些局限性而设计的,特别适合大内存堆的应用。G1的工作过程主要分为以下几个阶段:
Young GC(年轻代垃圾收集):当年轻代空间不足时触发,主要处理Eden区和部分Survivor区中的对象。存活的对象会被复制到新的Survivor区或直接晋升到老年代。
Initial Mark(初始标记):这是一个短暂的暂停(Stop-the-world事件),在此期间,G1会标记出所有从根对象可达的对象集合,并识别出可能包含大量可回收对象的区域。
Root Region Scanning(根区域扫描):扫描新晋升到老年代的对象引用的所有区域,这一步通常在年轻代垃圾收集后并发执行。
Concurrent Marking(并发标记):并发地遍历整个堆,找出所有存活的对象。此阶段不会导致应用长时间停顿。
Remark(重新标记):另一个短暂的暂停,确保所有存活对象都被正确地标记。这是对并发标记阶段的一个补充,以修正标记过程中发生的变动。
Cleanup(清理):清理阶段首先会对未使用的Region进行释放,同时计算各个Region的优先级,为接下来的混合收集做准备。
Mixed GC(混合垃圾收集):在某些情况下,G1会选择性地回收一部分年轻代和老年代的空间,目的是减少Full GC的发生。这个过程会持续进行直到老年代的空间被清理到一定程度。
67. OOM排查与原因分析
当Java应用程序遇到OutOfMemoryError(OOM)错误时,意味着JVM无法为新对象分配内存,这可能是由于堆内存、永久代/元空间不足或其他资源限制导致的。
确定OOM类型 Java Heap Space:最常见的OOM类型,表示堆内存不足。通常是由于创建了过多的对象或对象过大,而GC无法及时回收足够的内存。 Metaspace(或Permanent Generation,在JDK 8之前):如果加载了大量的类定义(例如通过动态类加载),可能会耗尽元空间或永久代的内存。 Unable to Create New Native Thread:不是直接的内存问题,但可能是因为系统级别的资源限制(如线程数过多)导致无法创建新的线程。 Direct Buffer Memory:使用NIO时分配的直接缓冲区超过了JVM允许的最大值。
启用并分析GC日志 使用参数如-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log来记录详细的GC活动。 分析GC日志可以帮助理解堆的使用情况、垃圾回收的频率以及每次GC后的堆剩余空间,从而判断是否因为频繁的Full GC或过小的堆大小导致OOM。
生成并分析堆转储文件 使用jmap -dump:format=b,file=/path/to/heapdump.hprof <pid>生成堆转储文件。 利用工具如Eclipse MAT(Memory Analyzer Tool)、VisualVM等分析堆转储文件,找出占用大量内存的对象及其引用链,识别潜在的内存泄漏点。
检查代码中的常见问题 静态集合类:检查是否有静态集合类存储了大量数据且未被清理。 缓存机制:确保缓存有合理的淘汰策略,避免无限增长。 长生命周期的对象持有短生命周期对象的引用:这种情况可能导致短生命周期的对象无法被GC回收。 大对象分配:对于特别大的对象,考虑是否可以优化其结构或分配方式。
调整JVM参数 根据分析结果适当调整JVM参数,比如增加堆大小(-Xms和-Xmx)、调整年轻代和老年代的比例(-XX:NewRatio)、设置元空间大小(-XX:MaxMetaspaceSize)等。 考虑更换垃圾收集器或调优现有收集器的参数以提高性能和减少停顿时间。
监控和预警 实施持续的监控方案,利用Prometheus、Grafana等工具监控JVM的内存使用情况。 设置合适的阈值和告警规则,以便在接近极限前得到通知并采取行动。
68. String对象创建机制
在Java中,String对象的创建可以通过多种方式实现,主要分为直接赋值和使用构造方法两种:
直接赋值(如 String str = "Hello";):这种方式会首先在字符串常量池中查找是否已经存在相同内容的字符串。如果存在,则直接返回该字符串的引用;如果不存在,则在常量池中创建一个新的字符串并返回其引用。这种方式有助于节省内存,因为相同的字符串共享同一个实例。
使用构造方法(如 String str = new String("Hello");):这种方式总是会在堆上创建一个新的String对象,即使字符串常量池中已经存在相同内容的字符串。这意味着可能会有两个相同的字符串存在于内存中——一个在常量池中,另一个在堆上。
此外,从JDK 7开始,对于某些操作如intern()方法,字符串常量池被移到了堆内存中,这改变了之前它位于永久代/元空间的情况,使得字符串的管理更加灵活。
69. String、StringBuffer、StringBuilder区别
String:
不可变对象,一旦创建后内容不能修改。
每次对String的操作都会生成新的String对象,性能较差,尤其是在进行大量字符串拼接时。
线程安全,适用于不需要频繁修改的场景。
StringBuffer:
可变对象,允许修改其内容而不必每次都创建新对象。
提供了一系列的方法用于追加、插入等操作。
方法是同步的(线程安全),适合多线程环境下的字符串操作,但由于同步开销,性能不如StringBuilder。
StringBuilder:
类似于StringBuffer,也是可变对象,提供了高效的字符串操作方法。
方法是非同步的(非线程安全),因此在单线程环境下比StringBuffer更高效。
如果确定不会在多线程环境中使用,应优先选择StringBuilder以获得更好的性能。
70. 主动触发垃圾回收的方法
尽管Java的垃圾收集是自动化的,但在某些情况下可能希望手动触发垃圾回收来优化资源管理或进行调试。以下是几种主动触发垃圾回收的方法:
System.gc():调用System.gc()可以建议JVM执行垃圾回收,但请注意这是一个“建议”,JVM可以选择忽略这个请求。此方法通常不会立即导致垃圾回收发生,而是由JVM根据当前系统状态决定何时执行。
Runtime.getRuntime().gc():这与调用System.gc()效果相同,实际上是同一种机制的不同调用方式。它同样只是一个建议,并不能保证立即执行垃圾回收。
VisualVM或其他监控工具:使用如VisualVM这样的工具可以在不修改代码的情况下手动触发垃圾回收。这对于测试和分析非常有用。
71. 对象finalization机制
Java中的对象finalization机制通过finalize()方法实现,允许在对象被垃圾回收器回收之前执行一些清理工作
finalize()方法:每个类都可以重写Object类中的finalize()方法。当一个对象变得不可达并且即将被垃圾收集器回收时,JVM会在适当的时候调用这个对象的finalize()方法。
72. 判断对象可回收的标准
判断一个对象是否可以被回收主要依据以下几个标准:
引用计数法(较少使用):给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。任何计数为0的对象即视为不再需要。
可达性分析算法(主流方法):通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径被称为引用链,当一个对象到GC Roots没有任何的引用链相连(用图论的话来说就是从GC Roots到这个对象不可达),则证明此对象是不可用的。
73. Java内存分配与回收策略
Java内存分配与回收策略主要包括堆内存的分区管理以及不同类型的垃圾回收算法的应用:
内存分配策略:
新生代(Young Generation):新创建的对象首先分配在这里。通常分为Eden区和两个Survivor区(From和To)。大多数对象在Minor GC后会很快死亡,存活下来的对象将被移动到Survivor区或直接晋升到老年代。
老年代(Old Generation):长期存活的对象最终会被晋升到这里。采用不同的垃圾回收算法(如标记-清除或标记-整理)以应对较少但较大的对象。
元空间(Metaspace):用于存储类的元数据信息(取代了JDK 8之前的永久代)。默认情况下,元空间位于本地内存中,并根据需要动态扩展。
垃圾回收策略:
Minor GC/Young GC:针对新生代的垃圾回收,频率较高但速度快。
Major GC/Old GC:专门处理老年代的垃圾回收,发生频率较低但耗时较长。
Full GC:同时清理新生代、老年代及元空间,是最耗费资源的操作,应尽量避免频繁发生。
G1收集器:一种分区域的垃圾收集器,旨在提供更可预测的停顿时间和更高的吞吐量。它将堆划分为多个大小相等的区域,并能够优先回收收益最大的区域。
74. CMS的STW阶段
CMS(Concurrent Mark Sweep)垃圾收集器旨在减少“Stop-the-world”(STW)事件的发生,以降低应用程序的停顿时间。然而,在其运行过程中仍然存在几个不可避免的STW阶段:
初始标记(Initial Mark):
这是一个短暂的STW阶段,主要任务是标记直接从GC Roots可达的对象集合。尽管这个过程很快,但它确实需要暂停所有应用线程来确保一致性。
重新标记(Remark):
另一个STW阶段,目的是完成标记阶段的工作。它会检查自并发标记阶段以来发生变化的对象,并修正这些变化。这是CMS中最耗时的STW阶段,因为它需要确保所有存活对象都被正确地标记。
可选的预清理(Optional Precleaning):
虽然这不是严格意义上的STW阶段,但有时在重新标记之前会有一次预清理操作,尝试并发地处理一些工作,以减少后续STW阶段的工作量。
75. 判断class对象是否相同
使用==运算符:
直接比较两个Class对象引用是否指向同一个实例。由于每个加载到JVM中的类都有唯一的Class对象表示,因此如果两个Class对象引用相等,则它们代表的是同一个类。
使用equals()方法:
Class类重写了equals()方法,基于类的全限定名进行比较。不过,对于正常情况下的类加载器和类定义,==和equals()的结果应该是一致的。
76. 类的主动使用与被动使用
主动使用包括但不限于以下场景:
创建某个类的新实例(例如通过new关键字)。
调用类的静态方法。
访问或设置类的静态字段(final修饰的静态字段除外,除非该字段是在声明时就进行了赋值)。
使用反射API调用Class.forName()方法。
初始化某个类的子类会导致先初始化父类。
JVM启动时指定的主类(包含main()方法的那个类)。
被动使用的例子包括:
通过子类访问父类的静态字段并不会触发子类的初始化。
引用类的常量字段(由编译时常量表达式初始化的static final字段),不会导致该类的初始化,因为这些常量会被内联到使用它们的地方。
加载类但不实际使用它(如仅仅加载而不创建实例或调用方法)。
77. 栈帧内部结构
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和执行的基本数据结构,每个方法在执行时都会创建一个对应的栈帧,并将其压入当前线程的Java虚拟机栈中。栈帧通常包含以下几个部分:
局部变量表:存储方法执行过程中使用的所有局部变量,包括基本数据类型、对象引用以及返回地址等。局部变量表的大小在编译期确定,并且在方法调用时分配相应的内存空间。
操作数栈:也称为操作栈,是一个后进先出(LIFO)的数据结构,用于存储计算过程中的中间结果。Java字节码指令集中的许多指令都需要从操作数栈中弹出值、执行运算并将结果推回栈顶。
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,通过这个引用可以访问到类或接口的方法信息。这使得方法能够动态地与其它方法链接起来,实现方法调用的功能。
方法返回地址:当一个方法开始执行后,程序计数器会记录下当前方法执行完毕后的返回地址,即方法调用完成后应该继续执行的字节码指令的位置。
78. 局部变量线程安全性
局部变量存储在线程的栈帧中,这意味着每个线程都有自己独立的一份局部变量副本,因此局部变量本质上是线程安全的。然而,需要注意以下几点以确保真正的线程安全性:
基本数据类型:对于基本数据类型的局部变量,由于它们是直接存储在栈上的,不同线程之间不会共享这些变量,因此是线程安全的。
对象引用:如果局部变量是指向对象的引用,则虽然引用本身是线程安全的,但被引用的对象可能不是。如果多个线程通过相同的引用访问同一个对象,那么对该对象的操作需要适当的同步机制来保证线程安全。
避免误用静态成员变量:即使是在局部作用域内,如果局部变量引用的是静态成员变量,依然可能存在并发问题,因为静态成员变量是由所有实例共享的。
80. 堆外内存分配可能性
堆外内存(Off-heap Memory)指的是不直接由Java堆管理的内存区域,它可以提供一些独特的优势:
减少GC停顿:由于数据存储在堆外,垃圾回收器不会扫描这些区域,因此可以减少因GC导致的应用暂停时间。
提高性能:对于需要频繁序列化和反序列化的场景,使用堆外内存可以直接操作原始字节流,避免了对象拷贝带来的开销。
跨进程共享:堆外内存可以在不同JVM实例之间共享,适合分布式系统或需要高效IPC(进程间通信)的场景。
Java中可以通过多种方式分配堆外内存,最常见的是通过java.nio.ByteBuffer的allocateDirect()方法,或者使用第三方库如Apache DirectMemory、Netty等。此外,JNI(Java Native Interface)也可以用来直接操作本地内存。
然而,使用堆外内存时需要注意内存泄漏问题,因为这部分内存不受JVM自动垃圾回收机制的管理,必须手动释放。如果管理不当,可能导致应用占用过多的物理内存,影响系统的稳定性和性能。
81. GC Roots对象类型
在垃圾回收过程中,确定哪些对象是可达的是非常重要的。从一组被称为“GC Roots”的对象开始遍历对象图,所有能够从GC Roots到达的对象都被认为是活动的,其余的对象则被视为垃圾。常见的GC Roots对象类型包括:
虚拟机栈(栈帧中的局部变量表)中的引用对象:当前正在执行的方法中的局部变量和参数。
方法区中的静态成员变量:类级别的静态变量引用的对象。
常量池中的引用:运行时常量池中的引用类型的常量。
本地方法栈中JNI(即一般说的Native方法)引用的对象:由本地代码(例如C/C++编写的代码)创建并持有的对象引用。
同步锁持有的对象:当前被锁定的对象,通常与线程状态相关联。
82. ZGC工作原理
ZGC(Z Garbage Collector)是专为低延迟设计的垃圾收集器,适用于大容量堆(数TB级别)。它的目标是在不影响应用程序性能的前提下实现极短的停顿时间(通常小于10毫秒)。ZGC的关键特性及工作原理如下:
并发标记周期:ZGC采用着色指针技术,每个指针包含额外的信息来跟踪对象的状态(未访问、已标记、已重映射)。这使得标记阶段可以与应用线程并发执行,而不需要停止整个应用。
并发转移周期:ZGC能够在不停止应用的情况下移动对象。当检测到某个区域的对象过于分散或接近满载时,ZGC会将对象迁移到新的位置,并更新指向该对象的所有引用。这一过程同样支持并发执行,从而减少了应用停顿时间。
读屏障(Read Barrier):为了确保在对象迁移期间,应用线程能正确访问到最新的对象位置,ZGC引入了读屏障机制。每当应用试图访问一个对象时,读屏障会检查该对象是否已被移动,并自动调整引用以指向新位置。
分代收集优化:尽管ZGC本身不区分新生代和老年代,但它通过特殊的处理策略对短期存在的对象进行快速回收,提高了整体效率。
ZGC的设计极大地减少了Full GC的发生频率,并显著缩短了垃圾回收导致的应用暂停时间,非常适合那些对响应时间和可用性要求极高的应用场景。不过,它也带来了更高的复杂性和资源消耗。
83. 查看JVM默认垃圾收集器
-
命令行参数:启动Java应用时,可以使用-XX:+PrintCommandLineFlags参数来打印出JVM的默认设置,包括使用的垃圾收集器
-
运行时查询:在运行的应用程序中,可以通过编程的方式获取当前使用的垃圾收集器信息。例如,通过读取ManagementFactory.getGarbageCollectorMXBeans()返回的信息来确定正在使用的垃圾收集器。
-
JVM版本和平台相关:不同的JVM版本以及操作系统平台可能有不同的默认垃圾收集器。例如,在较新的JDK版本中,默认可能是G1垃圾收集器;而在更早的版本中,默认可能是Serial或Parallel收集器。具体取决于JVM的版本及其配置。
85. 32位与64位JVM内存限制
32位JVM:
地址空间限制:由于地址总线宽度为32位,理论上最大可访问4GB的地址空间。
实际堆内存限制:通常小于4GB,因为除了堆之外,还需要分配给栈、方法区、程序计数器等其他内存区域。实际可用的堆大小可能只有约1.5GB到2GB左右,具体取决于操作系统和JVM实现。
64位JVM:
理论上支持非常大的堆空间(超过4GB),但实际上受限于物理内存、操作系统限制及JVM自身的设计限制。
例如,某些操作系统可能对单个进程的最大内存有限制,而JVM也可能有其内部的限制(如最大堆大小)。
86. HashMap的key注意事项
使用HashMap时,对于键的选择有一些重要的注意事项:
正确重写hashCode()和equals():为了保证HashMap能够准确地存储和检索键值对,必须确保键类正确实现了hashCode()和equals()方法。这两个方法用于确定两个键是否相等以及如何分布这些键到桶中。
不可变性:理想情况下,作为键的对象应该是不可变的。如果一个键对象在其被用作键之后发生改变,可能会导致HashMap无法正确识别该键,进而影响查找效率甚至导致数据丢失。
避免高碰撞率:设计良好的hashCode()实现可以帮助减少哈希冲突的概率。尽量使生成的哈希码均匀分布,以提高性能。
线程安全:HashMap本身不是线程安全的。若需在多线程环境中使用,考虑使用ConcurrentHashMap或其他同步机制。
87. 垃圾回收算法对比
以下是几种常见的垃圾回收算法及其特点:
标记-清除(Mark-Sweep):
优点:简单直接,适合对象生命周期差异较大的场景。
缺点:会产生内存碎片,影响后续大对象的分配;执行效率较低,尤其是在堆较大时。
复制(Copying):
优点:高效,没有内存碎片问题;适用于年轻代,因为大多数对象寿命短。
缺点:需要额外的空间开销,即需要两倍的堆空间来支持复制操作。
标记-整理(Mark-Compact):
优点:解决了标记-清除算法中的内存碎片问题,适合老年代。
缺点:相比简单的标记-清除算法,移动对象的过程增加了复杂性和时间成本。
分代收集(Generational Collection):
优点:基于大部分对象快速变得不可用的经验,优化了垃圾回收的频率和效率。
缺点:需要维护多个代,并且不同代之间需要协调工作,增加了系统的复杂性。
88. 栈溢出触发条件
递归过深:当一个方法递归调用自身而没有适当的终止条件时,每次递归调用都会在栈中创建一个新的栈帧。如果递归层次过深,会导致栈空间耗尽,从而抛出StackOverflowError。
局部变量过多或过大:如果在一个方法内部定义了大量的局部变量或创建了非常大的对象(例如大数组),这些数据需要存储在该方法对应的栈帧中。如果栈帧所需的内存超出了允许的最大值,也会导致栈溢出。
无限循环中的方法调用:如果存在无限循环并且在循环体内有方法调用,这将不断消耗栈空间,最终导致栈溢出。
线程过多:每个线程启动时都会分配一定的栈空间,默认情况下这个大小是有限的(可以通过JVM参数如-Xss设置)。如果应用程序创建了过多的线程,并且每个线程都需要较大的栈空间,那么系统总的栈空间可能会被耗尽。
91. Java程序运行原理
编写源代码:开发者使用Java语言编写.java源文件。
编译:通过javac命令将源代码编译成字节码(.class文件),这种字节码不依赖于任何特定的操作系统或硬件平台。
加载:类加载器负责将编译好的字节码加载到JVM中。
链接:包括验证(确保字节码安全)、准备(为类变量分配内存并设置默认初始值)、解析(将符号引用转换为直接引用)。
初始化:执行类构造器<clinit>方法,为类变量赋初值。
执行:JVM解释或即时编译(JIT)字节码,然后在物理机器上执行。
92. 标记整理算法特点
标记-整理(Mark-Compact)算法的特点如下:
避免碎片化:与标记-清除不同,标记-整理不仅标记活动对象,还会移动这些对象,使它们紧密排列在一起,从而消除内存碎片,有利于后续的大对象分配。
适合老年代:由于老年代的对象存活率较高,频繁的复制操作成本过高,因此标记-整理更适合处理老年代的垃圾回收。
停顿时间较长:尽管减少了碎片问题,但移动对象的过程增加了额外的时间开销,可能导致更长的GC停顿时间。
93. 程序计数器作用
程序计数器(Program Counter Register)是JVM中的一个非常小的内存区域,每个线程私有,其主要作用如下:
记录当前执行位置:保存当前线程正在执行的Java方法的字节码指令地址;如果是本地方法,则计数器值为空(undefined)。
支持多线程切换:当线程从一个方法返回或抛出异常时,能够恢复到正确的位置继续执行。这使得即使在多线程环境下也能保证每条指令按正确的顺序执行。
94. Tomcat打破双亲委派机制
Tomcat为了实现类加载隔离,特别是为了支持Web应用之间的独立性,采用了一种特殊的类加载机制,打破了传统的双亲委派模型。具体来说:
Tomcat的类加载器层次结构:除了Bootstrap类加载器和System/App类加载器之外,Tomcat还定义了自己的类加载器,如Common类加载器、WebApp类加载器等。
打破双亲委派:在默认的双亲委派模型中,当一个类加载请求出现时,首先由父类加载器尝试加载;如果父类加载器无法找到该类,则子类加载器才会尝试加载。然而,在Tomcat中,每个Web应用程序都有自己的类加载器(WebApp类加载器),这些加载器会优先尝试加载自己Web应用中的类,而不是直接委托给父类加载器。这种设计允许不同的Web应用使用不同版本的库,避免了类冲突。
95. GC日志参数解析
GC日志可以帮助我们了解垃圾收集的行为,优化JVM性能。常用的GC日志相关参数包括:
-
-XX:+PrintGCDetails:打印详细的GC事件信息,包括每次GC的原因、各代内存变化等。
-
-XX:+PrintGCDateStamps:在GC日志前加上时间戳,便于分析不同时间段的GC行为。
-
-Xloggc:<file-path>:指定GC日志文件的位置,方便集中管理和分析。
-
-XX:+PrintHeapAtGC:在每次GC前后打印堆的状态,有助于理解对象分配和回收的情况。
-
-XX:+UseGCLogFileRotation:启用GC日志文件轮转,防止单个日志文件过大。
-
通过分析这些日志,可以识别出潜在的问题,比如频繁的Full GC或过长的停顿时间,并据此调整JVM参数以优化性能。
96. JRE、JDK、JVM、JIT区别
JVM(Java Virtual Machine):Java虚拟机,是运行Java字节码的抽象计算机。它提供了一个运行时环境来执行Java程序,确保“一次编写,到处运行”。
JRE(Java Runtime Environment):Java运行时环境,包含了JVM以及标准库,提供了运行Java应用程序所需的全部环境,但不包含开发工具。
JDK(Java Development Kit):Java开发工具包,除了包含JRE的所有内容外,还包括编译器(javac)、调试器和其他开发工具(如javadoc、jar等),用于开发Java应用程序。
JIT(Just-In-Time Compiler):即时编译器,是JVM的一部分。JIT将字节码转换成本地机器代码,以便提高执行效率。它只在方法首次调用时进行编译,之后直接执行已编译的本地代码。
97. 方法内联优化
方法内联是一种重要的编译优化技术,旨在减少方法调用的开销并增加优化机会:
减少方法调用开销:通过将被调用的方法体直接插入到调用点处,避免了创建新的栈帧和保存/恢复寄存器等操作。
提高优化可能性:内联后的代码更易于进行其他类型的优化,例如常量折叠、死代码消除等,因为编译器可以看到更多的上下文信息。
JVM中的实现:JIT编译器会根据一定的启发式规则决定是否对某个方法进行内联。通常,较小的方法更容易被内联,而较大的方法则可能不会被内联,除非它们被频繁调用。
限制与挑战:尽管方法内联有许多优点,但也存在一些限制,比如过大的方法可能导致代码膨胀,增加内存占用。此外,对于递归方法或动态绑定的方法,内联可能会变得复杂甚至不可能。因此,现代JIT编译器采用了复杂的算法来权衡内联的成本和收益。
98. 无用类判定标准
在Java中,类卸载(即判断一个类是否为“无用”的标准)需要满足以下条件:
该类的所有实例已被回收:堆中不存在该类的任何对象实例。
加载该类的ClassLoader已经被回收:如果加载该类的ClassLoader没有其他引用,并且可以被垃圾收集器回收,则该类也可能成为垃圾。
该类的Class对象没有任何地方被引用:包括静态变量、方法区中的符号引用等。这意味着除了ClassLoader之外,没有其他地方持有对该类的Class对象的引用。
只有当这三个条件同时满足时,JVM才会认为这个类是可以卸载的。然而,在实际应用中,类卸载并不常见,特别是在使用了自定义类加载器的应用中,如Web容器(Tomcat)或OSGi框架等。
99. 内存使用监控方法
监控Java应用程序的内存使用情况可以通过多种方式实现:
命令行工具:
jstat:用于监控JVM的垃圾回收和类加载统计信息。
jmap:生成堆转储文件,帮助分析内存泄漏等问题。
jconsole 和 jvisualvm:提供图形化界面,能够实时查看内存使用情况、线程状态等。
编程接口:
使用java.lang.management.ManagementFactory提供的API获取运行时数据,例如MemoryMXBean可以用来监控堆和非堆内存的使用情况。
第三方工具:
如Prometheus + Grafana、New Relic等,可以集成到应用程序中进行更详细的性能监控。
100. 永久代与元空间区别
永久代(PermGen):存在于JDK 7及之前版本的方法区实现中,主要用于存储类的元数据、常量池、静态变量等。由于其大小固定,容易导致OutOfMemoryError。
元空间(Metaspace):从JDK 8开始引入,取代了永久代,直接位于本地内存中。元空间不再有固定的大小限制,默认情况下会根据需要动态扩展,但可以通过参数设置最大值。元空间主要存储类的元数据,避免了永久代带来的内存溢出问题。
101. SWAP对性能的影响
SWAP是指操作系统将物理内存中暂时不活跃的数据交换到硬盘上的交换分区或文件的过程。虽然SWAP可以在物理内存不足时提供额外的空间,但它对性能有显著影响:
增加延迟:访问磁盘比访问RAM慢得多,因此频繁的SWAP操作会导致系统响应变慢。
降低吞吐量:当大量数据被交换到磁盘时,CPU和其他组件的利用率下降,整体系统的吞吐量也会受到影响。
为了减少SWAP对性能的影响,应确保有足够的物理内存,并适当调整SWAP配置或禁用SWAP以提高性能。
104. Serial收集器特点
Serial收集器的特点如下:
单线程工作:无论是标记还是清理阶段,都只使用一个线程执行,这使得它在多核处理器环境下效率较低。
简单高效:对于单核处理器或客户端应用来说,Serial收集器非常高效,因为它不需要管理线程间的同步开销。
适用于小型应用:由于其停顿时间较长,更适合于那些对停顿时间要求不高且资源有限的应用场景。
105. GC算法与收集器关系
不同的垃圾收集器通常采用一种或多种垃圾回收算法:
Serial, Parallel, CMS, G1等都是基于标记-清除、复制或标记-整理等基础算法的具体实现。
Serial和Parallel主要采用复制算法处理年轻代,老年代则使用标记-整理算法。
CMS专注于老年代,采用标记-清除算法,并发地执行大部分工作以减少停顿时间。
G1则结合了标记-整理和分代收集的思想,通过区域划分实现高效的垃圾回收。
注意:图片来自网络,侵删!