面试题答案
一键面试优化内存分配策略减少垃圾回收开销
- 对象池技术:
- 原理:预先创建一定数量的对象放入对象池中,当需要使用对象时,从对象池中获取,使用完毕后再放回对象池,而不是每次都创建新对象。这样可以减少对象创建和销毁的频率,进而减少垃圾回收的压力。
- 示例:在Java中,可以通过自定义一个类来管理对象池,例如使用
LinkedList
来存储对象,提供获取和归还对象的方法。
- 使用线程本地存储(Thread - Local):
- 原理:每个线程都有自己独立的存储空间,在线程内部使用
ThreadLocal
变量时,每个线程都会创建自己的实例,避免了多线程竞争,同时减少了对象在堆中频繁创建和销毁的情况。因为ThreadLocal
变量的生命周期与线程一致,在线程结束时才会被回收。 - 示例:定义
ThreadLocal
变量private static ThreadLocal<MyObject> threadLocal = ThreadLocal.withInitial(MyObject::new);
,然后在线程内部通过threadLocal.get()
获取对象。
- 原理:每个线程都有自己独立的存储空间,在线程内部使用
- 优化对象设计:
- 原理:尽量复用对象内部的可变状态,减少不可变对象的创建。例如,对于一些经常需要修改内容的对象,避免每次修改都创建新对象,而是提供修改其内部状态的方法。
- 示例:如果有一个表示日期的对象,对于日期的调整操作,不要每次都创建新的日期对象,而是在原对象上进行修改。
不同垃圾回收器在这种场景下的适用情况及原理
- CMS(Concurrent Mark - Sweep)垃圾回收器:
- 适用情况:适用于追求低停顿时间的应用场景,在高并发且频繁创建和销毁临时对象的场景下,如果应用对响应时间非常敏感,CMS可能是一个较好的选择。
- 原理:
- 初始标记(Initial Mark):暂停所有的应用线程,标记出所有GC Roots直接关联的对象。这个阶段速度很快,但是会造成短暂的应用停顿。
- 并发标记(Concurrent Mark):与应用线程并发执行,从GC Roots的直接关联对象开始遍历整个对象图,标记出所有可达对象。此阶段应用线程可以继续运行。
- 重新标记(Remark):再次暂停应用线程,修正并发标记期间因应用程序继续运行而导致标记产生变动的那一部分对象的标记记录。这个阶段停顿时间比初始标记稍长,但比完全暂停应用线程进行整个标记过程要短很多。
- 并发清除(Concurrent Sweep):与应用线程并发执行,清理所有未被标记的对象(即垃圾对象)。
- G1(Garbage - First)垃圾回收器:
- 适用情况:适用于堆内存较大,且对停顿时间有严格要求的应用场景。在频繁创建和销毁大量临时对象的高并发Java应用中,G1能较好地处理这种情况,特别是当堆内存较大时,它能更有效地利用内存和控制停顿时间。
- 原理:
- 将堆内存划分为多个大小相等的Region:G1不再将堆内存简单地划分为新生代和老年代,而是划分为大量大小相等的Region。每个Region在不同的阶段可以扮演不同的角色,比如Eden区、Survivor区或老年代。
- 标记 - 整理算法:
- 初始标记(Initial Mark):暂停应用线程,标记出所有GC Roots直接关联的对象,与CMS的初始标记类似。
- 并发标记(Concurrent Mark):与应用线程并发执行,标记出所有可达对象。在此过程中,G1会计算每个Region中存活对象的大小和回收收益等信息。
- 最终标记(Final Mark):暂停应用线程,处理并发标记阶段结束后剩余的少量对象的标记。
- 筛选回收(Live Data Counting and Evacuation):根据每个Region的回收收益,优先回收回收收益高的Region,采用复制算法,将存活对象复制到其他Region,同时实现了内存的整理,减少了内存碎片。