垃圾回收和内存分配策略

垃圾收集(Garbage Collection,下文简称GC)

谁需要GC?

垃圾收集主要是针对堆和方法区(永久代|1.8叫元空间)进行。
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。

判断一个对象是否可被回收

在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就 是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对 象)了。

1、引用计数算法

为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

public class Test {

    public Object instance = null;

    public static void main(String[] args) {
        Test a = new Test();
        Test b = new Test();
        a.instance = b;
        b.instance = a;
        a = null;
        b = null;
        doSomething();
    }
}

在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。

2、可达性分析算法

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

如图所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的, 因此它们将会被判定为可回收的对象。

作为GC Roots的对象包括下面几种:
1、虚拟机栈(栈帧中的本地变量表)中引用的对象。
2、本地方法栈中JNI(即一般说的Native方法)引用的对象。
3、方法区中类静态属性引用的对象。

static User u=new User();

4、方法区中常量引用的对象。

final static User u=new User();

5、所有被同步锁(synchronized关键字)持有的对象。

引用类型

Java 提供了四种强度不同的引用类型。

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软 引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强 度依次逐渐减弱。

强引用

强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值。
无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

被强引用关联的对象不会被回收。

Object obj = new Object();

软引用

软引用是用来描述一些还有用,但非必须的对象。
只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常

被软引用关联的对象只有在内存不够的情况下才会被回收。

Object obj = new Object();
SoftReference<object> sf = new SoftReference<object>(obj);
obj = null; 

示例

@Data
@AllArgsConstructor
public class User {
    private int id;
    private String name;
}
/**
 * 软引用的对象在内存不足时才会被回收
 * -Xms10m -Xmx10m -XX:+PrintGC
 */
public class TestSoftRef {

    public static void main(String[] args) {

        User u = new User(1, "zl");
        SoftReference<user> userSoft = new SoftReference<>(u);
        //保证new User(1,"Mark")这个实例只有userSoft在软引用
        u = null;

        System.out.println("beforeGC->" + userSoft.get());
        System.gc();
        System.out.println("AfterGc->" + userSoft.get());//new User(1,"Mark")没有被回收

        List<byte[]> list = new LinkedList<>();

        try {
            for (int i = 0; i < 100; i++) {
                //User(1,"Mark")实例一直存在
                System.out.println("********************" + userSoft.get());
                list.add(new byte[1024 * 1024 * 1]);
            }
        } catch (Throwable e) {
            //抛出了OOM异常后打印的,User(1,"Mark")这个实例被回收了
            e.printStackTrace();
            System.out.println("Throwable********************" + userSoft.get());
        }

    }
}

运行结果:


弱引用

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些。
被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。

Object obj = new Object();
WeakReference<object> wf = new WeakReference<object>(obj);
obj = null;

示例

/** * 弱引用的对象下一次GC时一定会被回收 */public class TestWeakRef {    public static void main(String[] args) {        User u = new User(1, "Mark");        WeakReference<user> userWeak = new WeakReference<>(u);        u = null;        System.out.println("beforeGC->" + userWeak.get());        System.gc();        System.out.println("AfterGc->" + userWeak.get());    }}

运行结果:


虚引用

虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

幽灵引用,最弱,被垃圾回收的时候收到一个通知

Object obj = new Object();PhantomReference<object> pf = new PhantomReference<object>(obj, null);obj = null;

注意:软引用 SoftReference和弱引用 WeakReference,可以用在内存资源紧张的情况下以及创建不是很重要的数据缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。
例如,一个程序用来处理用户提供的图片。如果将所有图片读入内存,这样虽然可以很快的打开图片,但内存空间使用巨大,一些使用较少的图片浪费内存空间,需要手动从内存中移除。如果每次打开图片都从磁盘文件中读取到内存再显示出来,虽然内存占用较少,但一些经常使用的图片每次打开都要访问磁盘,代价巨大。这个时候就可以用软引用构建缓存。

垃圾收集算法

标记-清除算法

标记-清除算法


首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。
标记-清除算法是最基础的收集算法,后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的。

缺点
1、执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低。
2、内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-复制算法(新生代)

标记-复制算法


将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

缺点:
将可用内存缩小为了原来的一半,浪费内存。

标记-整理算法

标记-整理算法


让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点:不会产生内存碎片
缺点:需要移动大量对象,处理效率比较低。

分代收集理论

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

它建立在两个分代假说之上:

  1. 弱分代假说:绝大多数对象都是朝生夕灭的。
  2. 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

一般将堆分为新生代老年代

  • 新生代使用:复制算法

  • 老年代使用:标记-清除 或者 标记-整理 算法


专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[1]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清除”或者“标记—整理”算法来进行回收。


上图所示的 eden 区、s0("From") 区、s1("To") 区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s1("To"),并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。经过这次GC后,Eden区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。

垃圾收集的分类

新生代收集(Minor GC)

指目标只是新生代的垃圾收集。

Minor GC 非常频繁,回收速度一般也比较快。

老年代收集(Major GC)

指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。

出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

混合收集(Mixed GC)

指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为。

整堆收集(Full GC)

收集整个Java堆和方法区的垃圾收集。

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

1、调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2、老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

3、空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。

4、JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

5、Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

内存分配策略

1、对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。

2、大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。

3、长期存活的对象进入老年代

虚拟机给每个对象定义了一个对 象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象 年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程 度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold设置。

4、动态对象年龄判定

虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

5、空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。

垃圾收集器

java -XX:+PrintCommandLineFlags -version 查看使用的垃圾回收器

-XX:+UseSerialGC 			新生代和老年代都用串行收集器
-XX:+UseParNewGC 			新生代使用ParNew,老年代使用Serial Old
-XX:+UseParallelGC 			新生代使用ParallerGC,老年代使用Serial Old(JDK1.8默认)
-XX:+UseParallerOldGC		新生代使用ParallerGC,老年代使用Parallel Old
-XX:+UseConcMarkSweepGC		新生代使用ParNew,老年代的用CMS
-XX:+UseG1GC			G1

并行:垃圾收集的多线程的同时进行。

并发:垃圾收集的多线程和应用的多线程同时进行。

搭配

image


新生代

新生代


Serial

最古老的,单线程,独占式,成熟,适合单CPU 服务器

ParNew

它是 Serial 收集器的多线程版本。

Parallel Scavenge

与 ParNew 一样是多线程收集器。

其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。

这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。

老年代

老年代


Serial Old

是 Serial 收集器的老年代版本

Parallel Old

是 Parallel Scavenge 收集器的老年代版本。

CMS

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。

G1

G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。

堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。


未来的垃圾回收</byte[]>

posted @ 2021-09-08 17:06  zlintent  阅读(69)  评论(0)    收藏  举报