JAVA面试题之JVM(GC)
1、Java字节码文件的格式?
一个字节码文件由6部份组成
(1)第一部分General Information,这一部份中说明了JDK使用的版本号Major version=51表示使用的是JDK 7,Minor version=0表示的是次版本号,这也限制了运行此字节码文件需要的最低JDK版本号,不信你可以试试下面这条语句,其实就是从字节码文件中获取到class的版本号
System.out.println(System.getProperty(“java.class.version”));
(2)第二部分constant pool,常量池中存放着类中定义的所有常量,大致包括类名,方法名,特征符以及字符串等
(3)第三部分Interfaces,表示类实现接口
(4)第四部分Fields ,类中定义的成员变量
(5)第五部分Methods ,表明类中定义的方法数(clinit对静态变量进行赋值由JVM调用, init为类的构造函数 bar用户自定义函数)
(6)第六部分Attributes ,表示是类格式属性
2、内部类的存储方式是什么?
3、垃圾回收器的分类及优缺点?
(1) Serial收集器是最基本、发展历史最悠久的收集器,在JDK1.3.1之前是新生代搜集的唯一选择
优点:简单高效、对于限定单个cpu的环境来说,serial收集器没有线程交互的开销,可专心做垃圾收集以获得最高的单线程收集效率。
缺点:单线程收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,直到它搜集结束。
适用场景:用户桌面应用(一般用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆或者一两百兆的新生代),停顿时间可以控制在100毫秒左右,这点停顿是可以接受的。
(2) ParNew收集器其实是Serial收集器的多线程版本。除了多线程收集,其他与Serial收集器相比并没有太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的收集器,其中的一个与性能无关的原因是,除了Serial收集器,目前只有它能和CMS收集器配合工作(CMS收集器是hotspot虚拟机中第一款真正意义上的并发收集器,简单来说就是你可以一边制造垃圾,收集器在不打断你的情况下进行收集)。
ParNew收集器适用于多核cpu环境,在单核情况下性能往往不如Serial收集器,甚至优于存在线程交互的开销,该收集器在通过超线程技术实现的两个cpu的环境中都不能百分之百的保证可以超越Serial收集器。不过一般在服务器环境下,多核心处理器非常常见,开发者可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
(3)Parallel Scavenge收集器
与其收集器关注点不同的是,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
适用场景:后台运算而不需要太多交互的任务。
(4)Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。
使用场景:
1)Client模式。
2)Server端,JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
3)Server端,CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
(5)Parallel Old收集器
这个收集器是JDK1.6中开始提供的,在此之前跟Parallel Scavenge相对应的是老生代收集器是PS MarkSweep收集器,这个收集器本身与Serial Old非常接近,所以在官方的许多质量中都是直接以Serial Old代替PS MarkSweep进行讲解。
由于Serial Old收集器是单线程,所以其在服务端应用性能会拖累Parallel Scavenge,也就是说是用了Parallel Scavenge也不一定获得吞吐量最大化的效果。
(6)CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以最短回收停顿时间为目标的收集器。目前很大一部分JAVA应用集中在互联网网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的用户体验,而CMS收集器就非常符合这类应用的需求。
从名字(Mark Sweep)上就可看出,CMS收集器是基于“标记-清除”算法实现的,它的运作过程分为4个步骤,包括:初始标识、并发标识、重新标识、并发清除。其中初始标识和重新标识这两个步骤仍然需要“Stop the world”。初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点:并发收集、低停顿。
缺点:1)、CPU资源敏感。
2)、无法处理浮动垃圾(Floating Garbage),即无法收集并发运行中产生的新的垃圾。
(7)G1收集器(了解)
G1(Garbage-First)是目前最前沿的成果之一。是面向服务端应用的垃圾收集器,hotspot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。在JDK7u4(即JDK Update4)及以后的版本中G1收集器可以进行商用了。
优点:并行与并发、分代收集、空间整合、可预测的停顿
4、GC垃圾收集算法?
(1)引用计数器算法
在JDK1.2之前,使用的是引用计数器算法,即当这个类被加载到内存以后,就会产生方法区,堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1,而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了!这种算法在JDK1.2之前的版本被广泛使用,但是随着业务的发展,很快出现了一个问题
当我们的代码出现下面的情形时,该算法将无法适应
a) ObjA.obj = ObjB
b) ObjB.obj - ObjA
这样的代码会产生如下引用情形 objA指向objB,而objB又指向objA,这样当其他所有的引用都消失了之后,objA和objB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。
(2)根搜索算法
根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
5、目前java中可作为GC Root的对象有?
(1) 虚拟机栈中引用的对象(本地变量表)
(2) 方法区中静态属性引用的对象
(3) 方法区中常量引用的对象
(4) 本地方法栈中引用的对象(Native对象)
6、java中的四种引用详解?
(1)强引用
只要引用存在,垃圾回收器永远不会回收
Object obj = new Object();
//可直接通过obj取得对应的对象 如obj.equels(new Object());
而这样 obj对象对后面new Object的一个强引用,只有当obj这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。
(2)软引用
非必须引用,内存溢出之前进行回收,可以通过以下代码实现
Object obj = new Object();
SoftReference sf = new SoftReference(obj);
obj = null;
sf.get();//有时候会返回null
这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;
软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
(3)弱引用
第二次垃圾回收时回收,可以通过如下代码实现
Object obj = new Object();
WeakReference wf = new WeakReference(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器
(4) 虚引用(幽灵/幻影引用)
垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现
Object obj = new Object();
PhantomReference pf = new PhantomReference(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
7、GC垃圾回收算法和使用?
(1)标记-清除算法
标记-清除算法采用从根集合进行扫描,对存活的对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收
标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片!
(2)复制算法
复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。
(3)标记-整理算法
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题
使用:
我们知道,JVM为了优化内存的回收,进行了分代回收的方式,对于新生代内存的回收(minor GC)主要采用复制算法,下图展示了minor GC的执行过程。
对于新生代和旧生代,JVM可使用很多种垃圾回收器进行垃圾回收,下图展示了不同生代不通垃圾回收器,其中两个回收器之间有连线表示这两个回收器可以同时使用。
而这些垃圾回收器又分为串行回收方式、并行回收方式合并发回收方式执行,分别运用于不同的场景。如下图所示
8、CMS垃圾回收器执行过程?
(1)初始标记
(2)并发标记
(3)重新标记
(4)并发清除
(5)并发重设状态等待下次CMS的触发(CMS-concurrent-reset)
在初始标记的时候,需要中断所有用户线程,在并发标记阶段,用户线程和标记线程并发执行,而在这个过程中,随着内存引用关系的变化,可能会发生原来标记的对象被释放,进而引发新的垃圾,因此可能会产生一系列的浮动垃圾,不能被回收。
9、Minor GC、Major GC和Full GC之间的区别?
(1)从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。这一定义既清晰又易于理解。但是,当发生Minor GC事件的时候,有一些有趣的地方需要注意到:
当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。
执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。
质疑常规的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。
所以 Minor GC 的情况就相当清楚了——每次 Minor GC 会清理年轻代的内存。