JVM面试篇(下)
垃圾收集器
简述 Java 垃圾回收机制
在 java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM
中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或
者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收
的集合中,进行回收。
GC 是什么?为什么要 GC
-
GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,
忘记或者错误的内存
-
回收会导致程序或系统的不稳定甚至崩溃 Java 提供的 GC 功能可以自动监测对象是否超过
作用域从而达到自动
-
回收内存的目的, Java语言没有提供释放已分配内存的显示操作方法。
垃圾回收的优点和原理。2 种回收机制
Java 语言最显著的特点就是引入了垃圾回收机制,它使 Java 程序员在编写程序时不再考虑内存
管理的问题。
由于有这个垃圾回收机制, Java 中的对象不再有“作用域”的概念,只有引用的对象才有“作
用域”。
-
垃圾回收机制有效的防止了内存泄露,可以有效的使用可使用的内存。
-
垃圾回收器通常作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死
亡的或很长时间没有用过的对象进行清除和回收。
程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。垃圾回收有分代复制
垃圾回收、标记垃圾回收、增量垃圾回收。
垃圾回收器的基本原理是什么?
对于 GC 来说,当程序员创建对象时,GC 就开始监控这个对象的地址、大小以及使用情况。
主动通知虚拟机进行垃圾回收的办法?
通常,GC 采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象
是"可达的",哪些对象是"不可达的"。当 GC 确定一些对象为" 不可达"时,GC 就有责任回收这
些内存间。
垃圾回收器可以马上回收内存吗?
可以。程序员可以手动执行 System.gc(),通知 GC 运行,但是 Java 语言规范并不保证 GC 一
定会执行。
我们能保证 GC 执行吗?
不能,虽然你可以调用 System.gc() 或者 Runtime.gc(),但是没有办法保证GC 的执行。
Java 中引用类型有哪些?
-
强引用:发生 gc 的时候不会被回收。
-
软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
-
弱引用:有用但不是必须的对象,在下一次 GC 时会被回收。
-
虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用PhantomReference 实现虚
引用,虚引用的用途是在 gc 时返回一个通知。
强引用、软引用、弱引用、虚引用的区别?
思路: 先说一下四种引用的定义,可以结合代码讲一下,也可以扩展谈到ThreadLocalMap
里弱引用用处。
-
强引用
我们平时 new 了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足
的情况下,JVM 宁愿抛出 OutOfMemory 错误也不会回收这种对象
-
软引用
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不
足了,就会回收这些对象的内存。
SoftReference<String> softRef=new SoftReference<String>(str); // 软引用
-
用处:软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时
显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
-
如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面
时,需要重新构建
-
如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出.
如下代码
-
-
Browser prev = new Browser(); // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
if(sr.get()!=null){
rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取
}else{
prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了
sr = new SoftReference(prev); // 重新构建
}
-
弱引用
具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过
程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
String str=new String("abc"); WeakReference<String> abcWeakRef = newWeakReference<String>(str); str=null; 等价于 str = null; System.gc();
-
虚引用
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收
器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
怎么判断对象是否可以被回收?
垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是
「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。
一般有两种方法来判断:
-
引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计
数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
-
可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象
到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
在 Java 中,对象什么时候可以被垃圾回收
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full
GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么
正确的永久代大小对避免 Full GC 是非常重要的原因.
JVM 运行时堆内存如何分代?
Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、 From Survivor 区和 To Survivor 区)和
老年代。
参考图 1:
参考图 2:
-
从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来
指定。
-
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –
XX:NewRatio 来指定 ),
即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。
其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个Survivor 区域分别被命
名为 from 和 to,以示区分.
默认的,Eden: from : to = 8 :1 : 1 ( 可以通过参数–XX:SurvivorRatio 来设定 ),即: Eden
= 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是
有一块 Survivor 区域是空闲着的。
因此,新生代实际可用的内存空间为 9/10 ( 即 90% )的新生代空间。
新生代
是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发
MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、 ServivorTo 三个区。
Eden 区
Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内
存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
Servivor from 区
上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
Servivor to 区
保留了一次 MinorGC 过程中的幸存者。
MinorGC 的过程(复制->清空->互换)
MinorGC 采用复制算法。
-
eden、 servicorFrom 复制到 ServicorTo,年龄+1首先,把 Eden 和 ServivorFrom 区域
中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值
到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);
-
清空 eden、 servicorFrom然后,清空 Eden 和 ServicorFrom 中的对象;
-
ServicorTo 和 ServicorFrom 互换最后, ServicorTo 和 ServicorFrom 互换,原
ServicorTo 成为下一次 GC 时的 ServicorFrom 区。
老年代
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC (常常称之为 FULL GC)不会频繁执行。在进行 FULL
GC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才
触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进
行垃圾回收腾出空间。
FULL GC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记
的对象。ajorGC 的耗时比较长,因为要扫描再回收。FULLGC 会产生内存碎片,为了减少内存
损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,
就会抛出 OOM(Outof Memory)异常.
永久代
指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放
入永久区域, 它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这
也导致了永久代的区域会随着加载的Class 的增多而胀满,最终抛出 OOM 异常。
JVM 内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为 Eden和 Survivor。
思路: 先讲一下 JAVA 堆,新生代的划分,再谈谈它们之间的转化,相互之间一些参数的配置
(如: –XX:NewRatio,–XX:SurvivorRatio 等),再解释为什么要这样划分,最好加一点自己
的理解。
答:
这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
-
共享内存区划分
- 共享内存区 = 持久带 + 堆 持久带 = 方法区 + 其他 Java 堆 = 老年代 + 新生代 新生代 = Eden + S0 + S1
-
一些参数的配置
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。 默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定) Survivor 区中的对象被复制次数为 15(对应虚拟机参数-XX:+MaxTenuringThreshold)
-
为什么要分为 Eden 和 Survivor?为什么要设置两个 Survivor 区?
1、如果没有 Survivor,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代。 老年代很快被填满,触发 Major GC.老年代的内存空间远大于新生代,进行一次 Full GC 消耗的时间比 Minor GC 长得多,所以需要分为 Eden和 Survivor。 2、Survivor 的存在意义,就是减少被送到老年代的对象,进而减少 Full GC 的 发生,Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存 活的对象,才会被送到老年代。 3、设置两个 Survivor 区最大的好处就是解决了碎片化,刚刚新建的对象在 Eden 中,经历一次 Minor GC,Eden 中的存活对象就会被移动到第一块 survivor space S0,Eden 被清空;等 Eden 区再满了,就再触发一次 Minor GC,Eden 和 S0 中的存活对象又会被复制送入第二块 survivor space S1(这 个过程非常重要,因为这种复制算法保证了 S1 中来自 S0 和 Eden 两部分的 存活对象占用连续的内存空间,避免了碎片化的发生)
JVM 中一次完整的 GC 流程是怎样的,对象如何晋升到老年代
思路:先描述一下 Java 堆内存划分,再解释 Minor GC,Major GC,full GC,描述它们之间转
化流程。
答:
-
Java 堆 = 老年代 + 新生代
-
新生代 = Eden + S0 + S1
-
当 Eden 区的空间满了, Java 虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活
下来的对象,则会转移到 Survivor 区。
-
大对象(需要大量连续内存空间的 Java 对象,如那种很长的字符串)直接进入老年态;
-
如果对象在 Eden 出生,并经过第一次 Minor GC 后仍然存活,并且被Survivor 容纳的
话,年龄设为 1,每熬过一次 Minor GC,年龄+1,若年龄超过一定限制(15),则被晋
升到老年态。即长期存活的对象进入老年态。
-
老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行 Full GC,Full GC 清理
整个内存堆 – 包括年轻代和年老代。
-
Major GC 发生在老年代的 GC,清理老年区,经常会伴随至少一次 MinorGC,比 Minor
GC 慢 10 倍以上。
JVM 中的永久代中会发生垃圾回收吗
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full
GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正
确的永久代大小对避免 Full GC 是非常重要的原因。请参考下 Java8:从永久代到元数据区。
(译者注:Java8 中已经移除了永久代,新加了一个叫做元数据区的 native 内存区)
JAVA8 与元数据
在 Java8 中, 永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间
的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用
本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。 类的元数据放入 native
memory, 字符串池和类的静态变量放入 java 堆中, 这样可以加载多少类的元数据就不再由
MaxPermSize 控制, 而由系统的实际可用空间来控制。
如何判断对象可以被回收?
判断对象是否存活一般有两种方式:
-
引用计数:
每个对象有一个引用计数属性,新增一个引用时计数加 1,引用释放时计数减1,计数为 0
时可以回收。此方法简单,无法解决对象相互循环引用的问题。
-
可达性分析(Reachability Analysis):
从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots 没有
任何引用链相连时,则证明此对象是不可用的,不可达对象。
引用计数法
在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简
单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之
关联的引用, 即他们的引用计数都不为0, 则说明对象不太可能再被用到,那么这个对象就是可
回收对象。
可达性分析
为了解决引用计数法的循环引用问题, Java 使用了可达性分析的方法。通过一系列的“GC
roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是
不可达的。要注意的是,不可达对象不等价于可回收对象, 不可达对象变为可回收对象至少要经
过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
Minor GC 与 Full GC 分别在什么时候发生?
新生代内存不够用时候发生 MGC 也叫 YGC,JVM 内存不够的时候发生 FGC
垃圾收集算法有哪些类型?
-
GC 最基础的算法有三类: 标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回
收器一般都采用分代收集算法。
-
标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标
记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有
被标记的对象。
-
复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两
块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块
上面,然后再把已使用过的内存空间一次清理掉。
-
标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对
象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
-
分代收集算法,“分代收集”(Generational Collection)算法,把 Java 堆分为新生代和
老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
说一下 JVM 有哪些垃圾回收算法?
-
标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎
片。
-
复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到
另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一
半。
-
标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界
以外的内存。
-
分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基
本采用复制算法,老年代采用标记整理算法。
标记-清除算法
标记无用对象,然后进行清除回收。
标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶
段:
- 标记阶段:标记出可以回收的对象。
- 清除阶段:回收被标记的对象所占用的空间。
标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进
行改进的。
优点:实现简单,不需要对象进行移动。
缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。
标记-清除算法的执行的过程如下图所示:
复制算法
为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区
域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个
区域中,最后将当前使用的区域的可回收的对象进行回收。
优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
复制算法的执行过程如下图所示:
标记-整理算法
在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率
会较高,这样会有较多的复制操作,导致效率变低。标记- 清除算法可以应用在老年代中,但是
它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-
Compact)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到
内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未
用的内存都各自一边。
优点:解决了标记-清理算法存在的内存碎片问题。
缺点:仍需要进行局部对象移动,一定程度上降低了效率。
标记-整理算法的执行过程如下图所示:
分代收集算法
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内
存划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代
(YoungGeneration)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点
是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周
期将内存划分为几块。一般包括年轻代、老年代 和 永久代,
如图所示:
当前主流 VM 垃圾收集都采用”分代收集” (Generational Collection)算法, 这种算法会根据对
象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代、老年代、永久代, 这样就可以根据
各年代特点分别采用最适当的 GC 算法。
新生代与复制算法
每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对
象的复制成本就可以完成收集。
目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要回收
大部分对象,即要复制的操作比较少,但通常并不是按照 1: 1 来划分新生代。一般将新生代划
分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用
Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另
一块 Survivor 空间中。
老年代与标记复制算法
因为老年代对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标
记—整理” 算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存。因而采用
Mark-Compact 算法。
-
JAVA 虚拟机提到过的处于方法区的永生代(Permanet Generation), 它用来存储 class
类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类
-
对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的From Space(Survivor
目前存放对象的那一块),少数情况会直接分配到老生代。
-
当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,
EdenSpace 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和
FromSpace 进行清理。
-
如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。
-
在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。
-
当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。 默认情况下年龄到达 15 的对象会
被移到老生代中。
GC 垃圾收集器
Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法;
年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和年老代分别提供了多种
不同的垃圾收集器, JDK1.6 中 SunHotSpot 虚拟机的垃圾收集器如下:
说一下 JVM 有哪些垃圾回收器?
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示
了 7 种作用于不同分代的收集器,其中用于回收新生代的收集器包括 Serial、PraNew、Parallel
Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个 Java
堆的 G1 收集器。不同收集器之间的连线表示它们可以搭配使用。
-
Serial 收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
-
ParNew 收集器 (复制算法): 新生代收并行集器,实际上是 Serial 收集器的多线程版本,在
多核 CPU 环境下有着比 Serial 更好的表现;
-
Parallel Scavenge 收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用
CPU。吞吐量 = 用户线程时间/(用户线程时间+GC 线程时间),高吞吐量可以高效率的利用
CPU 时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
-
Serial Old 收集器 (标记-整理算法): 老年代单线程收集器,Serial 收集器的老年代版本;
-
Parallel Old 收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,
ParallelScavenge 收集器的老年代版本;
-
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最
短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC 回收停顿时间。
-
G1(Garbage First)收集器 (标记-整理算法): Java 堆并行收集器,G1 收集器是JDK1.7 提
供的一个新收集器,G1 收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。
此外,G1 收集器不同于之前的收集器的一个重要特点是:G1 回收的范围是整个 Java 堆(包
括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
Serial 与 Parallel GC 之间的不同之处?
Serial 与 Parallel 在 GC 执行的时候都会引起 stop-the-world。它们之间主要不同 serial 收集
器是默认的复制收集器,执行 GC 的时候只有一个线程,而 parallel 收集器使用多个 GC 线程来
执行。
热门相关:攻略初汉 超级融合 一世倾心:误惹腹黑师弟 妖魔哪里走 黄金渔场