垃圾判断:
-
引用计数算法:
在对象中添加一个引用计数器,当每有一个地方引用它时,计数器值加一。当引用失效时,计数器值就减一。当一个对象的计数器为零时,表示该对象没有被任何其他对象引用,因此可以被释放。
- 优点:是可以及时回收垃圾对象,避免内存泄漏,且不会产生暂停时间。
- 缺点:维护计数器会增加额外的开销。无法处理循环引用的情况,即两个或多个对象互相引用,导致它们的计数器永远不为零,无法被回收。主流的Java虚拟机里面都没有选用引用计数算法来管理内存。
-
可达性分析算法:
通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
GC Roots:虚拟机栈中引用的对象、在方法区中类静态属性引用的对象、在方法区中常量引用的对象、在本地方法栈中JNI引用的对象、所有被同步锁持有的对象。
图中虽然object4、object5、object6之间有互相引用,但它们到GC Roots不可达,所以它们被判定为可回收对象。
再介绍一下引用的区别:
- 强引用:最常见的引用类型,就是new出来的对象。只要有强引用指向对象,对象就不会被垃圾回收器回收。
- 软引用:只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。
- 弱引用:被弱引用关联的对象只 能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只 被弱引用关联的对象。
- 虚引用:一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
垃圾回收算法:
再将回收算法前我们需要了解两个假说:
- 弱分代假说:绝大多数对象都是朝生夕灭的。
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分 出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
1、标记清除算法
这个算法正如他的名字一样,算法分为“标记”和清除两个阶段。先把需要清除的对象进行标记,再标记完成后,统一回收掉所有被标记的对象,也可反过来,标记存活的对象,统一回收所有未被标记的对象。这是最基础的收集算法,后续大多数的收集算法都是以标记清除算法为基础。
缺点:第一个执行效率不稳定,如果有大量的对象需要清除,那么要进行大量的标记和清除,效率随回收对象的增长而降低(所以比较适合老年代)。第二个内存空间会存在碎片化问题,使其原本有足够大空间,但由于太过碎片化无法存大对象,从而不得不提前触发垃圾收集。
2、标记复制算法
这个算法主要是用来解决当有大量需要回收的对象时效率低下的问题。标记的不要回收的对象,但我们要操作的是不需要回收的对象,此时我们所需要的时间就会少很多。这个方法则是半区复制法。我们将内存等半分,每次只使用一块,当第一快内存用完之后,只要将还存活的对象复制到第二块去,然后将第一块内存清空。这样往复操作。这个操作简单高效,还解决了内存碎片化问题。缺点就是每次内存只能使用一半,太过于浪费。
现在好多JVM大多使用这样是收集算法去回收新生代,但是并不是1:1的比例,而是将其划分为 Eden(8):Survivor(1):Survivor(1),每次分配内存只使用Eden和其中一块Survuvor,当发生垃圾收集时,将这个块Survuvor和Eden中存活的对象复制到空闲那块Survuvor上去,然后之间清空这个块Survuvor和Eden。这个比例是通过测试得来的合理分配。这样一来才浪费了10%的内存。可以接受。当然如果出现了某次意外,一个Survivor无法存放所有的存活对象,可以向老年代“借”内存用来存放。
3、标记整理算法
标记整理算法就是将需要回收的对象标记然后清除,最后将存活下来的对象进行整理。这样子即利用了100%的内存空间,又解决内存碎片化问题。但是如果是向老年区这个每次清除都有大量的对象存活的区域,那么移动对象也是很大的负担。所以一般都是先进行标记清除算法,待碎片化太过于严重再使用一次标记整理算法,解决碎片化。这样的方式被CMS收集器使用着。
ps:移动过程中要停止用户线程。