JVM面试真题总结(九)
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
描述CMS垃圾收集的工作过程
CMS(Concurrent Mark Sweep)
垃圾收集器的工作过程主要可以分为以下四个阶段:
初始标记(Initial Mark)
- 这个阶段的目标是标记所有的
GC Roots
能直接关联到的对象。- 这个阶段需要停止所有的用户线程,但是一般情况下,这个阶段会很快完成。
并发标记(Concurrent Mark):
- 在这个阶段,垃圾收集器会遍历对象图,并标记所有的存活对象,从初始标记的对象开始。
- 这个阶段是与用户线程并发执行的,不需要停止用户线程。
重新标记(Remark):
- 因为在并发标记阶段,用户线程还在运行,可能会修改了对象图,所以需要重新标记一次,以确保标记的准确性。
- 这个阶段需要停止所有的用户线程。
并发清除(Concurrent Sweep):
- 在这个阶段,垃圾收集器会清除所有未被标记的对象。
- 这个阶段是与用户线程并发执行的,不需要停止用户线程。
如何防止内存泄漏?
防止内存泄漏主要需要对代码进行精细的管理和控制,以下是一些常用的方法:
及时释放对象:
- 当对象不再使用时,及时将其引用设置为
null
,以便让垃圾回收器能够回收。使用弱引用(WeakReference):
- 在Java中,使用WeakReference可以在不影响垃圾回收器回收对象的同时,还能使用到这个对象。
- 当该对象只剩下弱引用时,垃圾回收器会回收它。
避免使用静态变量:
- 静态变量会在类加载时初始化,并在整个程序运行期间都存在,容易造成内存泄漏。
- 如果非要使用,应确保在不需要时将其设置为
null
。及时关闭流、数据库连接等资源:
- 这些资源都是有限的,使用完后必须及时关闭,否则会造成资源泄漏。
使用
finally
块:
- 在Java中,finally块始终会被执行,无论是否有异常发生。
- 因此,可以在
finally
块中释放资源。避免在对象中保存过多的数据:
- 如果一个对象保存了大量的数据,那么当这个对象被其他对象引用时,会消耗大量的内存。
使用内存分析工具:
- 内存分析工具(如
MAT,VisualVM
等)可以帮助你找出内存泄漏的来源,从而更好地修复问题。对集合类进行适当的管理:
- 对于集合类,如
List,Map
等,应该避免让它们过大,并且在不再使用它们时,应该清空或者设置为null
。注意线程的使用:
- 如果线程对象得不到及时的销毁,也会造成内存泄漏。
- 因此,对于线程对象,我们要特别小心,确保其生命周期得到良好的管理。
简述内存屏障及其类型
内存屏障(
Memory Barrier
),也被称为内存栅栏,是一种处理器指令
- 用于防止特定操作的重排序。
它可以确保某些内存操作的顺序性,以及它们对其他处理器可见的顺序。
内存屏障是处理器设计和多线程编程中的重要概念
- 它是实现诸如
volatile,synchronized
等高级同步构造的基础。内存屏障主要有以下四种类型:
LoadLoad屏障:
- 这种屏障确保了屏障之前的所有Load操作在屏障之后的Load操作之前完成。
- 即不允许Load操作的重排序。
StoreStore屏障:
- 这种屏障确保了屏障之前的所有Store操作在屏障之后的Store操作之前完成。
- 即不允许Store操作的重排序。
LoadStore屏障:
- 这种屏障确保了屏障之前的所有Load操作在屏障之后的Store操作之前完成。
StoreLoad屏障:
- 这种屏障确保了屏障之前的所有Store操作在屏障之后的Load操作之前完成。
- 这是最强的一种内存屏障,也是开销最大的一种。
在Java中,volatile关键字和synchronized关键字的实现就使用了内存屏障。
例如,对volatile变量的写操作会插入StoreStore和StoreLoad屏障
- 读操作会插入LoadLoad和LoadStore屏障。
而synchronized关键字在锁定和解锁时也会插入相应的内存屏障,以确保操作的顺序性和可见性。
哪些情况会导致栈内存溢出?
栈内存溢出一般发生在递归调用且递归深度过深的场景。
- 当一个线程请求的栈深度大于JVM所允许的深度,将抛出
StackOverflowError
异常。栈内存主要用于存储局部变量和执行动态链接,还用于方法调用和返回。
- 每次方法调用都会创建一个新的栈帧,这个栈帧会被添加到线程的栈顶。
- 如果这个方法调用其他方法,那么新的栈帧会继续被添加到栈顶。
- 当方法调用完成,相应的栈帧会被弹出栈。
每个线程都有一个私有的JVM栈,其大小可以固定也可以动态扩展。
- 如果固定大小的栈满了,或者动态扩展的栈无法继续扩展,那么JVM就会抛出
StackOverflowError
。例如,如果你写了一个递归函数,没有提供适当的递归出口,那么这个函数就会无限递归下去
- 每次递归都会向栈添加一个新的栈帧,最终导致栈内存溢出。
public void recursive() { recursive(); }
以上面的代码为例,这个方法会不断地调用自己,每次调用都会创建一个新的栈帧并压入栈中
- 但是没有任何方法可以弹出栈帧,因此最终会导致栈内存溢出。