JVM—下


对象的创建流程与内存分配

创建流程

对象内存分配方式

内存分配的方法有两种:不同垃圾收集器不一样

  • 指针碰撞(Bump the Pointer)
  • 空闲列表(Free List)

指针碰撞示意图:

内存分配安全问题

在分配内存的时候,虚拟机给A线程分配内存过程中,指针未修改。此时B线程同时使用了同样一块内存。是不是就出现了线程的安全性问题?

在JVM中有两种解决办法:

  1. CAS 是乐观锁的一种实现方式。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。

  2. TLAB本地线程分配缓冲(Thread Local Allocation Buffer即TLAB):为每一个线程预先分配一块内存

JVM在第一次给线程中的对象分配内存时,首先使用CAS进行TLAB的分配。当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。

对象内存分配流程【重要】:

对象怎样才会进入老年代?

内存担保机制是个怎么回事?

对象内存分配:

  • 新生代:新对象大多数都默认进入新生代的Eden区
  • 进入老年代的条件:四种情况
    • 存活年龄太大,默认超过15次【-XX:MaxTenuringThreshold】
    • 动态年龄判断:MinorGC之后,发现Survivor区中的一批对象的总大小大于了这块Survivor区的50%,那么就会将此时大于等于这批对象年龄最大值的所有对象,直接进入老年代。
      • 举个栗子:Survivor区中有一批对象,年龄分别为年龄1+年龄2+年龄n的多个对象,对象总和大小超过了Survivor区域的50%,此时就会把年龄n及以上的对象都放入老年代。
      • 为什么会这样?希望那些可能是长期存活的对象,尽早进入老年代。
      • -XX:TargetSurvivorRatio可以指定
    • 大对象直接进入老年代:前提是Serial和ParNew收集器
      • 举个栗子:字符串或数组
      • -XX:PretenureSizeThreshold 一般设置为1M
      • 为什么会这样?为了避免大对象分配内存时的复制操作降低效率。避免了Eden和Survivor区的复制
    • MinorGC后,存活对象太多无法放入Survivor

空间担保机制:当新生代无法分配内存的时候,我们想把新生代的老对象转移到老年代,然后把新对象放入腾空的新生代。此种机制我们称之为内存担保。

  • MinorGC前,判断老年代可用内存是否小于新时代对象全部对象大小,如果小于则继续判断
  • 判断老年代可用内存大小是否小于之前每次MinorGC后进入老年代的对象平均大小
    • 如果是,则会进行一次FullGC,判断是否放得下,放不下OOM
    • 如果否,则会进行一些MinorGC:
      • MinorGC后,剩余存活对象小于Survivor区大小,直接进入Survivor区
      • MinorGC后,剩余存活对象大于Survivor区大小,但是小于老年代可用内存,直接进入老年代
      • MinorGC后,剩余存活对象大于Survivor区大小,也大于老年代可用内存,进行FullGC
      • FullGC之后,任然没有足够内存存放MinorGC的剩余对象,就会OOM

老年代的担保示意图:

案例演示:对象分配过程

01-大对象直接进入老年代

package com.hero.jvm.object;
/
 测试:大对象直接进入到老年代
 -Xmx60m -Xms60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
 -XX:PretenureSizeThreshold

/
public class YoungOldArea {
	public static void main(String[] args) {
		byte[] buffer = new byte[1024102420]; //20M
	}
}
  • -XX:NewRatio=2 新生代与老年代比值
  • -XX:SurvivorRatio=8 新生代中,Eden与两个Survivor区域比值
  • -XX:+PrintGCDetails 打印详细GC日志
  • -XX:PretenureSizeThreshold 对象超过多大直接在老年代配,默认值为0,不限制

02-对象内存分配的过程:

/
-Xmx600m -Xms600m -XX:+PrintGCDetails
/
public class HeapInstance {
	public static void main(String[] args) {
		List<Picture> list = new ArrayList<>();
		while (true){
			try {
				Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
			list.add(new Picture(new Random().nextInt(1024  1024)));
		}
	}
}
class Picture{
	private byte[] pixels;
	public Picture(int length){
		this.pixels = new byte[length];
	}
}

案例演示:内存担保机制

案例准备

  • JVM参数: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
  • 分配三个1MB的对象和一个5MB的对象
  • -Xmn10M新生代内存的最大值:包括Eden区和两个Survivor区的总和

代码如下:

/
 内存分配担保案例
/
public class MemoryAllocationGuarantee {
    private static final int _1MB = 1024  1024;
    public static void main(String[] args) {
    	memoryAllocation();
    }
    public static void memoryAllocation() {
    	byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[1  _1MB];//1M
        allocation2 = new byte[1  _1MB];//1M
        allocation3 = new byte[1  _1MB];//1M
        allocation4 = new byte[5  _1MB];//5M
        System.out.println("完毕");
    }
}

堆内存分配情况如下

设置JVM参数:

-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC

查看GC日志

通过GC日志我们发现:在分配allocation4的时候,发生了一次Minor GC,新生代从5268K变为了662K,但是你发现整个堆的占用并没有很大变化。

分析过程

担保前的堆空间:

发生Minor GC,触发担保机制:

放不下了怎么办? 内存担保

担保后的新生代:

担保后的老年代:

小结

  1. 当Eden区存储不下新分配的对象时,会触发minorGC

  2. GC之后,还存活的对象,按照正常逻辑,需要存入到Survivor区。

  3. 当无法存入到幸存区时,此时会触发担保机制

  4. 发生内存担保时,需要将Eden区GC之后还存活的对象放入老年代。后来的新对象或者数组放入Eden区。

对象内存布局:在堆中,对象里面都有些啥

对象里的三个区

  • 堆内存中,一个对象在内存中存储的布局可以分为三块区域:
  • 对象头(Header):Java对象头占8byte。如果是数组则占12byte。因为JVM里数组size需要使用4byte存储。
    • 标记字段MarkWord:
      • 用于存储对象自身的运行时数据,它是synchronized实现轻量级锁和偏向锁的关键。
      • 默认存储:对象HashCode、GC分代年龄、锁状态等等信息。
      • 为了节省空间,也会随着锁标志位的变化,存储数据发生变化。下面画图解释
    • 类型指针KlassPoint:
      • 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
      • 开启指针压缩存储空间4byte,不开启8byte。
      • JDK1.6+默认开启
    • 数组长度:如果对象是数组,则记录数组长度,占4个byte,如果对象不是数组则不存在。
    • 对齐填充:保证数组的大小永远是8byte的整数倍。
  • 实例数据(Instance Data):生成对象的时候,对象的非静态成员变量也会存入堆空间
  • 对齐填充(Padding):JVM内对象都采用8byte对齐,不够8byte的会自动补齐。

对象头的大小:

对象头信息是与对象自身定义的数据无关的额外存储成本。考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构,以便在极小的空间内,尽量多的存储数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态如下(JDK1.8)。

基本数据类型和包装类的内存占用情况(了解):

案例01

打印空对象的内存布局信息

1)代码

<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.9</version>
</dependency
import org.openjdk.jol.info.ClassLayout;
public class ObjLock01 {
	public static void main(String[] args) {
		Object o = new Object();
		System.out.println("new Object:" +
		ClassLayout.parseInstance(o).toPrintable());
	}
}

2)控制台输出

3)分析

  • 首先对象头是包含MarkWord和类型指针这两部分信息的;
  • 开启指针压缩的情况下,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节;
  • 新建Object对象,会在内存占用16个字节,其中Header占12个(MarkWord占8个+KlassPoint占4个),没有实例数据,补充对齐4个。

结论:对象大小 = 对象头12 + 实例数据0 + 对齐填充4 = 16 bytes

案例02

打印空对象和赋值后的对象内存布局信息

1)代码

import org.openjdk.jol.info.ClassLayout;
public class ObjLock02{
    public static void main(String[] args) {
        Hero a = new Hero();
        System.out.println("new A:" +
        ClassLayout.parseInstance(a).toPrintable());
        a.setFlag(true);
        a.setI(1);
        a.setStr("ABC");
        System.out.println("赋值 A:" +
        ClassLayout.parseInstance(a).toPrintable());
   }
        static class Hero {
            private boolean flag;
            private int i;
            private String str;
            public void setFlag(boolean flag) {
            	this.flag = flag;
            }
            public void setStr(String str) {
            	this.str = str;
            }
            public void setI(int i) {
            	this.i = i;
            }
        }
}

2)控制台输出

3)分析

  • 新建对象Hero时,对象头占12个(MarkWord占8个+KlassPoint占4个)
  • 实例数据中 boolean占一个字节,会补齐三个,int占4个,String占4个,无需补充对齐。

结论:对象的大小 = 12对象头 + 43的实例数据 + 0的填充 = 24bytes

如何访问一个对象呢?

有两种方式:

  1. 句柄:稳定,对象被移动只要修改句柄中的地址
  2. 直接指针:访问速度快,节省了一次指针定位的开销

通过句柄访问对象

通过直接指针访问对象

JVM垃圾收集器

GC基本原理

垃圾回收

为什么垃圾回收呢?

如果不进行垃圾收集,内存数据很快就会被占满

理论计算:

计算题:256byte 400w / 1024 /1024 = 976.5625 MB

400百万256byte请求 约等于对象 1GB

实际压测:90万请求 1GB 流量

什么是垃圾?

在内存中,没有被引用的对象就是垃圾。

如何找到这个垃圾?

主要是2种:引用计数法和根可达算法

1)引用计数法(Reference Counting)

通过引用计数方法,找到这个垃圾:

当这个对象引用都消失了,消失一个计数减一,当引用都消失了,计数就会变为0。此时这个对象就会变成垃圾。

在堆内存中主要的引用关系有如下三种:单一引用、循环引用、无引用

由此可见,引用计数算法不能解决循环引用问题。为了解决这个问题,JVM使用了根可达分析算法。

public class Test {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
        object1.object = object2;
        object2.object = object1;
        object1 = null;
        object2 = null;
    }
}
class MyObject{
	MyObject object;
}

2)根可达算法(GCRoots Tracing)

又叫根搜索算法。在主流的商用程序语言中(Java和C#),都是使用根搜索算法判定对象是否存活的。

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

可作GCRoots的对象:

  • 虚拟机栈中,栈帧的本地变量表引用的对象。
  • 方法区中,类静态属性引用的对象。
  • 方法区中,常量引用的对象。
  • 本地方法栈中,JNl引用的对象。

3)回收过程

即使在可达性分析算法中不可达的对象,也并非是“非死不可”。被判定不可达的对象处于“缓刑”阶段。要真正宣告死亡,至少要经历两次标记过程:

  • 第一次标记:如果对象可达性分析后,发现没有与GC Roots相连接的引用链,那它将会被第一次标记;
  • 第二次标记:第一次标记后,接着会进行一次筛选。筛选条件:此对象是否有必要执行finalize() 方法。在 finalize() 方法中没有重新与引用链建立关联关系的,将被进行第二次标记。

第二次标记成功的对象将真的会被回收,如果失败则继续存活

/
 演示:
 1.对象可以在被GC时自我拯救。
 2.机会只有一次,对象的finalize()方法只会被系统自动调用一次
/
public class finalizeEscapeGC {
    public static finalizeEscapeGC SAVE_HOOK = null;
    public void isAlive() {
    	System.out.println("你瞅啥, 哥还活着 :)");
    }
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("执行 finalize() !");
        finalizeEscapeGC.SAVE_HOOK = this;
    }
    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new finalizeEscapeGC();
        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        //因为finalize方法优先级很低,所以暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("哦不, 哥死了 :(");
        }
        //下面这段代码与上面的完全相同,但是这次自救却失败了
        SAVE_HOOK = null;
        System.gc();
        //因为finalize方法优先级很低,所以暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("哦不, 哥死了 :(");
        }
    }
}

4)对象引用

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)四种,这四种引用强度依次逐渐减弱。

  1. 强引用
  • 代码中普遍存在,只要强引用还在,就不会被GC。
Object obj = new Object();
  1. 软引用
  • 非必须引用,内存溢出之前进行回收,如内存还不够,才会抛异常。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
Object o = sf.get();//有时候会返回null
System.out.println("o = " + o);

应用场景:软引用可用来实现内存敏感的高速缓存。

举例:

  • 应用需要读取大量本地文件,如果每次读取都从硬盘读取会严重影响性能,如果一次性全部加载到内存,内存可能会溢出。
  • 可以使用软引用解决这个问题,使用一个HashMap来保存文件路径和文件对象管理的软引用之间的映射关系.
  • 内存不足时,JVM会自动回收缓存文件对象的占用空间,有效地避免了OOM问题。
Map<String, SoftReference<Bitmap>> fileCache = new HashMap<String,SoftReference<Bitmap>>
  1. 弱引用

非必须引用,只要有GC,就会被回收。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
//System.gc();
Object o = wf.get();//有时候会返回null
boolean enqueued = wf.isEnqueued();//返回是否被垃圾回收器标记为即将回收的垃圾
System.out.println("o = " + o);
System.out.println("enqueued = " + enqueued);
  • 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
  • 作用:监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。
  1. 虚引用
  • 虚引用是最弱的一种引用关系。垃圾回收时直接回收
  • 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, new
ReferenceQueue<>());
obj=null;
Object o = pf.get();//永远返回null
boolean enqueued = pf.isEnqueued();//返回是否从内存中已经删除
System.out.println("o = " + o);
System.out.println("enqueued = " + enqueued);
  • 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。

  • 作用:跟踪对象被垃圾回收的状态,仅仅是提供一种确保对象被回收后,做某些事情的机制。类似事件监听机制

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
public class TestReference {
	public static void main(String[] args) {
        //强引用
        // Object obj = new Object();
        //软引用
        // SoftReference<Object> sf = new SoftReference<Object>(obj);
        // obj = null;
        // Object o = sf.get();//有时候会返回null
        // System.out.println("o = " + o);
        //弱引用
        // Object obj = new Object();
        // WeakReference<Object> wf = new WeakReference<Object>(obj);
        // obj = null;
        // //System.gc();
        // Object o = wf.get();//有时候会返回null
        // boolean enqueued = wf.isEnqueued();//返回是否被垃圾回收器标记为即将回收
        的垃圾
        // System.out.println("o = " + o);
        // System.out.println("enqueued = " + enqueued);
        //虚引用
        Object obj = new Object();
        PhantomReference<Object> pf = new PhantomReference<Object>(obj, new
        ReferenceQueue<>());
        obj=null;
        Object o = pf.get();//永远返回null
        boolean enqueued = pf.isEnqueued();//返回是否从内存中已经删除
        System.out.println("o = " + o);
        System.out.println("enqueued = " + enqueued);
    }
}

如何清除垃圾?

JVM提供3种方法,清除垃圾对象:

  • Mark-Sweep 标记清除算法
  • Copying 拷贝算法
  • Mark-Compact 标记压缩算法

1)标记清除算法(Mark-Sweep)

最基本的算法,主要分为标记和清除2个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象

缺点:

  • 效率不高,标记和清除过程的效率都不高
  • 空间碎片,会产生大量不连续的内存碎片,会导致大对象可能无法分配,提前触发GC 。

2)拷贝算法(Copying)

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

现在商业虚拟机都是采用这种收集算法来回收新生代,当回收时,将Eden和Survivor中还存活着的对象拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

  • 优点:没有碎片化,所有的有用的空间都连接在一起,所有的空闲空间都连接在一起
  • 缺点:存在空间浪费

3)标记-整理算法(Mark-Compact)

老年代没有人担保,不能用复制回收算法。可以用标记-整理算法,标记过程仍然与“标记-清除”算法一样,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

缺点:性能较低,因为除了拷贝对象以外,还需要对象内存空间进行压缩,所以性能较低。

4)分代回收(Generational Collection)

当前商业虚拟机都是采用这种算法。根据对象的存活周期的不同将内存划分为几块。

  • 新生代,每次垃圾回收都有大量对象失去,选择复制算法。
  • 老年代,对象存活率高,无人进行分配担保,就必须采用标记清除或者标记整理算法

用什么清除垃圾

有 8 种不同的垃圾回收器,它们分别用于不同分代的垃圾回收。

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1、ZGC

两个垃圾回收器之间有连线表示它们可以搭配使用,可选的搭配方案如下:

串行收集器

基本概念

使用单线程进行垃圾回收的收集器,每次回收时,串行收集器只有一个工作线程,对于并行能力较弱的计算机来说,串行收集器性能会更好。

串行收集器可以在新生代和老年代中使用,根据作用于不同的堆空间,分为新生代串行收集器和老年代收集器。

配置参数 -XX:+UseSerialGC :年轻串行(Serial),老年串行(Serial Old)

Serial收集器:年轻串行

  • Serial收集器是新生代收集器,单线程执行,使用复制算法。
  • 进行垃圾收集时,必须暂停其他所有的工作线程。
  • 对于单个CPU的环境来说,Serial收集器由于没有线程交互的开销,收集效率更高。

Serial Old收集器:老年串行

Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。

什么是Safe point?

  • GC:保洁阿姨打扫
  • 应用程序:人弄乱屋子
  • 如果弄乱速度 > 打扫速度怎么办?
  • STW
  • 在哪里STW?沙发上,床上,桌子上还是砧板上

Safepoint挂起线程的点主要有:

  1. 循环的末尾

  2. 方法返回前

  3. 调用方法的call之后

  4. 抛出异常的位置

并行收集器

Parallel Scavenge收集器

配置参数: -XX:+UseParallelGC

目标是达到一个可控制的吞吐量(Throughput)

特点:

  • 吞吐量优先收集器
  • 新生代使用并行回收收集器,采用复制算法
  • 老年代使用串行收集器

什么是吞吐量呢?

吞吐量 = 运行用户代码时间 /(运行用户代码时间+垃圾收集时间)

虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

Parallel Old收集器

配置参数: -XX:+UseParallelOldGC

特点:

  • Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
  • 在注重吞吐量,CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

ParNew收集器

配置参数: -XX:+UseParNewGC

配置参数: -XX:ParallelGCThreads=n 设置并行收集器收集时使用的并行收集线程数。一般最好和计

算机的CPU相当

特点:

  • 新生代并行(ParNew),老年代串行(Serial Old)
  • Serial收集器的多线程版本

注意:单CPU性能并不如Serial,因为存在线程交互的开销

CMS收集器

配置参数: -XX:+UseConcMarkSweepGC 应用CMS收集器。

尽管CMS收集器采用的是并发回收,但是在其初始标记和重新标记这两个阶段中仍然需要执行“STW”暂停程序中的工作线程,不过暂停时间并不会太长,目前所有的垃圾收集器都做不到完全不需要“STW”,只是尽可能地缩短暂停时间。

由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。

特点:

  • 低延迟:减少STW对用户体验的影响【低延迟要求高】
  • 并发收集:可以同时执行用户线程
  • CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收。
  • CMS收集器的垃圾收集算法采用的是标记-清除算法。
  • 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。
  • CMS收集器对CPU资源非常敏感。

CMS垃圾回收器:

CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段:

  • 初始标记(Initial-Mark)阶段:
    • 本阶段任务:标记出GCRoots能直接关联到的对象。
    • 一旦标记完成之后就会恢复之前被暂停的所有应用线程。
    • 由于直接关联对象比较小,所以这里的速度非常快。
    • 会STW
  • 并发标记(Concurrent-Mark)阶段:
    • 本阶段任务:从GC Roots的直接关联对象遍历整个对象图
    • 这个过程耗时较长
    • 不会STW
  • 重新标记(Remark)阶段:
    • 本阶段任务:修正并发标记期间,因用户程序继续运作产生的新的对象记录
    • 这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
    • 会STW
  • 并发清除(Concurrent-Sweep)阶段:
    • 本阶段任务:清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。
    • 由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

G1(Garbage-First)收集器

Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。

  • JDK 8以后G1收集器才被Oracle官方称为“全功能的垃圾收集器”。
  • G1是一款面向服务端应用的垃圾收集器,大内存企业配置的垃圾收集器大多都是G1。
  • JDK 9发布之日G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则被声明为不推荐使用(Deprecate)。

G1最大堆内存是 32MB2048=64G ,G1最小堆内存 1MB2048=2GB ,低于此值建议使用其它收集器。

特点:

  1. 并行与并发:充分利用多核环境下的硬件优势

  2. 多代收集:不需要其他收集器配合就能独立管理整个GC堆

  3. 空间整合:“标记-整理”算法实现的收集器,局部上基于“复制”算法不会产生内存空间碎片

  4. 可预测的停顿:能让使用者明确指定消耗在垃圾收集上的时间。当然,更短的GC时间的代价是回收空间的效率降低。

G1收集器的运作大致可划分为以下几个步骤:

  1. 初始标记:标记一下GC Roots能直接关联到的对象,需要停顿线程,但耗时很短

  2. 并发标记:是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行

  3. 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录

  4. 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划

G1中有三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的条件下被触发。

G1内存划分

G1垃圾收集器相对比其他收集器而言,最大的区别在于它取消了新生代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的新生代、老年代区域。

好处:不用单独的空间对每个代进行设置,不用考虑每个代内存如何分配。

局部采用复制算法:

  • G1新生代垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间
  • G1通过将对象从一个区域复制到另外一个区域,完成了清理工作。 相当于在正常的处理过程中,G1完成了堆的压缩,这样就不会有cms内存碎片问题了。

Humongous区域:在G1中,有一种特殊的区域叫Humongous区域

  • 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。 这些巨型对象,默认直接会被分配在老年代。
  • 但是,如果是一个短期存在的巨型对象,在分区之间来回拷贝,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。

收集器相关参数

-XX:+UseG1GC
# 使用 G1 垃圾收集器
-XX:MaxGCPauseMillis=
# 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是 200 毫秒。
-XX:G1HeapRegionSize=n
# 设置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。
# 目标是根据最小的 Java 堆大小划分出约 2048 个区域。
# 默认是堆内存的1/2000。
-XX:ParallelGCThreads=n
# 设置并行垃圾回收线程数,一般将n的值设置为逻辑处理器的数量,建议最多为8。
-XX:ConcGCThreads=n
# 设置并行标记的线程数。将n设置为ParallelGCThreads的1/4左右。
-XX:InitiatingHeapOccupancyPercent=n
# 设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。

ZGC(Z Garbage Collector)

Z Garbage Collector,也称为ZGC,在 JDK11 中引入的一种可扩展的低延迟垃圾收集器,在 JDK15 中发布稳定版。

ZGC的目标:

  • < 1ms 最大暂停时间(jdk < 16 是 10ms,jdk >=16 是 <1ms )
  • 暂停时间不会随着堆、live-set 或 root-set 的大小而增加
  • 适用内存大小从 8MB 到16TB 的堆

ZGC 具有以下特征:

  • 并发
  • 基于 region
  • 压缩
  • NUMA 感知
  • 使用彩色指针
  • 使用负载屏障

ZGC 收集器是一款基于 Region 内存布局的, 不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。ZGC 的核心是一个并发垃圾收集器,这意味着所有繁重的工作都在Java 线程继续执行的同时完成。这极大地限制了垃圾收集对应用程序响应时间的影响。

相关参数:

-XX:+UseZGC # 启用 ZGC
-Xmx # 设置最大堆内存
-Xlog:gc # 打印 GC日志
-Xlog:gc # 打印 GC 详细日志

总结:

在众多的垃圾回收器中,没有最好的,只有最适合应用的回收器,根据应用软件的特性以及硬件平台的特点,选择不同的垃圾回收器,才能有效的提高系统性能。【小马拉大车、大车驮小马的现象都不合理!】

Minor GC 、Major GC和 Full GC 有什么区别?

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。Minor GC 非常频繁,回收速度比较快。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集, Major GC 一般比 Minor GC慢 10 倍以上。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,需按上下文区分到底是指老年代的收集还是整堆收集。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。

文章作者: Gustavo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Gustavo !
评论
  目录