垃圾回收常见面试题:
如何判断对象是否死亡。
简单的介绍一下强引用、软引用、弱引用、虚引用。虚引用与软引用和弱引用的区别、使用软引用能带来的好处
如何判断一个常量是废弃常量
如何判断一个类是无用的类
垃圾收集有哪些算法,各自的特点?
HotSpot 为什么要分为新生代和老年代?
常见的垃圾回收器有哪些?
介绍一下 CMS、G1 收集器。
Minor GC 和 Full GC 有什么不同呢?
1.如何判断对象可以回收
1.1 引用计数法
什么是引用计数法
引用计数法是指在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;计数器为零的对象就是可以被回收的。
缺点:这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。例如,当二个对象相互引用时,并且这二个对象也不可能再被访问,那么这二个对象将永远存在于内存当中不会被回收。
1.2 可达性分析算法
1.什么是可达性分析算法
可达性分析算法就是通过一系列称为 “GC Roots” 的对象作为根节点,然后通过对象之间的引用关系查看对象是否有引用链能够到达根节点,如果有就认为是可达的,不可以被回收;如果没有就认为是不可达的,可以被回收。例如:
下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。
2.可以作为 GC Roots 的对象
-
在虚拟机栈中引用的对象。也就是栈帧中的局部变量表里引用的对象,例如当前正在运行的方法所用到的参数、局部变量、临时变量等。
-
在本地方法栈中JNI(即通常所说的 Native 方法)引用的对象
-
静态属性引用的对象。例如Java类的引用类型静态变量
-
被同步锁(synchronized关键字)持有的对象
-
Java虚拟机内部的引用,如基本数据类型对应的Class对象、一些常驻的异常对象(比如NullPointException、OutOfMemoryError等)、系统类加载器等
-
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
除了上述这些固定的 GC Roots 集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其它对象临时性的加入,共同构成完整的GC Roots集合。例如后面会提到的分代收集和局部回收,如果只针对Java堆中某一块区域发起垃圾收集(例如只针对新生代的垃圾收集),而这个区域的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象一并加入GC Roots集合中。
3.四种引用
下面四种引用的引用强度从上到下依次减弱
强引用:
-
被强引用所引用的对象无论什么时候,都不会被垃圾回收器所回收。我们平时创建的对象一般都是强引用。
创建强引用示例
new User()
软引用:
-
被软引用所引用的对象是一些还有用但非必须的对象。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可以和一个引用队列联合使用。
创建软引用示例
new SoftReference(new User())
弱引用:
-
被弱引用引用的对象,在垃圾回收时,无论内存是否充足,都会被回收。弱引用可以和一个引用队列联合使用。JDK 提供了 WeakReference 类来实现弱引用。
创建弱引用示例
new WeakReference(new User())
虚引用:
-
被虚引用引用的对象就和没有任何引用一样,在任何时候都可能被垃圾回收,必须配合引用队列使用。JDK 提供了 PhantomReference 类来实现虚引用。
创建虚引用示例
new PhantomReference(new User())
Note
和引用队列联合使用的意思就是如果引用的对象被垃圾回收,Java 虚拟机就会把这个对象加入到与之关联的引用队列中。
引用队列是JVM为我们提供的一种保障,如果后期想对被回收的对象进行处理,就可以从引用队列中取出来进行使用。
一个对象的引用通常是存储在一个变量中的,这个变量可以是类的成员属性、成员方法的参数或返回值,也可以是普通方法中的局部变量等
2.垃圾回收算法
2.1 分代收集
1.分代收集理论
当前商业虚拟机的垃圾收集器,大多数都遵循了分代收集的理论进行设计,分代收集实际建立在二个分代假说之上:
-
弱分代假说:绝大多数对象是朝生夕灭的
-
强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
这二个分代假说奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域。
把分代收集理论放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为新生代和老年代二个区域。顾名思义,新生代中,每次垃圾回收都有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。针对新生代或老年代或整个堆的收集则有如下不同的名词:
部分收集
-
新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
-
老年代收集(Major GC/Old GC):指目标只是老年代的收集。目前只有CMS收集器会有单独收集老年代的行为。
-
混合收集:指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
注意
1.对象之间可能会存在跨代引用。例如,新生代中的对象有可能被老年代中的对象所引用,具体解决方法见<<深入理解Java虚拟机>>
2.在HotSpot中,新生代内存不足,会触发Minor GC,使用新生代垃圾回收器完成垃圾回收;老年代内存不足,会触发Full GC,使用新生代垃圾回收器完成新生代的垃圾回收,使用老年代垃圾回收器完成老年代的垃圾回收
2.HotSpot 中的分代垃圾回收
HotSpot虚拟机中,堆内存分为新生代、老年代二大区域,大多数对象创建时都会被分配在新生代。
-
新生代空间不足时,会触发 minor gc,垃圾对象会被清理,存活的对象年龄加 1,并且 Eden 区和其中一个 Survivor 区的存活对象会被复制到另外一个空 Survivor 区。
-
当老年代空间不足,一般会触发 full gc。
3.新生代的对象什么时候进入老年代
-
对象年龄超过阈值的进入老年代
-
大对象直接进入老年代
-
老年代担保:在 Minor GC 之后,新生代存活的对象占用的空间大于 Survivor 区的空间大小时,那么多出来的对象会提前进入老年代,剩下的对象会进入 Survivor 区。
新生代使用的是复制算法,为了内存利用率,只使用其中一个 Survivor 空间来做轮换备份,因此如果大量对象在 Minor GC 后仍然存活,导致 Survivor 空间不够用,就会通过分配担保机制,将多出来的对象提前转到老年代,此时如果老年代的可用内存小于该对象的大小,就会触发 Full GC。
2.2 标记清除算法
什么是标记清除
首先标记出存活的对象,然后统一回收所有未被标记的对象。
优点:
-
回收速度比较快
缺点:
-
标记清除后会产生内存碎片
2.3 标记整理算法
什么是标记整理算法
首先标记出所有存活的对象,然后让所有存活的对象向内存空间一端移动,最后把边界以外的内存全部清理掉。
优点:不会产生内存碎片
缺点:整理这个阶段的效率比较低
2.4 标记复制算法
什么是标记复制算法
它将可用内存划分为大小相等的二块,只使用其中的一块。当这一块的内存用完了,就将还活着的对象复制到另外一块上面,然后再把使用过的这块内存一次性清理掉。
优点
不会有内存碎片
缺点
需要占用双倍内存空间
现在的商用Java虚拟机优先采用了这种收集算法去回收新生代。但是,IBM曾有一项研究对新生代的朝生夕死的特点做了一个更量化的诠释:新生代中的对象有98%熬不过第一轮收集。因此,并不需要1:1的比例划分新生代的内存空间
2.5 JVM的相关参数
3.垃圾回收器
3.1 垃圾回收器概述
什么是垃圾回收器
垃圾收集算法是内存回收的方法论,而垃圾回收器是内存回收的实践者。《Java虚拟机规范》中对垃圾收集器如何实现并没有做出任何规定,因此不同厂商、不同版本的虚拟机所包含的垃圾回收器都可能会有很大差别,不同的虚拟机一般也会提供各种参数供用户根据自己的应用特点和要求组合出各个内存分代所使用的收集器。
垃圾回收器的分类
1. 串行
- 单线程
- 堆内存较小,适合个人电脑
2. 吞吐量优先
- 多线程
- 堆内存较大,多核 cpu
- 让单位时间内,STW 的时间最短,垃圾回收时间占比最低,这样就称吞吐量高
3. 响应时间优先
- 多线程
- 堆内存较大,多核 cpu 尽可能让单次 STW 的时间最短
不同垃圾回收器的组合
上图展示了七种作用于不同分代的垃圾回收器,如果二个回收器之间存在连线,这说明它们可以搭配使用,但这个关系不是一成不变的,在JDK的一些版本中,某些组合已经被废弃了。
直到目前还没有最好的垃圾回收器,跟更加不存在万能的垃圾回收器,我们应选择对具体的应用最合适的垃圾回收器。
3.2 Serial 垃圾回收器
Serial 垃圾回收器是一个单线程垃圾回收器,它在进行垃圾回收的时候必须暂停其它所有线程( "Stop The World" ),直到它回收结束。采用的标记复制算法。
优点:简单而高效、没有线程交互的开销。
缺点:Stop The World 时会给用户带来非常不好的体验
上面这个图中新生代采用的Serial垃圾回收器,老年代采用的是Serial Old垃圾回收器,分别使用了不同的垃圾回收算法。后面的图同理。
3.3 ParNew 垃圾回收器
ParNew 垃圾回收器其实就是 Serial 垃圾回收器的多线程版本,除了使用多线程进行垃圾回收外,其余行为和 Serial 垃圾回收器完全一样。新生代采用标记-复制算法。
3.4 Parallel Scavenge 垃圾回收器
Parallel Scavenge 垃圾回收器的关注点是吞吐量(高效率的利用 CPU)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总执行时间的比值。采用的是标记-复制算法。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,是 JDK1.8 默认的收集器。
Note:如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
3.5 Serial Old 垃圾回收器
Serial 垃圾回收器的老年代版本,它同样是一个单线程收集器。主要有两大用途:一种用途是在 JDK1.5 及之前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
3.6 Parallel Old 垃圾回收器
Parallel Scavenge 垃圾回收器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
3.7 CMS 垃圾回收器
CMS(Concurrent Mark Sweep)垃圾回收器的目标是用最短的停顿时间进行垃圾回收,实现了让垃圾回收线程与用户线程同时工作。非常适合在注重用户体验的应用上使用。采用的是标记清除算法。在老年代上工作。
CMS 收集器是一种实现了“标记-清除”算法的收集器,它的工作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
-
初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
-
并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
-
重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
-
并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
优点:并发收集、低停顿。
缺点:
-
对 CPU 资源敏感;
-
无法处理浮动垃圾;
-
它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
3.8 G1 垃圾回收器
G1 (Garbage-First)垃圾回收器主要面向有多颗处理器和大容量内存的服务器。在进行垃圾回收时,既有很短的停顿时间,又具备很高的吞吐量。同时工作在新生代和老年代,被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:
-
并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
-
分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
-
空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
-
可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
G1 收集器的运作大致分为以下几个步骤:
-
初始标记
-
并发标记
-
最终标记
-
筛选回收
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
从 JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器。
4.垃圾回收器常见面试题
1.如何判断一个常量是废弃常量?
如果一个常量没有被任何变量引用的话,那这个常量就是废弃常量,在垃圾回收时可以被回收掉。例如:在字符串常量池中存在字符串 "abc",如果当前没有任何 String 类型的变量引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了。
2.如何判断一个类是无用的类?
方法区主要回收的是无用的类,那么如何判断一个类是无用的类呢,类需要同时满足 3 个条件才能算是 “无用的类”,才可以被被回收掉:
-
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
-
该类对应的类对象没有在任何地方被引用。
-
加载该类的 ClassLoader 已经被回收。
Note:虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收文章来源:https://www.toymoban.com/news/detail-623981.html
3.HotSpot 为什么要分为新生代和老年代?
因为 Java 中对象的存活时间有长有短,通过将 Java 堆划分为新生代和老年代,在新生代存储存活时间短的对象,在老年代中存储存活时间长的对象,这样就可以根据各个年代的特点选择合适的垃圾回收器。在新生代中,每次垃圾回收都会有大量对象死去,所以可以选择”标记-复制“算法。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以必须选择“标记-清除”或“标记-整理”算法进行垃圾回收。文章来源地址https://www.toymoban.com/news/detail-623981.html
到了这里,关于JVM之垃圾回收器的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!