Java面试系列02:Java基础之GC

1.GC 是什么? 为什么要有 GC?

垃圾收集(Garbage Collection)通常被称为“GC”,由虚拟机“自动化”完成垃圾回收工作。既然GC会自动回收,开发人员为什么要学习GC和内存分配呢?当需要排查各种内存溢出,内存泄露问题时,当垃圾成为系统达到更高并发量的瓶颈时,我们就需要对GC的自动回收实施必要的监控和调节。JVM中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生随线程而灭。栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理。GC垃圾回收主要集中在堆和方法区,在程序运行期间,这部分内存的分配和使用都是动态的。

2.如何判断一个对象是否存活?(或者 GC 对象的判定方法)

引用计数算法和可达性分析算法
引用计数算法:
给对象添加一个引用计数器,每当有一个地方引用它时计数器加1,引用释放时计数减1,当计数器为0时可以回收。
评价:引用计数算法实现简单,判断高效,在微软COM和Python语言等被广泛使用,但在主流的Java虚拟机中没有使用该方法,主要是因为无法解决对象相互循环引用的问题。
可达性分析算法:
基本思想是通过一系列称为“GC Root”的对象(如系统类加载器、栈中的对象、处于**状态的线程等)作为起点,基于对象引用关系,开始向下搜索,所走过的路径称为引用链,当一个对象到GC Root没有任何引用链相连,证明对象是不可用的。

3.简述 Java 垃圾回收机制(垃圾回收算法)

1)标记清除算法
包含“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。标记清除算法是最基础的收集算法,后续的收集算法都是基于该思路并对其缺点进行改进而得到的。
主要缺点:

  • a. 一个是效率问题,标记和清除过程的效率都不高;
  • b. 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2)复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当一块内存用完了,就将还存活着的对象复制到另外一块上,然后清理掉前一块。
优点:每次对半区内存回收时、内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:将内存缩小为一半,性价比低,持续复制长生存期的对象则导致效率低下。
在GC回收过程中,当Eden区满时,还存活的对象会被复制到其中一个Survivor区(s0);当回收时,会将Eden和使用的Survivor区还存活的对象,复制到另外一个Survivor区(s1),然后对Eden和用过的Survivor区进行清理。如果另外一个Survivor区(s1)没有足够的内存存储时,则会进入老年代。对象每经历一次复制,年龄加1。由于Eden中的对象属于像浮萍一样“瞬生瞬灭”的对象,所以并不需要1:1的比例来分配内存,而是采用了8(Eden):1(s0):1(s1)的比例来分配。而针对那些像“水熊虫”一样,历经多次清理依旧存活的对象,则会进入老年代,而老年的清理算法则采用下面要讲到的“标记整理算法”。
Java面试系列02:Java基础之GC
3)标记整理算法
标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。这种算法不既不用浪费50%的内存,也解决了复制算法在对象存活率较高时的效率低下问题。
4)分代收集算法
将Java的堆内存逻辑上分成两块,新生代和老年代,针对不同存活周期、不同大小的对象采取不同的垃圾回收策略。
新生代:在新生代中大多数对象都是瞬间对象,只有少量对象存活,复制较少对象即可完成清理,因此采用复制算法
老年代:老年代中的对象,存活率较高,又没有额外的担保内存,因此采用标记整理算法。

4.垃圾回收机制的优点和原理,并考虑两种回收机制(机制看上面的 2.如何判断一个对象是否存活?)

1)Java语言中一个显著的特点就是引入了垃圾回收机制,使C++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。
2)垃圾回收可以有效的防止内存泄露,有效的使用内存。
3)垃圾回收器通常是作为一个单独的低级别的线程(后台线程)运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。

5.垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?

基本原理:
可达性分析算法:…
垃圾回收器不可以马上回收内存。
垃圾收集器不可以被强制执行,但程序员可以通过调用System.gc方法来建议执行垃圾收集。记住,只是建议。一般不建议自己写System.gc,因为会加大垃圾收集工作量 程序员可以手动执行System.gc(),通知GC运行,但是并不保证GC一定会执行。

6.Java 中会存在内存泄漏吗,请简单描述。

所谓内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。
Java中有垃圾回收机制,它可以保证一对象不再被引用的时候,即对象变成了孤儿的时候,对象将自动被垃圾回收器从内存中清除掉。
由于Java 使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。
Java中的内存泄露的情况:
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。

7.深拷贝和浅拷贝

浅拷贝
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值。
如果属性是引用类型,拷贝的就是内存地址 ,两个变量指向同一个内存地址,因此浅拷贝会带来数据安全方面的隐患。
深拷贝
基础类型的拷贝和浅拷贝一样。
在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大,需要实现 Cloneable 并重写 clone() 方法。

8.System.gc() 和 Runtime.gc() 会做什么事情?

java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的简写,两者的行为没有任何不同。
唯一要能说有什么不同那就是在字节码层面上调用前者比调用后者短一点点,前者是1条字节码而后者是2条。
实际运行起来性能几乎一样。不过如果对字节码大小非常非常敏感的话建议用System.gc()。
从通常的代码习惯说也是System.gc()用得多些。

9.finalize() 方法什么时候被调用?析构函数 (finalization) 的目的是什么?

调用时机:
垃圾回收器(garbage collector)决定回收某对象时,就会运行该对象的finalize()方法;
析构函数 (finalization) 的目的
GC本来就是内存回收了,应用还需要在finalization做什么呢? 答案是大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。

10.如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?

不会,在下一个垃圾回调周期中,这个对象将是被可回收的。
也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。

11.什么是分布式垃圾回收(DGC)?它是如何工作的?

RMI 子系统实现基于引用计数的“分布式垃圾回收”(DGC),以便为远程服务器对象提供自动内存管理设施。
当客户机创建(序列化)远程引用时,会在服务器端 DGC 上调用 dirty()。
当客户机完成远程引用后,它会调用对应的 clean() 方法。
针对远程对象的引用由持有该引用的客户机租用一段时间。
租期从收到 dirty() 调用开始。
在此类租约到期之前,客户机必须通过对远程引用额外调用 dirty() 来更新租约。
如果客户机不在租约到期前进行续签,那么分布式垃圾收集器会假设客户机不再引用远程对象。

12.串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?

串行GC:
整个扫描和复制过程均采用单线程的方式,相对于吞吐量GC来说简单;适合于单CPU、客户端级别。
吞吐量GC:
采用多线程的方式来完成垃圾收集。
适合于吞吐量要求较高的场合,比较适合中等和大规模的应用程序。

13.在 Java 中,对象什么时候可以被垃圾回收?

当一个对象到GC Roots不可达时,在下一个垃圾回收周期中尝试回收该对象,如果该对象重写了finalize()方法,并在这个方法中成功自救(将自身赋予某个引用),那么这个对象不会被回收。
但如果这个对象没有重写finalize()方法或者已经执行过这个方法,也自救失败,该对象将会被回收。

14.JVM的永久代中会发生垃圾回收么?

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。
如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。
这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
请参考下Java8:从永久代到元数据区(译者注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)。