面试官:如何在一个方法中创建一个局部byte类型数组?
小白:(是不是太基础了,暗笑)byte[] arrays = new byte[1024]。
面试官:这个局部arrays变量指向的数组对象什么时候会被GC回收?
小白:没有变量引用这个数组对象,或者arrays在虚拟机栈中的局部变量表的局部变量空间(Slot)被重用,发生垃圾回收时将会被回收掉。
面试官:数组对象没有被变量引用会被GC回收,为什么?
小白:JVM通过一系列被称为"GC Roots"的对象引用作为起始点,通过引用关系遍历对象,能被遍历到的(可到达的)对象就被判定为存活对象,没有被遍历到的(不可到达的)对象就被判定为死亡对象,找出所有存活对象来把其它对象判定为可回收对象,这就是可达性分析算法。当这个局部arrays变量所在的方法被执行时,会在当前线程的Java虚拟机栈中创建一个栈帧,这个栈帧的局部变量表中会存储arrays变量所指向的数组指针,当设置arrays=null,也就是arrays不再引用这个数组对象,arrays和这个数组对象之间的引用关系就断掉了,发生垃圾回收时,以Java虚拟机栈的栈帧中里的引用类型的变量为"GC Roots”,遍历引用关系,发现这个数组对象和"GC Roots”引用链之间没有关联了,也就是不可达,即被标识为可回收对象,等待被回收。
面试官:除了你刚刚说的Java虚拟机栈的栈帧里的引用类型局部变量可以作为"GC Roots”,还有哪些也可以作为"GC Roots”?
小白:当前所有正在被调用的方法里的引用类型的参数、局部变量和临时值;Java类的引用类型静态变量;所有当前被启动类加载器或系统类加载器加载的Java类,例如如rt.jar中的java.util.*;Java类的运行时常量池里的引用类型常量;String常量池里的引用;本地方法栈中JNI的引用;虚拟机里的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
面试官:当一个对象被标识为可回收对象就一定会被回收掉吗?
小白:不一定。一个对象被标识为可回收对象后,还需要经过再次筛选,即查看这个对象有没有覆盖finalize()方法,或finalize()方法有没有被虚拟机执行过,如果没有覆盖finalize()方法或finalize()方法有没有被虚拟机执行过,如果没有覆盖finalize()方法或finalize()方法已经被虚拟机执行过,那么这个对象将会被回收掉,否则这个对象将会被放到一个叫F-Queue的队列中,这个队列中对象的finalize()方法将会被虚拟机创建的低优先级的Finalizer线程执行,在执行finalize()方法的过程中,只要这个对象和GC Roots引用链产生关联,即再次被GC Roots集合中的成员引用,那么它将被标记为不可回收对象,继续存活。
面试官:刚刚一直说到垃圾回收,那么Minor GC、Major GC和Full GC有什么区别?
小白:Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。Major GC清理Tenured区,用于回收老年代,出现Major GC通常会出现至少一次Minor GC。Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。
面试官:垃圾回收算法有哪些?
小白:标记-清除算法分为两部分,标记和清除。首先标记出所有需要被回收的对象,然后在标记完成后统一回收掉所有被标记的对象。这个算法简单,但是有两个缺点:一是标记和清除的效率不是很高;二是标记和清除后会产生很多的内存碎片,导致可用的内存空间不连续,当分配大对象的时候,没有足够的空间时不得不提前触发一次垃圾回收。
复制算法将可用的内存空间分为大小相等的两块,每次只是用其中的一块,当这一块被用完的时候,就将还存活的对象复制到另一块中,然后把原已使用过的那一块内存空间一次回收掉。这个算法常用于新生代的垃圾回收。复制算法解决了标记-清除算法的效率问题,以空间换时间,但是当存活对象非常多的时候,复制操作效率将会变低,而且每次只能使用一半的内存空间,利用率不高。
标记-整理算法分为三部分:一是标记出所有需要被回收的对象;二是把所有存活的对象都向一端移动;三是把所有存活对象边界以外的内存空间都回收掉。
标记-整理算法解决了复制算法多复制效率低、空间利用率低的问题,同时也解决了内存碎片的问题。
分代收集算法根据对象生存周期的不同将内存空间划分为不同的块,然后对不同的块使用不同的回收算法。一般把Java堆分为新生代和老年代,新生代中对象的存活周期短,只有少量存活的对象,所以可以使用复制算法,而老年代中对象存活时间长,而且对象比较多,所以可以采用标记-清除和标记-整理算法。
面试官:JVM运行时数据区中的方法区可以进行垃圾回收吗?
小白:方法区和堆一样,都是线程共享的内存区域,被用于存储已被虚拟机加载的类信息、即时编译后的代码、静态变量和常量等数据。根据Java虚拟机规范的规定,方法区无法满足内存分配需求时,也会抛出OutOfMemoryError异常,虽然规范规定虚拟机可以不实现垃圾收集,因为和堆的垃圾回收效率相比,方法区的回收效率实在太低,但是此部分内存区域也是可以被回收的。方法区的垃圾回收主要有两种,分别是对废弃常量的回收和对无用类的回收。当一个常量对象不再任何地方被引用的时候,则被标记为废弃常量,这个常量可以被回收。方法区中的类需要同时满足以下三个条件才能被标记为无用的类:Java堆中不存在该类的任何实例对象、加载该类的类加载器已经被回收、该类对应的java.lang.Class对象不在任何地方被引用,且无法在任何地方通过反射访问该类的方法,当满足上述三个条件的类才可以被回收,但是并不是一定会被回收,需要参数进行控制,例如HotSpot虚拟机提供了-Xnoclassgc参数进行控制是否回收。
面试官:如果让你配置JVM新生代和老年代的大小,你如何掌控?
小白:新生代配置原则:
追求响应时间优先
这种需求下,新生代尽可能设置大一些,并通过实际情况调整新生代大小,直至接近系统的最小响应时间。因为新生代比较大,发生垃圾回收的频率会比较低,响应时间快速。
追求吞吐量优先
吞吐量优先的应用,在新生代中的大部分对象都会被回收,所以,新生代尽可能设置大。此时不追求响应时间,垃圾回收可以并行进行。
避免设置过小
新生代设置过小,YGC会很频繁,同时,很可能导致对象直接进入老年代中,老年代空间不足发生FullGC。
老年代配置原则:
追求响应时间优先
这种情况下,可以使用CMS收集器,以获取最短回收停顿时间,但是其内存分配需要注意,如果设置小了会造成回收频繁并且碎片变多;如果设置大了,回收的时间会很长。所以,最优的方案是根据GClog分析垃圾回收信息,调整内存大小。
追求吞吐量优先
吞吐量优先通常需要分配一个大新生代、小老年代,将短期存活的对象在新生代回收掉。