垃圾回收与算法
一、GC要做的三件事情:
1.1、那些内存需要回收?
垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”,哪些已经“死去”(即不可能再被任何途径使用的对象)。
判断对象是否存活都与“引用”有关,Java中又加入了一些更富表达意义的引用概念:强引用,软引用,弱引用和虚引用,依次来判定一个对象是否可以被回收。
1.1.1、java四种引用类型:
强引用:类似Object obj = new Object();只要强引用还在,就永远不会被回收。
在java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的。也就是说使用了该类对象以后永远都不会被用到jvm,当然也不会被回收
因此:强引用是造成内存泄露的主要原因之一。
软引用:用来描述一些还有用但非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围之中进行第二次回收。
软引用需要用 SoftReference 类来实现,软引用通常用在对内存敏感的程序中。
弱引用:也是描述一些还有用但非必须的对象,比软引用强度更弱,被弱引用关联的对象,只能生存到下一次垃圾收集发生之前。。
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存
虚引用:为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
虚引用需要 PhantomReference 类来实现,它不能单独
1.2、什么时候回收?
-
首先当程序处于空闲时会触发GC
-
程序不可预知时,手动调用system.gc()进行垃圾回收。(不推荐)
-
java堆内存不足时,GC会被调用(当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止)
1.2.1、minor GC和Full GC(major GC)
- Minor GC:新生代GC,指发生在新生代的垃圾收集动作,所有的Minor GC都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个过程非常短暂。
- Full GC 也叫(major GC):老年代GC,指发生在老年代的GC。
1.2.2、java堆内存分为新生代和老年代,而新生代中又分为1个eden区和2个survior区(比例分配是8:1:1)
一般情况下,新创建的对象都会被分配到eden区中,这些对象经过minor gc后仍然会存活,并且会被转移到survior区中,对象在survior中每熬过一个minor gc,年龄就会增加一岁,当年龄增加到一定程度后就会被转移到老年代中。
为什么会有两个survior区?:(这两个区域大小相等)
因为假设设想一下只有一个 Survibor 区 那么就无法实现对于 S0 区的垃圾收集,以及分代年龄的提升。
1.2.3、年轻代的垃圾回收算法:(复制算法)
-
复制算法思想:将内存分为两块,每次只有其中一块,当这一块儿内存使用完,就将还存活的对象复制到另一块上面。(复制算法不会产生内存碎片)
-
复制算法分析1::当eden区满时,还存活的对象将被复制到survior区,当一个survior区满时,此区域的存活对象将被复制到另外一个survior区中,当另一个survior区也满了时,之前从survior复制过来的并且此时还存活的对象,将可能会被复制到老年代。因为年轻代中的对象基本都是朝生夕死(80%以上)
-
复制算法分析2::在GC开始的时候,对象只会存在于eden区,和名为“From”的Survior区,Survior区“to”是空的。紧接着GC
eden区中所有存活的对象都会被复制到“To”,而在from区中,仍存活的对象会根据他们的年龄值来决定去向,
年龄到达一定只的对象会被复制到老年代,没有到达的对象会被复制到to survior中,经过这次gc后,eden区和from
survior区已经被清空。这个时候,from和to会交换他们的角色,也就是新的to就是上次GC前的from
1.2.4、Minor Gc 从年轻代回收内存:
当jvm无法为一个新的对象分配空间时会触发Minor GC,比如当Eden区满了。
当内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden和Survior区不存在内存碎片
写指针总是停留在所使用内存池的顶部。执行minor操作时不会影响到永久代,从永久带到年轻代的引用被当成
GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉(永久代用来存放java的类信息)。如果eden区域中大部分
对象被认为是垃圾,永远也不会复制到Survior区域或者老年代空间。如果正好相反,eden区域大部分新生对象不符合GC
条件,Minor GC执行时暂停的线程时间将会长很多。Minor may call “stop the world”;
1.2.5、Full GC:是清理整个堆空间包括年轻代和老年代。
1.2.6、当内存池被填满时:
当jvm无法为一个新的对象分配空间时会触发Minor GC,比如当Eden区满了。
当内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden和Survior区不存在内存碎片
写指针总是停留在所使用内存池的顶部。执行minor操作时不会影响到永久代,从永久带到年轻代的引用被当成
GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉(永久代用来存放java的类信息)。如果eden区域中大部分
对象被认为是垃圾,永远也不会复制到Survior区域或者老年代空间。如果正好相反,eden区域大部分新生对象不符合GC
条件,Minor GC执行时暂停的线程时间将会长很多。Minor may call “stop the world”;
1.2.7、执行时机(触发条件):
- 那么对于Minor GC的触发条件:大多数情况下,直接在eden区中进行分配。如果eden区域没有足够的空间,
那么就会发起一次Minor GC;
-
对于FullGC的触发条件:如果老年代没有足够的空间,那么就会进行一次FullGC
- 对于一个大对象,我们会首先在Eden 尝试创建,如果创建不了,就会触发Minor GC
- 随后继续尝试在Eden区存放,发现仍然放不下
- 尝试直接进入老年代,老年代也放不下
- 触发 Major GC 清理老年代的空间
- 放的下 成功
- 放不下 OOM
-
在发生MinorGC之前,虚拟机会先检查老年代最大可利用的连续空间是否大于新生代所有对象的总空间。
如果大于则进行Minor GC,如果小于则看HandlePromotionFailure设置是否是允许担保失败(不允许则直接FullGC)
如果允许,那么会继续检查老年代最大可利用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于
则尝试minor gc (如果尝试失败也会触发Full GC),如果小于则进行Full GC。
但是,具体什么时候执行,这个是由系统来进行决定的,是无法预测的。
-
发生在老年代的GC ,基本上发生了一次Major GC 就会发生一次 Minor GC。并且Major GC 的速度往往会比 Minor GC 慢 10 倍。
1.3、怎么回收(回收什么东西)?
二、如何判定那些对象成为垃圾?
2.1、引用计数法(Reference Counting):
概念:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:实现简单、判定效率高。
缺点:很难解决对象之间相互循环引用的问题。
结论:虚拟机不是通过引用计数法来判断对象是否存活的。
2.2、根搜索算法(GC Roots Tracing)也叫(可达性分析算法)
概念:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即不可达),则证明此对象是不可用的。
可作为GC Roots的对象包括下面几种:
①:虚拟机栈中引用的对象;
②:方法区中类静态属性引用的对象;
③:方法区中常量引用的对象;
④:本地方法中Native方法引用的对象
三、垃圾收集算法:
3.1 标记清除算法(Mark-Sweep)
分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清
除阶段回收被标记的对象所占用的空间。
缺点:内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题,资源浪费。
3.2 复制算法(copying)
概念:为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小
的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用
的内存清掉
缺点:虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原
本的一半。且存活对象增多的话,Copying 算法的效率会大大降低
3.3 标记整理算法(Mark-Compact)
概念:结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清
理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
3.4 分代收集算法
概念:分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存
划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young
Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃
圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
3.4.1 新生代与复制算法
目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要
回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代
划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),(大概比例8:1)每次使用
Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另
一块 Survivor 空间中。
3.4.2 老年代与标记复制算法
老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。
-
JAVA 虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储 class 类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
-
对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老生代。
-
当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From Space 进行清理。
-
如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。
-
在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。
-
当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被
移到老生代中。
四、JVM参数:
-
-XX:NewSize和-XX:MaxNewSize
用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
-
-XX:SurvivorRatio
用于设置Eden和其中一个Survivor的比值,这个值也比较重要。
-
-XX:+PrintTenuringDistribution
这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。
-
-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。
-
-Xms:
参数含义:初始堆大小
默认值:物理内存的1/64(小于1GB)
-
-Xmx:
参数含义: 最大堆大小
默认值:物理内存的1/4(小于1GB)
-
-Xmn:年轻代大小
注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。
整个堆大小=年轻代大小 + 年老代大小 + 持久代大小.
增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。 -
-XX:NewSize: 设置年轻代大小
-
-XX:MaxNewSize: 年轻代最大值
-
-XX:PermSize: 设置持久代(perm gen)初始值(默认值为物理内存的1/64)
-
-XX:MaxPermSize:设置持久代最大值(默认值为物理内存的1/4)
-
-XX:+PrintGCDetails 设置输出形式:
输出形式: [GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]