《深入理解 JVM》

深入理解 JVM —— 内存

 

(4 条消息) 深入理解 JVM 一内存_张硕的博客 - CSDN 博客_jacvajvm 内存结构总结

最近发现有些架构师竟然不懂 JVM,我表示很吃惊,难道他工程师阶段打酱油了?那是不是说,直接去学习架构就行了 ?

接下来的一个礼拜,会把 JVM 核心内容做一个详细的总结。

JVM 内存

其中 Heap、Method Area 是允许线程间共享访问的区域,其余部分只在独立线程独立分配。
在我们常用的 jdk 中使用了 hotspotJVM,HotSpotJVM 中对 Native Method Stack、VM Stack 没有区分,合并在一起,统称为 Stack 理解即可。

program counter register

程序计数器是用来指示当前线程所执行的字节码的行号,JVM 解释器就是通过使用这个计数器来选取下一条字节码指令。对于多线程,每个线程的执行实际上是获取 cpu 时间片(多核处理器使用每个核心并行执行一条指令),通过这个计数器保证下一次线程执行时,可以恢复到上次记录的位置继续执行,因此每个线程都有一个独立的程序计数器。

JIT vs byteCode 解释器:

https://www.ibm.com/developerworks/cn/java/j-lo-just-in-time/

Stack(VM Stack+Native Method Stack)

Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、 操作数栈、 动态链接、 方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。也就是说,一个方法的执行开始与结束,对应这个这个栈帧的入栈与出栈。

局部变量表存放了编译期可知的各种基本数据类型(boolean、 byte、 char、 short、 int、float、 long、 double)、 对象引用(reference 类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和
returnAddress 类型(指向了一条字节码指令的地址)

Heap

所有对象、数组都在堆中创建(jdk7 + 该描述并不绝对)。
对象的结构:


对象头 Mark Word:用于存储对象自身的运行时数据,如哈希码(HashCode)、 GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等, 类型指针, 数组长度
对象实例数据:对象真正存储的有效信息,引用类型、基本类型等等
对齐填充: 因为 java 中数据类型必须是 byte 的整数倍, 通过 “对齐填充” 来保证。也就时这部分作为占位符的作用。
对象的创建:
1. 确保常量池中存放的是已解释的类,如果没有则加载这个类类型
2. 确保对象所属类型已经经过初始化阶段
3. 分配内存:TLAB(Thread Local Allocation Buffer,TLAB) 或者通过 CAS 锁直接在 eden 中分配对象
4. 如果需要,则为对象初始化零值否则 置初始值即可。
5. 设置 对象头 Header
6. 如果有引用,将该对象引用放入栈中。
7. 对象按照程序员的意愿进行初始化(构造器)

可以看到 3,6,7,对象初始化内存后会接着分配引用,这时虽然拿到引用,但是对象初始化未完成。这个也就是 double check(未使用 volatile)会出现问题的原因。

在内存分配有两种方式:
1. 直接移动指针划分需要的内存 (通过 CAS 方式保证其他线程不会并行的使用该内存);
2 预先对每个线程划分一小块可使用的内存,这个线程中的对象初始化时则直接使用这一小块内存,直到预先分配的内存使用完,再去使用 CAS 锁去 Heap 中分配新的内存 (-XX:+/-UseTLAB 设置)。

Heap 是 GC 主要的管理区域,从 GC 的角度来看,Heap 被细分为:youngGen、survivor0Gen、survivor1Gen、oldGen,GC 划分的目的是为了更好地回收管理内存。

从内存分配的角度来看:Heap 可能被划分为多个线程私有的分配缓冲区域(Thread Local Allocation Buffer,TLAB)。TLAB 划分的目的是为了更快的分配内存。

Method Area

也被称为 Non-Heap,用来区分 Heap, 尽管他们都是线程间共享的区域。主要存放 JVM 加载的 class 信息,如类名、类结构等类信息, 访问修饰符、 常量池、 字段描述、 方法描述等 JIT 编译后的代码等等。

因此对于频繁使用 cglib 等动态代理,会产生大量..$class,可能会导致该区域的 OOM(OOM:Pengen), 该区域在 HotSpot 等价于 Pengen.
(http://www.pointsoftware.ch/en/under-the-hood-runtime-data-areas-javas-memory-model/)
(http://stackoverflow.com/questions/19340013/difference-between-class-area-and-heap)

Method Area—Runtime Constant Pool

Runtime Constant Pool 作为 Method Area 的一部分,用于存放编译器生成的各种字面量以及符号引用(字面量和符号引用。字面量如文本字符串,java 中声明为 final 的常量值等等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符。),但是并不仅限于编译期,如 String.intern() 方法就可运行时加载到 RuntimeConstantPool 中。

举例来说:

public static void main(String[] args) {
        String s="cava";
            String str2 = new StringBuilder("ca").append("va").toString();
            System.out.println(str2.intern() == s);
            System.out.println(str2.intern() == new StringBuilder("ca").append("va").toString());

            System.out.println(str2.intern() == "cav"+"a");
    }

String 对象调用 intern 会直接从 Constant Pool 中返回这个字符,如果常量池中没有,则会直接将这个 String 对象复制到常量池中(JDK6), 或者直接将这个 String 的引用保存在常量池中(JDK7)。

而对于直接使用这个字符串,比如 String s=”cava”;,这个字符串会直接从常量池中返回, 如果这个常量池没有 (首次出现),则会把这个常量的引用放入常量池。
注意,对于 HotSpot 虚拟机,根据官方发布的路线图信
息,现在也有放弃永久代并逐步改为采用 Native Memory 来实现方法区的规划了,在目前已
经发布的 JDK 1.7 的 HotSpot 中,已经把原本放在永久代的字符串常量池移出。使用 for loop 死循环调用 String.intern() 并不能导致 Pergen 溢出。

Direct Memory

并不属于 JVM 内存规范中的部分,但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现。NIO 中会使用 DirectByteBuffer 对 DirectMemory 进行操作。

/**
*VM Args:-Xmx20M-XX:MaxDirectMemorySize=10M
*@author zzm
*/
public class DirectMemoryOOM{
private static final int_1MB=1024*1024;
public static void main(String[]args)throws Exception{
Field unsafeField=Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe=(Unsafe)unsafeField.get(null);
while(true){
unsafe.allocateMemory(_1MB);
}}}
Exception in thread"main"java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)

由 DirectMemory 导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显
的异常,如果读者发现 OOM 之后 Dump 文件很小,而程序中又直接或间接使用了 NIO,那就
可以考虑检查一下是不是这方面的原因。

参考资料:

深入理解 JVM

http://java.jr-jr.com/2015/12/02/java-object-size/


深入理解 JVM 一 GC(上)

 

(4 条消息) 深入理解 JVM 一 GC(上)_张硕的博客 - CSDN 博客_深入理解 jvm & g1 gc

对于 GC 我们首先会思考的问题是:
1. 哪些内存要回收?哪些不用?
2. 如何回收?算法
3. 何时回收?触发

GC 回收哪些内存?

在上一篇文章中,详细说了 jvm 内存的模型。
深入理解 JVM 一内存

因为 program counter register、stack(native method stack、VM stack)是随着 jvm 中线程的产生而产生,线程的湮灭而消失。这个几个区域,基本是在运行之前就已确定的,所以 gc 不作用于这部分内存。
gc 主要作用于 Heap、Method Area ,这些区域基本都是运行时才可知,才创建的。这部分内存的分配、回收都是动态的。

GC 如何回收?

很多人会说 GC 回收使用引用计数器算法:就是用一个计数器判断对象是否被引用,被引用一次,计数器 + 1,释放引用 - 1。当计数器为 0,则回收这个对象。但是主流 gc 中都没有使用这个算法,主要是因为这个算法无法解决类之间相互引用的问题。比如:

public class ReferenceCountingGC {
    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    private byte[] bigSize = new byte[2 * _1MB];// 占用空间

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();

        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        // 假设发生GC, objA、objB是否被回收。objA、objB相互引用中。
        System.gc();

    }

    public static void main(String[] args) {
        testGC();
    }
//[GC [PSYoungGen: 6717K->680K(76288K)] 6717K->680K(249856K), 0.0023914 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
//[Full GC [PSYoungGen: 680K->0K(76288K)] [ParOldGen: 0K->587K(173568K)] 680K->587K(249856K) [PSPermGen: 2569K->2568K(21504K)], 0.0316210 secs] [Times: user=0.09 sys=0.00, real=0.03 secs] 
//                Heap
//                 PSYoungGen      total 76288K, used 1966K [0x00000007ab300000, 0x00000007b0800000, 0x0000000800000000)
//                  eden space 65536K, 3% used [0x00000007ab300000,0x00000007ab4eb920,0x00000007af300000)
//                  from space 10752K, 0% used [0x00000007af300000,0x00000007af300000,0x00000007afd80000)
//                  to   space 10752K, 0% used [0x00000007afd80000,0x00000007afd80000,0x00000007b0800000)
//                 ParOldGen       total 173568K, used 587K [0x0000000701a00000, 0x000000070c380000, 0x00000007ab300000)
//                  object space 173568K, 0% used [0x0000000701a00000,0x0000000701a92d40,0x000000070c380000)
//                 PSPermGen       total 21504K, used 2575K [0x00000006fc800000, 0x00000006fdd00000, 0x0000000701a00000)
//                  object space 21504K, 11% used [0x00000006fc800000,0x00000006fca83ca8,0x00000006fdd00000)

对比删除对象实例运行后的 GC 日志,可以确认,上边的代码发生了 GC,两个相互引用的对象,(很明显引用计数器不会为 0),也被回收了。可以看出 jvm 并非使用简单的引用计数器回收算法。

实际上 hotspotJVM 使用了可达性分析算法, 通过一些称为 GC Roots 的对象作为起点,向下扫描,查看是否有个一条路径(称为引用链)到达某个对象,如果可以到达,则这个对象依旧存活(可达)。否则清除不可达的对象。

可以作为 GC Roots 对象有哪些?
GC Roots 最为可达性搜索的起点,首先保证这些对象是要可达的。
static 全局变量、常量;
栈帧中的本地列表;
(JNI)native 方法引用的对象。

http://it.deepinmind.com/gc/2014/05/13/debugging-to-understand-finalizer.html

再谈引用

简单来说,什么是引用:
一个 reference 类型的数据存储的是另一个对象内存地址,那么我们就称这个 reference 是这个对象的引用。
JDK2 之后,引用类型:
** 强引用:** 例如:String strongReference = new String(),这个 strongReference 就是个强引用,只要这个强引用还存在,还指向这个对象,那么这个对象就不会被 GC 回收。
通常我们的引用都是强引用。
软引用: 描述有用,但是非必须的对象。在内存将要溢出时,会对所有的软引用做二次回收,如果依旧没有足够内存,抛出 OOM。

SoftReference<String> softR = new SoftReference<String>(new String("soft Reference"));

弱引用: 更弱的引用,在下一次 GC 时被回收掉。

WeakReference<String> weakReference = new WeakReference(new String("Weak reference"));

虚引用: 对被指向的对象不构成任何影响,也无法通过虚引用获取对象。只是在虚引用指向的对象被回收时,系统会收到一个通知。PhantomReference

finalize() 与 FinalizerQueue

GC in Pergen

对于方法区的 GC,主要说两点:
1. 常量池中废弃的常量被回收;
2. 无用的 class 对象被卸载。对于大量使用反射、动态代理、cglib、bytecode 等技术的框架中,会频繁需要卸载无用的类。

GC 算法

Mark-Sweep(标记清除算法):就是用可达性分析先标记可回收的内存,然后清除掉这些被标记内存的占用。标记——> 清除

缺陷:
1. 效率低下,每次回收都要把整个内存空间从 roots 分析一遍;
2. 产生内存碎片,如果大对象调用内存,无法获取连续的空间,会触发 full-GC;而且要维护一份 free-list,无法通过直接移动指针分配内存。

**Copying **(复制算法):将整个内存分为两块: A,B,JVM 只在 A 上申请内存,当 A 需要清理时,清理 A, 然后把活下来的对象整齐的复制到 B 中,彻底清空 A。

相比于 Mark-Sweep 算法,优势是保证了内存的连续性(A 中每次都会清空,B 中是整齐连续的)。另外适用于 “对象朝生夕死” 的新生代,只需要复制少量的存活对象,然后彻底清空,效率高。

缺点:
1. 只能使用总内存的一部分
2. 需要内存担保(Handler Promotion)
(如上例子中,A 可以使用,B 不能被直接使用),当程序中有过多大对象的使用,比如 A 总共大小为 5M,现在有个对象 10M 怎么够用?这就需要 “担保内存”,当自身不够时,可以直接使用 “担保”(Handle Promotion)去分配内存。

另外,如果 A:B=9:1,那么可以大大提高使用的内存,但是如果清楚 A 时意外发现 100% 的对象都存活,需要复制到 B 中,明显 B 内存不够, 即使够用,复制 100% 的对象效率也是低下的。这时也同样需要 “担保内存”。

其实,在 hotspotJVM 中,A 就是 eden+S0,B 是 S1, 担保内存是 oldGen。当需要将 A 中存活的对象复制到 B 上但 B 不够的时候,会直接使用 oldGen(担保) 去。

mark-sweep-compact(标记整理算法):
标记已经不存活的对象,然后让所有存活的对象移动到内存的一端,然后清理掉不存活的对象空间。标记——> 移动(整理)——> 清除。

解决了内存空间不连续的问题。

分代收集:
就是针对内存的不同区域,使用不同的 GC 算法。比如,youngGen 使用 copying 算法;oldGen 使用 mark-sweep 或者 mark-sweep-compact.


连线的收集器都可以组合使用。

Serial 收集器
-XX:+UseSerialGC
最基本的的收集器,单线程,同时收集时会暂停所有其他线程的执行(stop-the-world)。用于 youngGen。客户端默认使用。

youngGen 中的 Serial 使用了 copying 收集算法。

“Serial” is a stop-the-world, copying collector which uses a single GC thread.

Parallel New 收集器 (-XX:+UseParNewGC、-XX:+UseConcMarkSweepGC 默认的 youngGen 收集器)
仅适用于新生代。
serial 的多线程版本,使用的 gc 算法与 serial 一致。但是还是会有 stop-the-world。

UseParNewGC is “ParNew” + “Serial Old”
UseConcMarkSweepGC is “ParNew” + “CMS” + “Serial Old”. “CMS” is used most of the time to collect the tenured generation. “Serial Old” is used when a concurrent mode failure occurs.

Parallel Scavenge 吞吐量收集器(-XX:UseParallelGC)

“Parallel Scavenge” is a stop-the-world, copying collector which uses multiple GC threads.
parallel scavenge 类似 parallel New, 同样是作用于年轻代,使用 copying 回收算法。但它更关注程序的吞吐量。

吞吐量 vs gc 停顿
http://ifeve.com/useful-jvm-flags-part-6-throughput-collector/
JVM 在专门的线程 (GC threads) 中执行 GC。 只要 GC 线程是活动的,它们将与应用程序线程 (application threads) 争用当前可用 CPU 的时钟周期。 简单点来说,吞吐量是指应用程序线程用时占程序总用时的比例。例如,吞吐量 99/100 意味着 100 秒的程序执行时间应用程序线程运行了 99 秒, 而在这一时间段内 GC 线程只运行了 1 秒。

然而吞吐量的提高总会带来暂停时间的增长:比如我想提高系统吞吐量,那么我就不能让 GC 执行太过频繁,才能减少上下文切换; 但是如果 GC 不够频繁,那么单次 GC 执行的时间肯定会增长
所以,我们要根据实际情况来找准调优目标,如果是后台运算,我们追求吞吐量,让计算能力更强。如果是图像化界面,与用户交互,我们则要缩短暂停时间,以减少卡顿提高体验。

几个调优参数:
-XX:MaxGCPauseMillis=(以毫秒为单位)。 通过设置这个值,让 gc 尽可能的保证 gc 最大的 停顿不要超过这个值,非一定。

通过 - XX:GCTimeRatio = 我们告诉 JVM 吞吐量要达到的目标值。 更准确地说,-XX:GCTimeRatio=N 指定目标应用程序线程的执行时间 (与总的程序执行时间) 达到 N/(N+1)的目标比值。 例如,通过 - XX:GCTimeRatio=8 我们要求应用程序线程在整个执行时间中至少 8/9 是活动的(因此,GC 线程占用其余 1/9)。 基于运行时的测量,JVM 将会尝试修改堆和 GC 设置以期达到目标吞吐量。 -XX:GCTimeRatio 的默认值是 99,也就是说,应用程序线程应该运行至少 99% 的总执行时间。
如果 - XX:GCTimeRatio = 与 - XX:MaxGCPauseMillis = 同时使用,优先达到停顿时间目标。

Serial Old 收集器
Serial 收集器的老年代版本

“Serial Old” is a stop-the-world, mark-sweep-compact collector that uses a single GC thread.

Parallel Old 收集器(-XX:+UseParallelOldGC)
parallel scavenge 的老年代版本,多线程的标记整理算法。
通常在注重吞吐量,以及 cpu 敏感的场合使用 Parallel Scavenge
+Parallel Old 的组合!

CMS(concurrent mark sweep)收集器
–XX:+UseConcMarkSweepGC
cms 收集器是一种以获取最短 GC Pause 为目标的收集器。非常适合重视响应时间的 B/S 系统中。特点是:并发收集(基本与用户线程同时进行)、短停顿。
主要有四个步骤:
1.CMS initial mark 初始标记:
标记 GC Roots 能直接关联到的对象,Stop-the-world 方式。
2.CMS concurrent mark 并发标记:
这里的 "并发" 指,GC 线程与用户线程同时并行执行。concurrent mark 是不使用 stop-the-world 的方式,进行可达性分析,并标记。
3.CMS remark 重新标记:
因为并发标记过程中,用户线程也在执行,存在引用变更,也可能会产生新的垃圾,所以需要对误差重新标记。
4.CMS concurrent Sweep 并发清除。
Concurrent Sweep,不使用 stop-the-world 方式,与用户线程同时运行,执行清理工作,会产生内存碎片。
注意:第四步中,把清理出来的空间地址放入 free list,以便后续使用。另外,存活的对象没有移动。

由于 2,4 两步占整个 CMS 的绝大部分时间,所以,我们认为 cms 的 gc 是与用户线程同时运行的,不存在 stop-the-world pause。

缺点:
1. 对 cpu 资源敏感。它虽然不会导致用户线程卡顿,但是因为在并发标记、并发清理时 (与用户线程同时) 占用 cpu 资源(或者说占用线程),所以会导致用户应用程序变慢(上线文切换导致的),最终吞吐量下降。

在并发标记、并发清理时,默认产生的 gc threads 个数是 (cpu 个数 + 3)/4,比如双核 cpu 启动,会启动 1 个 gc 线程,那么对 cpu 的占用可能会达到 50%;5 核心 cpu,会启动 2 个 gc 线程,对 cpu 的占用可能达到 40%;9 核心 cpu,启动 3 个 gc 线程,对 cpu 的占用可能达到 33%… 随着 cpu 核心数的增加,对 cpu 的负担比例会逐渐下降,但不会低于 25%。

2. 当 CMS 收集器无法处理 “浮动垃圾” 时,会产生 Concurrent Mode Failure 而失败,导致使用 Serial-Old 替代,重新进行 gc 回收。

在第四步,因为 gc 线程与用户线程并行执行,所以在 gc 线程执行清理工作的同时,用户线程的运行极可能产生其他的垃圾,这些垃圾无法在本次 GC 中标记、清理。我们称这些垃圾为浮动垃圾,这些浮动垃圾需要下次清理。

因为有浮动垃圾的存在,所以每次执行 CMS-GC 时,就需要为这些浮动垃圾预留空间,肯定不能等到 old-Gen 满了再清理。目前默认的 CMS 阈值是 92%,老年代被使用 92% 就开始执行清理工作。

3. 因为 CMS 基于标记 - 清理的算法实现的,所以会导致 old-Gen 内存碎片。当内存碎片过多或者 CMS 未完成时 oldGen 已经满了,则会产生 concurrent mode failure 然后切换为 SerialOld(mark-sweep-compact 带整理内存碎片)

The message “concurrent mode failure” signifies that the concurrent
collection of the tenured generation did not finish before the
tenured generation became full. In other words, the new generation is
filling up too fast, it is overflowing to tenured generation but the
CMS could not clear out the tenured generation in the background.

就是年轻代晋升到老年代的对象太多,导致 CMS 未完成之前,老年代已经被占满了。(也就是担保内存不够了,无法存放浮动的垃圾,可以减少 - XX:CMSInitiatingOccupancyFraction 指标)

G1 收集器:
http://www.oracle.com/technetwork/tutorials/tutorials-1876574.html

http://darktea.github.io/notes/2013/09/08/java-gc.html

https://www.dynatrace.com/resources/ebooks/javabook/how-garbage-collection-works/

另外附上一个非常有用的配置, 用来显示所有的 JVM 参数值 (包括没有设置的):

-XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version

详见:
https://www.javaworld.com/article/2073676/hotspot-jvm-options-displayed---xx--printflagsinitial-and--xx--printflagsfinal.html

 

文章目录GC 回收哪些内存?GC 如何回收?再谈引用finalize() 与 FinalizerQueueGC in PergenGC 算法


深入理解 JVM 一 GC(下) 

深入理解 JVM 一 GC(下) G1 Garbage Collector_张硕的博客 - CSDN 博客_garbagecollector

关于 java 程序性能

当我们调优 java 程序时,通常的目标有两个:
响应能力 或者 吞吐量

响应能力

响应能力指一个程序或者系统对请求的是否能够及时响应。
比如:
一个桌面 UI 能多快的响应一个事件;
一个网站能够多快返回一个页面请求;
数据库能够多快返回查询的数据;

对于这类对响应能力敏感的场景,长时间的停顿是无法接受的。

吞吐量

吞吐量关注的是,在一个指定的时间内,最大化一个应用的工作量。
如下方式来衡量一个系统吞吐量的好坏:

在一小时内同一个事务 (或者任务、请求) 完成的次数(tps)。
数据库一小时可以完成多少次查询;

对于关注吞吐量的系统,卡顿是可以接受的,因为这个系统关注长时间的大量任务的执行能力,单次快速的响应并不值得考虑。

应用程序运行实际 / 实际时间 (开始时间戳 - 结束时间戳)

understanding TPS

G1 Garbage Collector

G1 垃圾收集器

g1 收集器是一个面向服务端的垃圾收收集器,适用于多核处理器、大内存容量的服务端系统。
它满足短时间 gc 停顿的同时达到一个高的吞吐量。JDK7 以上版本适用。

g1 收集器的设计目标:

与应用线程同时工作,几乎不需要 stop-the-world(与 CMS 类似);
整理剩余空间, 不产生内存碎片;(CMS 只能在 full-GC 时,用 stop-the-world 整理碎片内存)
GC 停顿更加可控;
不牺牲系统的吞吐量;
gc 不要求额外的内存空间 (CMS 需要预留空间存储浮动垃圾);

G1 的设计规划,是要替换掉 CMS。

G1 在某些方便弥补了 CMS 的不足,比如,CMS 使用的是 mark-sweep 算法,自然会产生内存碎片;然而 G1 基于 copying 算法,高效的整理剩余内存, 而不需要使用 free-list 去管理内存碎片。
另外,G1 提供了更多手段,以达到对 gc 停顿时间可控

之前的 GC 收集器对 Heap 的划分:

G1 对 Heap 的划分:

heap 被划分为一个个相等的不连续的内存区域(regions), 每个 region 都有一个分代的角色: eden、survivor、old(old 还有一种细分 humongous,用来存放大小超过 region 50% 以上的巨型对象)。

但是对每个角色的数量并没有强制的限定,也就是说对每种分代内存的大小,可以动态变化 (默认年轻代占整个 heap 的 5%)

G1 最大的特点就是高效的执行回收,优先去执行那些大量对象可回收的区域(region)

另外,G1 使用了 gc 停顿可预测的模型,来满足用户设定的 gc 停顿时间,根据用户设定的目标时间,g1 会自动的选择哪些 region 要清楚,一次清除多少个 region

G1 从多个 region 中复制存活的对象,然后集中放入一个 region 中,同时整理、清除内存 (copying 收集算法)。

注意对比之前的垃圾收集器 (主要是 CMS):
对比使用 mark-sweep 的 CMS,g1 使用的 copying 算法不会造成内存碎片;
对比 ParallelScavenge(基于 copying)、ParallelOld 收集器 (基于 mark-compact-sweep),Parallel
会对整个区域做整理导致 gc Pause 会比较长,而 g1 只是特定的整理几个 region。

值得注意: g1 不是一个实时的收集器,与 parallelScavenge 一样,对 gc 停顿时间的设置并不绝对生效,只是 g1 有较高的几率保证不超过设定 gc 停顿时间。与之前的 gc 收集器对比,g1 会根据用户设定的 gc 停顿时间,智能评估一下哪几个 region 需要被回收可以满足用户设定。

G1 内存的分配

1.TLAB(TLAB 占用年轻代内存). 默认使用 TLAB 加速内存分配, 之前文章已经讲过,不赘述。
2.Eden. 如果 TLAB 不够用,则在 Eden 中分配内存生成对象。
3.Humongous. 如果对象需要的内存超过一个 region 的 50% 以上,会忽略前两个步骤直接在老年代的 humongous 中分配(连续的 Region)。

何时使用 G1(-XX:+UseG1GC)

1. 大内存中为了达到低 gc 延迟.
比如: heap size >=6G,gc pause <=0.5s
2.FullGC 时间太长,或者太频繁。

调优参数:
-XX:MaxGCPauseMillis=200
用户设定的最大 gc 停顿时间,默认是 200ms.
-XX:InitiatingHeapOccupancyPercent=45
默认是 45,也就是 heap 中 45% 的容量被使用,则会触发 concurrent gc。

G1 垃圾回收步骤详解

G1 提供了两种 GC 模式,Young GC 和 Mixed GC,两种都是 Stop The World(STW) 的

G1 Young GC(STW)

1. 当 eden 数据满了, 则触发 g1 YGC
2. 并行的执行:
YGC 将 eden region 中存活的对象拷贝到 survivor, 或者直接晋升到 Old Region 中;将 Survivor Regin 中存活的对象拷贝到新的 Survivor 或者晋升 old region。
3. 计算下一次 YGC eden、Survivor 的尺寸

G1 Mix GC

在 G1 GC 中,它主要是为 Mixed GC 提供标记服务的,并不是一次 GC 过程的一个必须环节。global concurrent marking 的执行过程分为五个步骤:

初始标记(initial mark,STW)
在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。

根区域扫描(root region scan)
G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。

并发标记(Concurrent Marking)
G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断

最终标记(Remark,STW)
该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。

清除垃圾(Cleanup,STW)
在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。

g1 对老年代回收 - 总结:

1. 并发标记阶段 (Concurrent Marking Phase):
在不产生 stop-the-world,与程序进程并发的情况下,活跃度(可达性分析)被分析出来。
活跃度越低,代表回收的效率越高,越值得优先回收。
2. 复制、清理阶段 (Copying/Cleanup Phase)
年轻代、老年代在这个阶段同时被回收掉。老年代被回收的 region,是根据这个 region 的存活度来选择的。

更多详细信息请点击

 

文章目录关于 java 程序性能响应能力吞吐量G1 Garbage Collectorg1 收集器的设计目标:G1 的设计规划,是要替换掉 CMS。之前的 GC 收集器对 Heap 的划分:G1 对 Heap 的划分:G1 内存的分配何时使用 G1(-XX:+UseG1GC)G1 垃圾回收步骤详解G1 Young GC(STW)G1 Mix GCg1 对老年代回收 - 总结:

原文地址:https://www.cnblogs.com/cx2016/p/13219691.html