描述一下GC的原理和回收策略?
3、描述一下GC的原理和回收策略?
- 回收区域:只针对堆、方法区;线程私有区域数据会随线程结束销毁,不用回收
- 回收类型:
- 1.堆中的对象:分代收集 GC 方法会吧堆划分为新生代、老年代。新生代:新建小对象会进入新生代;通过复制算法回收对象;老年代:新建大对象及老对象会进入老年代;通过标记-清除算法回收对象。
- 2.方法区中的类信息、常量池
- 判断一个对象是否可被回收:
- 1.引用计数法:有循环引用的缺点
- 2.可达性分析法:从 GC ROOT 开始搜索,不可达的对象都是可以被回收的。其中 GC ROOT 包括虚拟机栈/本地方法栈中引用的对象、方法区中常量/静态变量引用的对象。
提到垃圾回收,我们可以先思考一下,如果我们去做垃圾回收需要解决哪些问题?
一般说来,我们要解决三个问题:
1、回收哪些内存?
2、什么时候回收?
3、如何回收?
这些问题分别对应着引用管理和回收策略等方案。
提到引用,我们都知道Java中有四种引用类型:
- 强引用:代码中普遍存在的,只要强引用还存在,垃圾收集器就不会回收掉被引用的对象。
- 软引用:SoftReference,用来描述还有用但是非必须的对象,当内存不足的时候会回收这类对象。
- 弱引用:WeakReference,用来描述非必须对象,弱引用的对象只能生存到下一次GC发生时,当GC发生时,无论内存是否足够,都会回收该对象。
- 虚引用:PhantomReference,一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用取得一个对象的引用,它存在的唯一目的是在这个对象被回收时可以收到一个系统通知。
不同的引用类型,在做GC时会区别对待,我们平时生成的Java对象,默认都是强引用,也就是说只要强引用还在,GC就不会回收,那么如何判断强引用是否存在呢?
一个简单的思路就是:引用计数法,有对这个对象的引用就+1,不再引用就-1,但是这种方式看起来简单美好,但它却不能解决循环引用计数的问题。
因此可达性分析算法登上历史舞台,用它来判断对象的引用是否存在。
可达性分析算法通过一系列称为GCRoots的对象作为起始点,从这些节点从上向下搜索,所走过的路径称为引用链,当一个对象没有任何引用链与GCRoots连接时就说明此对象不可用,也就是对象不可达。
GC Roots对象通常包括:
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法中类的静态属性引用的对象
- 方法区中常量引用的对象
- Native方法引用的对象
可达性分析算法整个流程如下所示:
第一次标记:对象在经过可达性分析后发现没有与GC Roots有引用链,则进行第一次标记并进行一次筛选,筛选条件是:该对象是否有必要执行finalize()方法。没有覆盖finalize()方法或者finalize()方法已经被执行过都会被认为没有必要执行。 如果有必要执行:则该对象会被放在一个F-Queue队列,并稍后在由虚拟机建立的低优先级Finalizer线程中触发该对象的finalize()方法,但不保证一定等待它执行结束,因为如果这个对象的finalize()方法发生了死循环或者执行时间较长的情况,会阻塞F-Queue队列里的其他对象,影响GC。
第二次标记:GC对F-Queue队列里的对象进行第二次标记,如果在第二次标记时该对象又成功被引用,则会被移除即将回收的集合,否则会被回收。
总之,JVM在做垃圾回收的时候,会检查堆中的所有对象否会被这些根集对象引用,不能够被引用的对象就会被圾收集器回收。一般回收算法也有如下几种:
1).标记-清除(Mark-sweep)
标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
2).标记-整理(Mark-Compact)
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。该垃圾回收算法适用于对象存活率高的场景(老年代)。
3).复制(Copying)
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。
4).分代收集算法
不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。Java堆内存一般可以分为新生代、老年代和永久代三个模块:
新生代:
1.所有新生成的对象首先都是放在新生代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。
2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
老年代:
1.在老年代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC,即Full GC。Full GC发生频率比较低,老年代对象存活时间比较长。
永久代:
永久代主要存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如使用反射、动态代理、CGLib等bytecode框架时,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。
Minor GC/Major GC/Full GC
- Minor GC(Young GC):即新生代(分为一个 Eden 区和两个 Survivor 区)的垃圾回收
- Eden 区无用对象被回收,存活对象会移到 Survivor 区
- Survivor 区的存活对象会被复制到另一个 Survivor 区,复制次数也记做年龄,年龄足够大时(15)会移到老年代
- 如果 Survivor 区已满,则存活对象会被提前移动到老年代(过早提升),如果老年代也无法容纳,则会触发 Full GC(提升失败)
- 老年代的对象可能引用新生代对象,所以这个引用会被作为 GC Roots
- Major GC:通常是跟 Full GC 等价的,回收整个堆
- Full GC:回收整个堆,包括新生代和老年代
- 当要在老年代分配空间但无法容纳时触发
- 当主动调用 System.gc 时触发
垃圾收集器
垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现:
-
Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
-
Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
-
ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
-
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
-
Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
-
Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
-
G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
内存分配和回收策略
JAVA自动内存管理:给对象分配内存 以及 回收分配给对象的内存。
1、对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。
2、大对象直接进入老年代。如很长的字符串以及数组。很长的字符串以及数组。
3、长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。
4、动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。