1. 垃圾回收是从哪里开始的?
栈是真正执行程序的地方,所以需要获取哪些对象正在被使用、哪些对象已经死亡,需要从栈开始。同时,一个栈与一个线程是对应的。因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。同时,除了栈外,还有系统运行的寄存器等,也是存储程序运行数据的。
这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其他对象的引用。这种引用逐步扩展,最终以 null 引用或者基本类型结束,这样就形成了一棵以 Java 栈中引用所对应的对象为根节点的一棵对象树。如果栈中有多个引用,则最终会形成多棵对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。
因此,垃圾回收的顺序为栈、寄存器 —> 堆 —> 堆中其他对象,最终以 null 引用或基本类型结束。
2. 被标记为垃圾的对象一定会被回收吗?
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
- 如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记。
- 随后会再进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。假如对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为确有必要执行 finalize() 方法,那么该对象将会被放置在一个名为 F-Queue 的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的 Finalizer 线程去执行它们的 finalize() 方法。这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。这样做的原因是,如果某个对象的 finalize() 方法执行缓慢,或者更极端地发生了死循环,将很可能导致 F-Queue 队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this 关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
但是,finalize()方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及时。
3. 什么是内存泄漏?
3.1 内存泄漏的基本概念
在 Java 中,内存泄漏就是存在一些不会再被使用但是却没有被回收的对象,这些对象有下面两个特点:
- 这些对象是可达的,即在有向图中,存在通路可以与其相连;
- 这些对象是无用的,即程序以后不会再使用这些对象。
如果对象满足这两个条件,这些对象就可以判定为 Java 中的内存泄漏,这些对象不会被 GC 所回收,然而它却占用内存。
3.2 内存泄漏的原因
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是 Java 中内存泄漏常见的发生场景。
3.3 避免内存泄漏的方法
- 尽量不要使用 static 成员变量,减少生命周期。
- 及时关闭资源。
- 不用的对象,可以手动设置为 null。
3.4 内存泄漏和内存溢出的区别
内存溢出是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory(OOM)。3.5 怎么进行 OOM 的排查
TODO4. 什么是浮动垃圾?
并发的垃圾收集器允许在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行的时候产生,这样就造成了浮动垃圾。这些垃圾需要在下次垃圾回收周期时才能回收掉,所以,并发收集器一般需要 20% 的预留空间用于这些浮动垃圾。5. 强引用、软引用、弱引用、虚引用的区别
- 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。
- 如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
- 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
- 虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。它必须和引用队列(ReferenceQueue)联合使用。
- 当 GC 释放对象内存的时候,会将引用加入到引用队列。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动,这相当于是一种通知机制。当关联的引用队列中有数据的时候,意味着指向的堆内存中的对象被回收。
在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
6. 如何判断一个类是无用的类?
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?类需要同时满足下面3个条件才能算是无用的类:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是”可以”,而并不是和对象一样不使用了就会必然被回收。
7. 谈谈你对内存分配的理解?大对象怎么分配?
- 对象优先在 Eden 区分配:大多数情况下,对象在新生代的 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
- 大对象直接进入老年代:大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
-XX:PretenureSizeThreshold
,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。 - 长期存活的对象将进入老年代:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄(默认 15 岁)则移动到老年代中。
-XX:MaxTenuringThreshold
用来定义年龄的阈值。 动态对象年龄判定:为了更好的适应不同程序的内存情况,虚拟机不是永远要求对象年龄必须达到了某个值才能进入老年代。如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄。
8. 什么是空间分配担保?
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
- 如果不成立的话,虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于或者是 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。
9. 在 Hotspot JVM 中,32位机器下,Integer 对象的大小是 int 的几倍?
我们都知道在 Java 语言规范已经规定了 int 的大小是 4 个字节,那么 Integer 对象的大小是多少呢?要知道一个对象的大小,那么必须需要知道对象在虚拟机中的结构是怎样的,根据上面的图,那么我们可以得出 Integer 的对象的结构如下:
Integer 只有一个 int 类型的成员变量 value,所以其对象实际数据部分的大小是 4 个字节,然后再在后面填充 4 个字节达到 8 字节的对齐,所以可以得出 Integer 对象的大小是 16 个字节。因此,Integer 对象的大小是原生的 int 类型的 4 倍。