1、说说JVM的垃圾回收算法:
java中有四种垃圾回收算法,分别是:
复制算法、标记清除法、标记整理法、分代收集算法;
①、复制算法(Copying) :
将堆内存空间分为两块,每次只使用一块,当这一块使用完了,进行垃圾回收,先用可达性分析算法标记可达对象,然后将可达对象/存活对象复制到没有被使用的那个内存块中,最后再清除当前内存块中的所有对象。后续再按同样的流程来回复制和清除。
优点:
- 适合垃圾对象多,可达对象少的情况,这样复制耗时短。非常适合新生代的垃圾回收,
- 无内存碎片:因为会将存活对象进行移动,所以内存整齐。
缺点:
- 内存利用率低,浪费内存:始终有一半以上的空闲内存。
- 需要调整引用地址:可达对象移动后,内存地址发生了变化,需要调整所有引用,指向移动后的地址。
- 垃圾少时效率相对差,但还是比其他算法强:如果可达对象比较多,垃圾对象比较少,那么复制算法的效率就会比较低。只为了一点垃圾而移动所有对象未免有些小题大做。所以垃圾对象多的情况下,复制算法比较适合。
适用场景:
适合垃圾对象多,可达对象少的情况,这样复制耗时短。非常适合新生代的垃圾回收,因为新生代中的对象大多是“朝生夕灭死”的对象,垃圾对象占比高,所以采用复制算法回收性价比非常高,一次通常可以回收70-90%的内存空间。
②、标记清除法:
- 第一步: 当堆中有效内存空间被耗尽时,会STW(stop the world,暂停其他所有工作线程)
- 第二步:利用可达性分析算法,从GC Roots直接关联的对象开始向下遍历堆中的对象图,标记所有可达的对象,并在对象头中进行标记,遍历所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是非可达对象、可以被回收的垃圾对象。
- 第三步:回收非可达对象/垃圾对象。注意回收垃圾对象并不是真的置空,垃圾还在原来的位置,实际上是把垃圾对象的地址维护在空闲列表,当对象实例化的申请内存阶段会通过空闲列表找到合适大小的空闲内存分配给新对象。
优点:简单
缺点:
- 效率不高:需要可达性遍历和线性遍历,效率差。
- STW导致用户体验差:GC时需要暂停其他所有工作线程,用户体验差。
- 有内存碎片,要维护空闲列表: 垃圾对象回收后没有整理,导致堆中产生大量不连续的内存碎片,可能会导致之后程序运行的时候需分配大对象而找不到连续内存而不得不触发一次GC;
适用场景:
适合小型应用程序,内存空间不大的情况。应用程序越大越不适用这种回收算法。
③、标记整理法:
首先可达性分析法标记可达对象,然后将可达、存活对象按顺序整理到内存的一端,最后清理边界外的垃圾对象。相当于内存碎片优化版的标记清除算法,不用维护空闲列表。
优点:
- 无内存碎片:内存整齐、内存利用率最高。
- 效率最低:效率比其他两种算法都低
- 需要调整引用地址:可达对象移动后,内存地址发生了变化,需要调整所有引用,指向移动后的地址。
- STW导致用户体验差:移动时需要暂停其他所有工作线程,用户体验差。
④、分代收集算法:
- 根据对象的存活周期不同,java虚拟机一般将堆内存划分成新生代和老生代,新生代默认占总空间的1/3,老年代默认占2/3, 这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
- 在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要复制少量存活对象就可以完成垃圾回收,成本低;
- 老年代中因为对象的存活率极高,不适合采用复制算法,所以采用标记清理或者标记整理算法进行回收;
2、如何判断对象是否可以被回收/如何判断一个对象是否存活?
判断一个对象是否存活,分为两种算法1:引用计数法;2:可达性分析算法;
①、引用计数法:
给每一个对象设置一个引用计数器,当有一个地方引用该对象的时候,引用计数器就+1,引用失效/被释放时,引用计数器就-1; 当引用计数器为0的时候,就说明这个对象没有被引用,也就是垃圾对象,等待回收;
缺点:增加了空间的消耗,并且无法解决循环引用的问题:当A引用B,B也引用A的时候,此时AB对象的引用都不为0,此时也就无法垃圾回收,所以一般主流虚拟机都不采用这个方法;
②、GC的可达性分析算法:
从GC Roots直接关联的对象开始向下遍历堆中的对象图,标记所有可达的对象,并在对象头中进行标记,遍历所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是非可达对象、可以被回收的垃圾对象。 在java中可以作为GC Roots的对象有以下几种:
堆中的对象图指的是在内存中表示对象之间关系的数据结构。在Java虚拟机中,堆是用来存储所有被创建的对象的地方。对象之间的引用关系构成了一个对象图,描述了对象之间的相互关系。
对象图通常是一个有向图,其中对象是图中的节点,而对象之间的引用关系则是图中的边。当一个对象引用另一个对象时,就会产生一条边连接这两个对象。这种引用关系可以是单向的,也可以是双向的,取决于对象之间的实际关系。
垃圾回收器在执行标记阶段时,会遍历堆中的对象图,从根对象开始,递归地访问所有可达的对象,并标记它们为活动对象。通过遍历对象图,垃圾回收器可以确定哪些对象是存活的,哪些对象可以被回收释放。
因此,堆中的对象图是垃圾回收器在执行标记阶段时所依赖的数据结构,用于表示对象之间的引用关系,以及帮助确定哪些对象是活动的,哪些对象可以被回收。
3、可达性分析哪些对象可以作为GC Roots?
- 虚拟机栈中引用的对象
- 方法区类静态属性引用的对象
- 方法区常量池引用的对象
- 本地方法栈JNI引用的对象
备注:Java方法栈、本地方法栈中的参数引用、局部变量引用、临时变量引用等。临时变量是方法里的中间操作结果。
非可达对象被回收需要两次标记:
1、第一次标记后筛选非可达对象:第一次被标记后,会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,也就是是否有机会自救。假如对象没有覆盖或者已被JVM调用过finalize()方法,也就是说不想自救或已自救过,那么此对象需要被回收;假如对象覆盖并没被JVM调用过finalize()方法,该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。
2、第二次标记F-Queue里的未自救对象:稍后,收集器将对F-Queue中的对象进行第二次小规模的标记。如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this)赋值给某个引用类型的类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的F-Queue。如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
finalize()方法:
finalize()方法是对象逃脱死亡命运的最后一次机会,需要注意的是,任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。
另外,finalize()方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。
加分回答-宣告对象死亡要经历两次标记
真正宣告一个对象死亡,至少要经历两次标记过程:
1. 第一次标记
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。反之,该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。
2. 第二次标记
稍后,收集器将对F-Queue中的对象进行第二次小规模的标记。如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合。如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
finalize()方法是对象逃脱死亡命运的最后一次机会,需要注意的是,任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。另外,finalize()方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。
JVM中一次完整的GC流程是怎样的:
Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ),,新生代默认占总空间的1/3,老年代默认占2/3。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。
老年代的垃圾回收(又称Major GC)通常使用“标记-清理"或"标记-整理"算法。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
Minor GC ,Major GC,Full GC是什么?以及它的触发条件
①、Minor GC
Minor GC触发条件: 当Eden区满时,触发Minor GC。
- 当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存。)
- 因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
②、Major GC
- 指发生在老年代的GC,对象从老年代消失时,我们说 “Major Gc” 或 “Full GC” 发生了
- 出现了MajorGc,经常会伴随至少一次的Minor GC(但非绝对的,在Paralle1 Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)
也就是在老年代空间不足时,会先尝试触发MinorGc。
如果之后空间还不足,则触发Major GC
- Major GC的速度一般会比MinorGc慢1e倍以上,STW的时间更长,如果Major GC后,内存还不足,就报OOM了
③、Full GC
对年轻代和老年代都进行垃圾回收,Full GC 是开发或调优中尽量要避免的。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的存活对象平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把这些存活对象转存到老年代,且老年代的可用内存小于这些对象需要的内存大小
通过Minor GC后进入老年代的存活对象平均大小大于老年代的可用内存
由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把这些存活对象转存到老年代,且老年代的可用内存小于这些对象需要的内存大小
老年代空间不足
调用System.gc时,系统建议执行Full GC,但是不必然执行