我们都知道,在JAVA虚拟机中进行垃圾回收的场所有2个:堆,方法区。那么,首先要弄清什么是垃圾呢?——没有任何引用指向的一个对象或者多个对象的叫做垃圾。其次,如何定位到垃圾? 主要使用两个方法:
1.引用计数法(ReferenceCount) 在Java中,引用和对象是有关联的。通过引用计数来判断一个对象是否可回收。每个对象都有一个引用计数器,每被引用一次,计数器就+1,当对象引用失效一次,则计数器就-1,计数器为0的对象就视为垃圾对象。
缺点:当有A、B、C三个对象相互指向时,即A——>B, B——>C, C——>A;他们互相指向,但外部无指向,应该是垃圾;但因互相之间有指向,所以使用引用计数法无法定位。
2.根可达算法(RootSearching) main()方法里用到的,主要栈对象都是根。通过根search到的对象都是有用的对象,其他都是垃圾。一般更多使用根可达算法定位垃圾。
堆是被线程共享的一块内存区域,创建的对象和数组都保存在JAVA堆内存中,因此也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代VM使用分代收集算法,因此,JAVA堆从GC的角度可以分为新生代(Eden区,SurvivorFrom区,SurvivorTo区)和老年代。
用来存放新生的对象,占据堆1/3空间,由于频繁地创建新的对象,所以新生代会频繁出发MinorGC进行垃圾回收。新生代分为Eden区,SurvivorFrom区,SurvivorTo区。
Eden Space是Java新对象的出生地,如果Eden Space内存不够就会触发MinorGC,进行一次垃圾回收;如果新创建的对象占用内存过大,则会直接分配到老年代。
根据字面意思就可以推断出大概,是上一次GC的幸存者,在这一次GC被扫描。
对象的内存分配主要在新生代的Eden Space和SurvivorFrom区(Survivor目前存放对象的那一块),少数情况会直接分配给老年代。当这两个区内存空间不够时,就会进行GC。他们中的存活对象移到To区;如果To区无法足够存储某个对象,则将这个对象存储到老生代。
进行GC后,使用的便是Eden Space和SurvivorTo区。存放幸存者。
注:当对象在Survivor区躲过一次GC后,其年龄就会 +1,默认情况下年龄达到15的对象就会被移动到老生代。 这里的MinorGC采用复制算法,主要的步骤是:复制—>清空—>互换。互换就是原To区变成下一次GC的From区。
主要用于存放程序生命周期比较久的内存对象。 由于老年代的对象比较稳定,所以不会频繁的触发MajorGC。MajorGC采用标记-清除算法,先扫描再回收,耗时比较长,会产生内存碎片。
当老年代空间也满了装不下的时候,就会抛出OOM(Out Of Memory)异常。
处于方法区的永久代(Permanent Generation)是指内存的永久保存区域,用来存储class类、常量、方法描述等。对于永久代的回收主要包括废弃常量和无用的类。
其中,class在被加载的时候被放入永久区域,和存放实例的区域不同,GC不会在主程序运行的时候对永久区进行清理。但这同时也导致了一个问题——随着加载的class类越来越多,永久区空间不足而胀满,最终抛出OOM异常。
在Java8中,永久代被移除,被一个称为"元数据区"(元空间)的区域所替代。元空间本质上和永久代类似,但是最大的区别是—— 元空间存在本地内存上,并不存在虚拟机上,所以在默认情况下,元空间大小受本地内存的控制。 类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
垃圾收集算法有标记-清除(Mark-Sweep)算法,复制(copying)算法,标记-整理(Mark-compact)算法,分代收集(Generation Collecting)算法。
最基础的垃圾回收算法,主要步骤是:先标记后清除。先标记出所有需要回收的对象,清除阶段回收所有被标记的对象所占用的空间。
缺点:内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。将一块内存平均分为等大小的两块内存,每次只使用其中一块内存,当该块内存空间存满后,就将尚存活的对象复制到另一块内存上,把剩下的已使用的内存清理掉。
优点:实现简单,内存效率高,不易产生碎片 缺点:很明显,不仅是可用内存被压缩到原来的一半,并且存活对象增多的话,算法的效率就会大打折扣。
结合了以上两个算法,为了避免缺陷而提出。 标记阶段不变,标记后不再是清除,而是整理——将存活对象移向内存的一端,然后清除端外边界的对象。
分代收集(Generation Collecting)算法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。
老生代(Mark-compact)的特点是:每次垃圾回收时只有少量对象需要被回收。 新生代(Copying)的特点是:每次垃圾回收时都有大量垃圾需要被回收。 因此可以根据不同区域选择不同的算法。
分区算法是指,将整个堆空间划分为连续的不同小区间region, 每个小区间独立使用, 独立回收。
优点: 可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次 GC 所产生的停顿。
Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法;老年代主要使用标记-整理垃圾回收算法。JDK1.6 中 Sun HotSpot 虚拟机的垃圾收集器如下:(图源网络)
1.有STW,要停止应用停顿时间长。 2.简单高效,对单个COU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。
1.几乎是Serial收集器的多线程版。 2.有STW,但有特定的增强配给GMS使用。
1.吞吐量优先收集器。STW时间缩短是以牺牲吞吐量和新生代空间换取的。 2.与ParNew区别:有自适应调节策略。
Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。 在 Server 模式下,主要有两个用途:
在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。作为年老代中使用 CMS 收集器的后备垃圾收集方案。Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供。 在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
CMS(Concurrent Mark Sweep)是一种年老代垃圾收集器,以获取最短STW时间为目标。工作过程有4个阶段:
1.初始标记(initial mark):标记GCRoots能直接关联到的对象,速度很快,但存在STW。
2.并发标记:进行GCRoots Tracing,耗时很长。
3.重新标记:修正漏标错标,耗时较长,有STW。
4.并发清除:回收内存空间,耗时很长。在这一过程中,也会产生垃圾,这个垃圾叫做floatingGarbage。
*只有initial mark和remark阶段会产生STW。
缺点: 1.内存碎片太大。 2.产生floatingGarbage,只能在下一次清理时解决。
以上操作必须扫描整个老年代/新生代,不适合大内存;Young/Old都是独立的内存块,大小必须提前确定。
G1是一种服务端应用使用的垃圾收集器。主要目标是用在多核、大内存的机器上。通过分区(region),首先收集垃圾最多的区域,花费较少时间回收垃圾较少的区域。 存在Full GC:1.基于标记-整理算法,不产生内存碎片。2.可精确控制STW时间,在不牺牲吞吐量的前提下,实现低停顿垃圾回收。 工作过程有四个: 1.初始标记。 2.并发标记。(三色标记算法) 3.最终标记。(Remember set logs) 4.筛选回收。
相比与 CMS 收集器,G1 收 集器两个最突出的改进是:
基于标记-整理算法,不产生内存碎片。可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。 G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收 集效率.