垃圾收集只确认两个问题
- 什么样的对象需要被清理
- 怎么清理这些需要被清理的对象
一.什么样的对象需要被清理
方便一点,凡是被一下算法标记的对象,我称之为死掉的对象
1.引用计数算法
就是给每个对象添加一个引用计数器,当对象被引用一次,计数器就+1;引用失效时,计数器就-1。当计数器为0,就说明对象死了。这个方法实现简单,效率也可观。但是,主流的JVM没用这个算法,因为这个算法很难解决循环引用的问题。也就是A依赖B,B依赖A,但是对象堆里面没有任何其他对象不依赖AB了,可是AB各自还保留一个被对方引用的次数,无法被收集。
2.可达性算法
这个比上面高大上一点,Java通过可达性分析来判定对象是否还被引用。什么的可达性分析呢:
Java会从一些叫做GCRoot的对象开始向下遍历,可以遍历到的对象,就是被引用的对象,不可以遍历到的对象就是不可达对象,就是死掉的对象了
GCRoot 包括:栈中引用对象,方法区静态引用对象,方法区常量引用对象,本地方法引用对象(Native层的
二.怎么清理这些需要被清理的对象
以上算法找出来了死掉的对象
,如何处理他们,是另一个需要解决的问题~
1.标记-清除算法
把死掉的对象标记出来,然后清除,大部分算法是基于这个思想,不足之处也很明显,1是效率问题,标记和清除的过程都慢,2是空间问题,清除之后会带啦大量的不连续碎片空间。小的碎片会放不下大对象,导致大对象创建时又会触发一次回收 回收前 回收后
2.复制算法
一般都会说,这种算法将内存分成相同大小。
回收前
回收后
其实吧两块其实并不是非要等比划分内存的,大部分对象死的很早。Hotspot是划分了三块区域,一块大的两块小的,大的叫Eden,小的叫survivor,大小比例为8:1。清理时将Eden和survivor中存活的对象复制到另一块survivor内存上,然后,清理掉用过的两块内存,下次再用。当survivor不够大的时候,需要依靠新的分配担保去拓展空间。
3.标记-整理算法
合复制和标记算法,整理算法会把有用的存活对象向y,一端移动,这样避免了复制算法浪费那么多内存,也不会像普通标记回收算法一样导致内存碎片过于严重。
回收前
回收后
4.分代收集算法
将java堆内存分成老年代,新生代。根据经验,新生代死亡比较快,老年代比较持久。所以一般新生代区域使用复制方法,只需要复制几个就可以了,老年代比较持久,所以一般用标记清除,或标记整理来回收。其实sun的jvm就是采用这个中搜集策略,细则如下
永久存储区(Permanent Space):永久存储区是 JVM 的驻留内存,用于存放 JDK 自身所携带的 Class,Interface 的元数据,应用服务器允许必须的 Class,Interface 的元数据和 Java 程序运行时需要的 Class 和 Interface 的元 数据。被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 时,释放 此区域所控制的内存。
堆空间(The Heap Space):是 JAVA 对象生死存亡的地区,JAVA 对象的出生, 成长,死亡都在这个区域完成。堆空间又分别按 JAVA 对象的创建和年龄特征分 为养老区和新生区。
新生区(Young (New) generation space):新生区的作用包括 JAVA 对象的创 建和从 JAVA 对象中筛选出能进入养老区的 JAVA 对象。
伊甸园(Eden space):JAVA 对空间中的所有对象在此出生,该区的名字因此 而得名。也即是说当你的 JAVA 程序运行时,需要创建新的对象,JVM 将在该区 为你创建一个指定的对象供程序使用。创建对象的依据即是永久存储区中的元数 据。
幸存者 0 区(Survivor 0 space)和幸存者 1 区(Survivor1 space):当伊甸 园的控件用完时,程序又需要创建对象;此时 JVM 的垃圾回收器将对伊甸园区进 行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁工作。同时 将伊甸园中的还有其他对象引用的对象移动到幸存者 0 区。幸存者 0 区就是用于 存放伊甸园垃圾回收时所幸存下来的 JAVA 对象。当将伊甸园中的还有其他对象 引用的对象移动到幸存者 0 区时,如果幸存者 0 区也没有空间来存放这些对象时, JVM 的垃圾回收器将对幸存者 0 区进行垃圾回收处理,将幸存者 0 区中不在有其 他对象引用的 JAVA 对象进行销毁,将幸存者 0 区中还有其他对象引用的对象移 动到幸存者 1 区。幸存者 1 区的作用就是用于存放幸存者 0 区垃圾回收处理所幸 存下来的 JAVA 对象。
养老区(Tenure (Old) generation space):用于保存从新生区筛选出来的 JAVA 对象。
上面我们看了 JVM 的内存分区管理,现在我们来看 JVM 的垃圾回收工作是怎样运 作的。首先当启动 J2EE 应用服务器时,JVM 随之启动,并将 JDK 的类和接口, 应用服务器运行时需要的类和接口以及 J2EE 应用的类和接口定义文件也及编译 后的 Class 文件或 JAR 包中的 Class 文件装载到 JVM 的永久存储区。在伊甸园中 创建 JVM,应用服务器运行时必须的 JAVA 对象,创建 J2EE 应用启动时必须创建 的 JAVA 对象;J2EE 应用启动完毕,可对外提供服务。
JVM 在伊甸园区根据用户的每次请求创建相应的 JAVA 对象,当伊甸园的空间不 足以用来创建新 JAVA 对象的时候,JVM 的垃圾回收器执行对伊甸园区的垃圾回 收工作,销毁那些不再被其他对象引用的 JAVA 对象(如果该对象仅仅被一个没
有其他对象引用的对象引用的话,此对象也被归为没有存在的必要,依此类推), 并将那些被其他对象所引用的 JAVA 对象移动到幸存者 0 区。
如果幸存者 0 区有足够控件存放则直接放到幸存者 0 区;如果幸存者 0 区没有足 够空间存放,则 JVM 的垃圾回收器执行对幸存者 0 区的垃圾回收工作,销毁那些 不再被其他对象引用的 JAVA 对象(如果该对象仅仅被一个没有其他对象引用的 对象引用的话,此对象也被归为没有存在的必要,依此类推),并将那些被其他 对象所引用的 JAVA 对象移动到幸存者 1 区。
如果幸存者 1 区有足够控件存放则直接放到幸存者 1 区;如果幸存者 0 区没有足 够空间存放,则 JVM 的垃圾回收器执行对幸存者 0 区的垃圾回收工作,销毁那些 不再被其他对象引用的 JAVA 对象(如果该对象仅仅被一个没有其他对象引用的 对象引用的话,此对象也被归为没有存在的必要,依此类推),并将那些被其他 对象所引用的 JAVA 对象移动到养老区。 如果养老区有足够控件存放则直接放到养老区;如果养老区没有足够空间存放, 则 JVM 的垃圾回收器执行对养老区区的垃圾回收工作,销毁那些不再被其他对象 引用的 JAVA 对象(如果该对象仅仅被一个没有其他对象引用的对象引用的话, 此对象也被归为没有存在的必要,依此类推),并保留那些被其他对象所引用的 JAVA 对象。如果到最后养老区,幸存者 1 区,幸存者 0 区和伊甸园区都没有空 间的话,则 JVM 会报告“JVM 堆空间溢出(java.lang.OutOfMemoryError: Java heap space)”,也即是在堆空间没有空间来创建对象。
这就是 JVM 的内存分区管理,相比不分区来说;一般情况下,垃圾回收的速度要 快很多;因为在没有必要的时候不用扫描整片内存而节省了大量时间。 通常大家还会遇到另外一种内存溢出错误“永久存储区溢出 (java.lang.OutOfMemoryError: Java Permanent Space)”。
二.怎么清理这些需要被清理的对象 (垃圾收集器)
- 新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge
- 老年代收集器使用的收集器:Serial Old、Parallel Old、CMS
Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。
Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。
*ParNew收集器(停止-复制算法) *
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
Parallel Scavenge收集器(停止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。
Parallel Old收集器(停止-复制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先
CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择
GC的执行机制
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
1.年老代(Tenured)被写满
2.持久代(Perm)被写满
3.System.gc()被显示调用
4.上一次GC之后Heap的各域分配策略动态变化