参考
新生代老生代
垃圾回收算法参考
优化参考
面向GC编程
Java 堆内存
面试GC问答
一、算法
引用计数法:一个对象被引用计数器加一,取消引用计数器减一,引用计数器为0才能被回收。优点:简单。缺点:不能解决循环引用的问题,比如A引用B,B引用A,但是这两个对象没有被其他任何对象引用,属于垃圾对象,却不能回收;每次引用都会附件一个加减法,影响性能。
引用计数的方法由于存在显著的缺点,实际上并未被JVM所使用。标记清除法:分为两个阶段:标记阶段和清除阶段。标记阶段通过根节点标记所有可达对象,清除阶段清除所有不可达对象。缺点:因为清除不可达对象之后剩余的内存不连续,会产生大量内存碎片,不利于大对象的分配。由于无法找到足够的连续内存,不得不触发另一次GC。
可以做根节点的对象:虚拟机栈(栈帧中的本地变量表)中的引用的对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI(Native方法)的引用对象
复制算法:将内存空间分成相同的两块,每次只是用其中的一块,垃圾回收时,将正在使用的内存中的存活对象复制到另外一块空间,然后清除正在使用的内存空间中的所有对象,这种回收算法适用于新生代垃圾回收。优点:垃圾回收对象比较多时需要复制的对象很少,性能较好;不会存在内存碎片。缺点:将系统内存折半。
标记压缩算法:是一种老年代回收算法,在标记清除的基础上做了一些优化,首先从根节点开始标记所有不可达的对象,然后将所有可达的对象移动到内存的一端,最后清除所有不可达的对象。优点:不用将内存分为两块;不会产生内存碎片。
分代算法:新生代使用复制算法,老生代使用标记清除算法或者标记压缩算法。几乎所有的垃圾回收期都区分新生代和老生带。
分区算法:将整个堆空间分成很多个连续的不同的小空间,每个小空间独立使用,独立回收。为了更好的控制gc停顿时间,可以根据目标停顿时间合理地回收若干个小区间,而不是整个堆空间,从而减少gc停顿时间。
二、新生代young generation(执行minor gc 复制算法)和老年代old generation(执行major gc或叫full gc标记清除算法)
为什么要分代?
总有一些对象是长期使用的,如果每次GC都扫描他们,然后发现无法回收,这样效率就很差。所以就把对象增加了一个年龄属性,这样如果每次GC都回收不掉他们,他们的年龄就增加1,慢慢地就变成老年人了。达到一定年龄后,就要分区了。老年人会离开新生代区域,进入老年代区域。这样常规的GC只扫描新生代区域,处于老年代区域的对象就不会被频繁地扫描了。
80%的对象都是朝生夕死,对于负载不高的应用,可能数月都不会发生FullGC。新生代再细分,有三个区域:Eden区,即伊甸园区。还有2个Survivor区,即幸存者区,分别叫from和to。from和to这两块就是复制算法中提到的两块内存,它们互相复制来复制去,互相换着当from和to.当然其中有一个肯定是空的。当eden满时,minorgc开始,Eden区也会执行复制算法,把所有存活对象全部复制到to区域。from区域的对象会根据年龄决定去向,达到要求的就去老年代,没达到要求的也会进入to区域。然后,Eden和from区都会被清空。当to区域也被塞满时,所有对象都会被复制到老年区。
一个对象的一辈子:我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。
新生代98%的对象都是朝生夕死,虚拟机默认eden和survivor大小比例是8;1,也就是每次只有10%的内存空间是作为to区域被浪费,而不是传统复制算法中所说的浪费一半内存。
三、面向GC编程优化
- 减少创建对象的数量,比如使用StringBuilder代替string
- 老年代的GC很耗时,要减少进入老年代的对象数量。比如对象池,因为对象长期存活,会晋升到老年代,影响GC效率。另外对象池通常涉及并发访问,处理同步带来的开销也很大。而重新创建一个对象的开销,可能比这两者都要小。
- 在一个非常大的方法体内,对于较大的对象,将其引用置为null,某种程度可以帮助GC。但大部分情况下这种行为意义不大。
- 一些基于数组的数据结构,例如StringBuilder、StringBuffer、ArrayList、HashMap等等,在扩容的时候都需要做ArrayCopy,对于不断增长的结构来说,经过若干次扩容,会存在大量无用的老数组,而回收这些数组的压力,全都会加在GC身上。这些容器的构造函数中通常都有一个可以指定大小的参数,如果对于某些大小可以预估的容器,建议加上这个参数。
- 尽可能缩小对象的作用域,即生命周期。如果可以在方法内声明的局部变量,就不要声明为实例变量。除非你的对象是单例的或不变的,否则尽可能少地声明static变量。