1、说说内存溢出跟内存泄漏的区别?
- 内存泄露:申请的内存空间没有被正确释放,导致内存被白白占用。
- 内存溢出:申请的内存超过了可用内存,内存不够了。可能是泄漏导致的。
2、如何判断对象仍然存活?jvm是怎么判断某个对象是不是垃圾的?
两种方式:引用计数算法、可达性分析算法
引用计数算法:
引用计数器:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;
当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
可达性分析算法:
将一系列 GC Roots 作为初始的存活对象合集(Gc Root Set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,
这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。
3、GC Roots 有哪些?
「「固定的GC Roots:」」
- 1.在「「虚拟机栈(栈帧的本地变量表)中所引用的对象」」,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中「类静态属性引用的对象」,譬如 Java 类的「引用静态变量」。
- 在方法区中「「常量引用的对象」」,譬如字符串常量池中的引用。
- 在方法区栈中 「「JNI (譬如 Native 方法)引用的对象」」。
- Java 「「虚拟机内部的引用」」,如基本数据类型对应的 Class 对象,一些常驻的异常对象(空指针异常、OOM等),还有类加载器。
- 所有「「被 Synchronized 持有的对象」」。
- 反应 Java 虚拟机内部情况的 「「JMXBean、JVMTI 中注册的回调本地代码缓存等」」。
「「临时GC Roots:」」
- 「「为什么会有临时的 GC Roots ?」」:目前的垃圾回收大部分都是「「分代收集和局部回收」」,如果只针对某一部分区域进行局部回收,那么就必须要考虑的「「当前区域的对象有可能正被其他区域的对象所引用」」,这时候就要将这部分关联的对象也添加到 GC Roots 中去来确保根可达算法的准确性。这种算法是利用了「「逆向思维」」,找到使用的对象,剩下的就是垃圾,也被称为"间接垃圾收集"。
4、什么时候触发Full GC?
一共有6种触发条件:
- System.gc()等命令触发:System.gc()、jmap -dump 等命令会触发 full gc。
- Young GC 之前检查老年代:在要进行 Young GC 的时候,发现老年代可用的连续内存空间 < 新生代历次Young GC后升入老年代的对象总和的平均大小,说明本次 Young GC 后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,那就会触发 Full GC。
- Young GC 之后老年代空间不足:执行 Young GC 之后有一批对象需要放入老年代,此时老年代就是没有足够的内存空间存放这些对象了,此时必须立即触发一次 Full GC
- 老年代空间不足,老年代内存使用率过高,达到一定比例,也会触发 Full GC。
- 空间分配担保失败( Promotion Failure),新生代的 To 区放不下从 Eden 和 From 拷贝过来对象,或者新生代对象 GC 年龄到达阈值需要晋升这两种情况,老年代如果放不下的话都会触发 Full GC。
- 方法区内存空间不足:如果方法区由永久代实现,永久代空间不足 Full GC。
5、对象什么时候会进入老年代?
一共有四种情况:
- 长期存活的对象将进入老年代:在对象的对象头信息中存储着对象的迭代年龄,迭代年龄会在每次 YoungGC 之后对象的移区操作中增加,每一次移区年龄加一.当这个年龄达到 15(默认)之后,这个对象将会被移入老年代。#可以通过这个参数设置这个年龄值。- XX:MaxTenuringThreshold
- 大对象直接进入老年代:有一些占用大量连续内存空间的对象在被加载就会直接进入老年代.这样的大对象一般是一些数组,长字符串之类的对。#HotSpot 虚拟机提供了这个参数来设置。-XX:PretenureSizeThreshold
- 动态对象年龄判定:HotSpot 虚拟机并不是永远要求对象的年龄必须达到- XX:MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
- 空间分配担保:假如在 Young GC 之后,新生代仍然有大量对象存活,就需要老年代进行分配担保,把 Survivor 无法容纳的对象直接送入老年代。
6、CMS垃圾回收器的缺点有哪些?
优点:CMS 最主要的优点在名字上已经体现出来——并发收集、低停顿。
缺点:CMS 同样有三个明显的缺点。
- Mark Sweep(标记-清除) 算法会导致内存碎片比较多
- CMS 的并发能力比较依赖于 CPU 资源,并发回收时垃圾收集线程可能会抢占用户线程的资源,导致用户程序性能下降。
- 并发清除阶段,用户线程依然在运行,会产生所谓的理“浮动垃圾”(Floating Garbage),本次垃圾收集无法处理浮动垃圾,必须到下一次垃圾收集才能处理。如果浮动垃圾太多,会触发新的垃圾回收,导致性能降低。
G1 主要解决了内存碎片过多的问题。
7、什么是双亲委派?为什么要这样?
双亲委派模型的工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类
而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此
因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时 ,子加载器才会尝试自己去完成加载。
为什么这样?
答案是为了保证应用程序的稳定有序。保证安全。
例如类 java.lang.Object,它存放在 rt.jar 之中,通过双亲委派机制,保证最终都是委派给处于模型最顶端的启动类加载器进行加载,保证 Object 的一致。反之,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中就会出现多个不同的 Object 类。
8、如何打破双亲委派?什么时候有用?
如果想打破双亲委派模型则需要重写 loadClass()方法。
如何打破双亲委派模式?
-
继承重写(Override Inheritance):可以通过创建一个新的子类来覆盖父类的方法或属性。这种方法允许你修改或扩展原有的行为。但需要注意,这可能会导致你需要处理更多的细节,特别是对于继承链的管理和代码的复杂性。
-
装饰器模式(Decorator Pattern):使用装饰器模式可以在运行时动态地扩展一个对象的功能。通过添加装饰器,你可以在不修改原有对象的情况下,为其增加新的行为或修改现有行为。
-
事件监听(Event Listening):有些情况下,你可以通过注册事件监听器来捕获和处理对象的特定行为。这种方式通常用于需要在对象行为发生时采取一些特定动作的场景。
-
依赖注入(Dependency Injection):在某些情况下,通过依赖注入的方式可以替代双亲委派模式。依赖注入允许你在运行时动态地改变对象的依赖关系,从而实现定制化的行为。
什么时候有用?
打破双亲委派模式通常在以下情况下是有用的:
-
定制化需求:当你需要根据具体的需求修改或扩展现有的功能时,打破双亲委派模式可以让你更灵活地实现这些定制化需求。
-
不同的业务逻辑:如果不同的部分需要不同的行为或处理方式,你可能需要打破双亲委派模式以便针对不同的情况提供不同的实现。
-
性能优化:有时候,通过打破双亲委派模式可以实现更高效或更优化的实现方式,特别是在需要处理大量数据或频繁调用的情况下。
总之,打破双亲委派模式通常是为了实现更灵活、更定制化的软件设计,以满足特定的业务需求或性能优化要求。
9、为什么内存超过32G的64位JVM开启指针压缩优化失败?
在Java虚拟机(JVM)中,指针压缩(Pointer Compression)是一种优化技术,它可以减少指针的大小,从而在一定程度上降低内存占用。指针压缩通常在32位JVM上启用,因为32位系统的寻址空间有限,指针占用的内存较大。但在64位JVM上,由于寻址空间非常广阔(2的64次方),指针的大小本身不再是内存占用的瓶颈,因此默认情况下,64位JVM是不启用指针压缩的。
然而,即使在64位JVM上,有时也可以选择手动启用指针压缩。指针压缩的启用取决于几个因素:
-
堆内存的大小限制:在32位JVM中,由于地址空间限制,Java堆的大小通常受到4GB的限制。在64位JVM中,由于寻址空间更大,理论上可以支持非常大的堆内存,超过32GB。
-
启用指针压缩的条件:在64位JVM中,如果你希望启用指针压缩,需要考虑JVM实现是否支持,并且需要确保压缩后的指针可以正确映射到有效的地址范围内。通常,指针压缩的有效性受到堆内存大小和对象的内存分配模式的影响。
-
默认情况下的行为:
- 64位JVM默认情况下不启用指针压缩,因为64位地址空间很大,指针大小本身不是内存占用的主要限制因素。
-
手动启用指针压缩:
- 在一些特殊情况下,你可以手动启用指针压缩来降低内存占用。这通常需要通过JVM启动参数来完成。
-
常见的启动参数:
-XX:+UseCompressedOops
:这是用于启用指针压缩的参数。Oops
是Java HotSpot VM中对“ordinary object pointers”的简称。-XX:+UseCompressedClassPointers
:这是用于启用类指针压缩的参数。当启用该选项时,JVM会尝试将类指针进行压缩,以节省更多内存。
-
启用示例:
- 若要启用指针压缩,可以在JVM启动命令中添加如下参数:
java -XX:+UseCompressedOops -XX:+UseCompressedClassPointers YourMainClass
- 这将使JVM在运行时尝试使用压缩指针和类指针的技术来优化内存使用。
- 若要启用指针压缩,可以在JVM启动命令中添加如下参数:
为什么呢?
我们来算一下,既然指针压缩到了4byte,也就是32bit,同样按照之前算的,用排列组合的方式可以识别2^32个对象,也就是4G个对象,刚才同样也说了,在Java中,非简单对象都是必须以8byte对齐。
因此,其能够识别的最大内存就是4G*8byte=32GB。
这也是为什么很多Java服务在运行中,官方都建议单个运行实例的内存设置不要超过32GB的根本原因。典型的如Elasticsearch,很多资料都说设置JVM大小不要超过32G,但是很少有提到为什么。
10、类加载过程是什么样的?
关于JVM必备的一些知识-CSDN博客
11、jvm内存区域有哪些?分别的作用是什么?
关于JVM必备的一些知识-CSDN博客
堆内存结构:
12、jvm内存分配过程中的TLAB有什么用?
TLAB 是 Java 虚拟机(JVM)中的 "Thread-Local Allocation Buffer" 的缩写,即线程本地分配缓冲区。TLAB 是为了优化对象分配和提高程序性能而引入的概念。
在 Java 中,每当需要分配新的对象时,JVM 通常会使用堆内存来进行分配。为了减少线程竞争和提高分配效率,JVM 会为每个线程预分配一个 TLAB。每个线程都有自己的 TLAB,这样在对象分配时,线程可以直接在自己的 TLAB 上分配内存,而不需要每次都去竞争堆上的全局锁。这种设计有效地减少了多线程环境下的内存分配的竞争和同步开销,从而提高了并发程序的性能。
总结一下,TLAB 是 JVM 中的线程本地分配缓冲区,用于提高对象分配的效率和降低竞争。
13、逃逸分析跟栈上分配是什么?有什么用?
逃逸分析(Escape Analysis)和栈上分配(Stack Allocation)是 Java 虚拟机优化技术的一部分,主要用于提升程序的性能和内存利用率。
-
逃逸分析(Escape Analysis):
- 定义:逃逸分析是指编译器分析对象的动态作用域,即对象的引用是否逃出了方法的作用域。如果对象的引用没有逃出方法的作用域,编译器可以安全地将其分配在栈上,而不是在堆上。逃逸分析的主要目标是识别哪些对象可以在方法栈帧上分配,从而避免频繁的堆内存分配和垃圾回收操作。
- 用途:逃逸分析可以显著提升程序的性能,尤其是在多线程环境下。如果对象被分配在栈上,它们的访问速度更快,因为栈上分配不需要加锁或者同步操作,而且这些对象的生命周期仅限于方法执行期间,减少了垃圾收集器的压力。
-
栈上分配(Stack Allocation):
- 定义:栈上分配是指将对象分配到当前线程的方法栈帧上,而不是分配到堆上。这些对象的生命周期与方法的生命周期相同,当方法返回时,它们自动被销毁,无需进行垃圾回收。
- 用途:栈上分配可以显著减少垃圾收集器的负担,因为这些对象不需要进行垃圾回收。同时,栈上分配还可以提高程序的执行速度,因为在方法栈帧上分配对象比在堆上分配对象更快速,不需要进行复杂的内存管理操作。
综上所述,逃逸分析和栈上分配都是优化技术,通过减少对象的堆分配和垃圾回收操作来提升程序的性能和响应速度。逃逸分析用于确定对象的引用是否会逃出方法作用域,以便安全地进行栈上分配,从而达到优化的目的。
14、Java对象的内存布局有了解吗?
对象内存布局
- 「1.对象头」: 对象头又分为 「MarkWord」 和 「Class Pointer」 两部分。
- 「MarkWord」:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位,gc记录信息等等。
- 「ClassPointer」:用来指向对象对应的 Class 对象(其对应的元数据对象)的内存地址。在 32 位系统占 4 字节,在 64 位系统中占 8 字节。
- 「2.Length」:只在数组对象中存在,用来记录数组的长度,占用 4 字节
- 「3.Instance data」: 对象实际数据,对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。(这里不包括静态成员变量,因为其是在方法区维护的)
- 「4.Padding」:Java 对象占用空间是 8 字节对齐的,即所有 Java 对象占用 bytes 数必须是 8 的倍数,是因为当我们从磁盘中取一个数据时,不会说我想取一个字节就是一个字节,都是按照一块儿一块儿来取的,这一块大小是 8 个字节,所以为了完整,padding 的作用就是补充字节,「保证对象是 8 字节的整数倍」。
15、java 中 finalize(),类似c语言中的析构函数,介绍一下?
利用这个点,可以让即将被回收的对象可以有最后的机会逃逸。不建议使用finalize()函数。
在 Java 中,finalize()
方法是一个特殊的方法,用于在对象被垃圾收集器回收之前执行一些清理操作。这个方法具体涉及到以下几个方面:
1. 作用和用途:
-
资源释放和清理:
finalize()
方法允许在对象即将被垃圾收集器回收之前执行最后的资源释放和清理操作,如关闭文件、释放网络连接等。 -
对象状态的清理:在某些情况下,对象的状态可能需要在被回收前进行特定的清理,以避免资源泄露或其他不良影响。
2. 执行时机和注意事项:
-
非确定性:
finalize()
方法的执行时机不确定,这意味着不能依赖它来进行关键资源的释放或复杂的对象状态管理。垃圾收集器在决定回收对象时,可能会调用finalize()
方法,但并不保证一定会调用,也不能确定何时调用。 -
性能影响:由于
finalize()
方法执行的时机不确定,可能会导致垃圾收集器的性能问题。因此,官方建议尽量避免使用finalize()
方法,而是通过显式的资源管理(如使用try-with-resources
、finally
块等)来确保资源的及时释放和对象状态的正确管理。
3. 示例:
以下是一个简单的示例,展示了如何在 Java 中使用 finalize()
方法来释放对象的资源:
public class ResourceExample {private File file;public ResourceExample(String filename) {this.file = new File(filename);}@Overrideprotected void finalize() throws Throwable {try {// 关闭文件等资源释放操作if (file != null && file.exists()) {file.delete(); // 示例中的资源释放操作}} finally {super.finalize();}}public static void main(String[] args) {ResourceExample example = new ResourceExample("example.txt");// 使用 example 对象example = null; // 让对象成为垃圾收集的候选对象// 在一定条件下,垃圾收集器可能会调用 example 的 finalize() 方法System.gc(); // 手动触发垃圾收集器}
}
总结:
尽管 finalize()
方法提供了在对象被垃圾收集器回收之前执行清理操作的机会,但由于其非确定性和性能问题,官方推荐避免使用它来管理重要资源或复杂对象的状态。更好的做法是利用现代 Java 提供的自动资源管理特性(如 try-with-resources)来确保资源的及时释放和程序的可靠性。
16、垃圾回收器跟垃圾回收算法了解吗?
关于JVM必备的一些知识-CSDN博客
17、JVM调优有哪些手段?有没有实际的经验例子可以讲解?
1、对于并发情况较高,新生代对象创建销毁较为频繁的应用,可以适当调整新生代老年代内存占比,适当增大新生代空间;
2、64位的jvm开启指针压缩;
3、对于CMS fullgc 偶尔影响服务接口性能的情况,可以考虑使用G1替代;
4、cms垃圾回收器,在在业务低峰期,job检测内存占比(Runtime),手动触发fullgc;避免在业务高峰期出现fullgc影响程序接口;
18、有没有jvm相关问题分析的实际经验例子讲解一下?
1、oom问题的排查定位及解决;
2、cpu资源销毁大,线程阻塞、死锁问题排查定位及修复;
资料参考
链接:https://blog.csdn.net/weixin_61440595/article/details/137505233
https://zhuanlan.zhihu.com/p/651655476