JVM总结

作者: Lcap | 来源:发表于2019-05-04 22:58 被阅读0次

    JVM (Java Virtual Machine),在java生态圈中具有重要地位,不仅支持Java还支持Kotlin、Groovy、Scala、Jython等多种其他语言, 封装各种操作系统和底层硬件的操作为Native代码(大部分为C/C++代码),编程时只需调用Java包中提供的接口即可。从而做到“一次编译,到处运行”(当然得是有JRE环境的机器)。本篇主要总结JVM的内存分配与垃圾回收机制,主要内容如下:

       一、 内存分配回收之历史
       二、 “垃圾”识别算法
       三、 JVM内存模型
       四、 垃圾回收(Garbage Collection)
       五、 JVM调优思路
    

    JVM内存分配与垃圾回收

    一、 内存分配回收之历史

    在最先的编程语言中,大对象的分配是需要人工指定分配(malloc)回收(free)内存的,如C语言的代码片段:

    // Code within C
    int send_request() {
        size_t n = read_size();
        int *elements = malloc(n * sizeof(int));
    
        if(read_elements(n, elements) < n) {
            // elements is not freed!
            return -1;
        }
    
        //  There are a lot of code here!
    
        free(elements)
        return 0;
    }
    

    上述代码,在read_elements(n, elements) < n条件满足时,直接返回-1,并未回收elements指向的内存,从而造成内存泄漏。事实上,若是代码量足够大,且并非一蹴而就,是很容易出现上述疏忽,从而造成内存泄漏。

    为了解决上述问题,C++里引入了智能指针的概念:

    int send_request() {
        size_t n = read_size();
        auto elements = make_shared<vector<int>>();
    
        // read elements
    
        store_in_cache(elements);
    
        // process elements further
    
        return 0;
    }
    

    智能指针采用引用计数(稍后介绍)的方式判断内存是否依然还在使用,即每次引用该内存(本例elements所指的内存),引用计数加一,每个引用离开作用区域引用计数则减一。引用计数为0时,内存被自动回收。
    智能指针相对于原先的手动分配和回收,先进了很多,但其依赖程序员指明调用,仍不够"智能"。

    二、“垃圾”识别算法

    为了让程序员更专注于业务逻辑,能不能将内存分配和回收完全交给第三方机制呢?
    要解决这个问题,首先要区分哪部分内存还在使用,哪部分内存已经没用了(垃圾)。先驱者们提出了用于识别“垃圾”的引用计数可达性分析方法。

    图1-3 引用计数
    • 引用计数:标识每个对象(内存区域)被引用的次数,引用计数为0的对象以及只被其引用的对象,则为垃圾。引用计数简单好用,但是有个循环引用问题,如图1-4所示红色区域的对象,虽理论上为垃圾,但其中每个对象的引用计数均不为0,因而无法被标识。不过不用担心,各个采用引用计数区分垃圾的高级语言,如Python等,使用了如弱引用等方法处理该的问题。(Java并没有采用这种方法,因此不会继续展开说明)

      图1-4 引用计数的循环依赖问题
    • 可达性分析:由GC Root出发,依次扫描并标识引用到的对象,扫描结束后,所有未被标识的对象则为垃圾。Java采用的即使可达性分析方法,可达性分析如图1-5所示。

      图1-5 可达性分析
      GC Root 在不同编程语言中实现不同。在Java中 ,GC Root为:
      - 局部变量(Local variables)
      - 活动线程(Active threads)
      - 静态变量(Static fields)
      - JNI(Java Native Inteface)引用的对象。
      
      三、JVM内存模型

      JVM内存模型见图1-1,其中绿色方框为线程私有,红色部分为线程公用。运行时数据区主要分为:

      1. 程序计数器
      2. 虚拟机栈(VM Stack)
      3. 堆(Heap)
      4. 本地方法栈
      5. 元空间(Metaspace )
    图1-1 JVM内存模型

    1. 程序计数器
    记录当前线程执行的 字节码行号指示器(告诉线程,程序执行到哪了),每个线程独立储存,互不干涉(私有)。
    2. 本地方法栈
    与虚拟机栈的功能相似,只不过专为Native方法服务。
    3. 虚拟机栈
    每个线程私有,每个方法执行时,生成一个栈帧,将局部变量表、操作数栈、动态链接、方法返回地址保存,并且入栈,执行完成后,该栈帧返回并出栈。
    4. 堆
    GC(Garbage Colletion)的主要区域,存放各种对象实例,可分为年轻代(Young)年老代(Tenured/Old),年轻代又分为一个伊甸区(Eden)和两个幸存者区(Survivor),其中Virtual是由JVM控制分配但未使用的内存。堆结构的详细结构如图1-2所示(G1回收器的堆内存分配除外)。

    图1-2 JVM堆
    5. 元空间(Metaspace)
    JDK1.8将之前的方法区(Method Area)移除后,加入了元空间,用以加载的类信息和常量池。元空间并不占用-Xmx设置的虚拟内存,而是占用额外的直接内存,(如:使用-Xmx1G配置了JVM最大内存为1G,最终App可能占用了1.5G内存,而多余部分就是被Metaspace占用了)。

    四、垃圾回收(Garbage Collection)

    垃圾回收器可以分为三大类:

    • Serial GC
    • Parallel GC
    • Mostly Concurrent GC
    1. 单线程(Serial)回收器

    在JVM面世之后,Serial回收器就开始服役,回收机制开始工作时,会暂停JVM中APP的工作线程(Stop The World),开始标记 - 清扫 - 压缩的清理过程。在多核处理器盛行的今天,也许Serial回收器看起来需要淘汰了,但该回收器依然适用于客户端内存回收和内存需求小(100M)的应用。

    2. 并行(Parallel)回收器

    并行回收器,依然会暂停JVM中APP的工作线程,但其利用多核处理器,能够高效的完成回收工作,有着极高的吞吐量表现。一般注重·数据计算以及强调CPU利用率的应用适合使用。
    (吞吐量:若工作CPU时钟周期为T,垃圾回收CPU始终周期为t,吞吐量 = T/(T + t)

    3. 并发(Mostly Concurrent)回收器

    在APP的工作线程运行的同时,占用少量CPU资源并发的回收垃圾。CMS回收器(Concurrent Mark Sweep)和G1回收器(Garbage First)是该类回收器的代表。
    为什么是Mostly Concurrent?因为CMS和G1在回收过程中,依然会有2次短暂STW(Stop The World)。CMS和G1主要适用于交互式低延时应用。
    2次短暂STW: First Mark(标识出GC Root)和 Remark(重新标记并发标记过程中进入引用链的对象)
    CMS在清理Tenured过程中并没有对内存的压缩的操作,而是维护了一张记录空闲内存的表,但这也造成了内存的碎片化,这是CMS一个弊端,我猜测这也是官方后续不推荐使用CMS的一个原因。

    对应的新生代(Young)和年老代(Tenured)的回收器使用组合和命令如下表所示:

    Young Tenured JVM options
    Incremental Incremental -Xincgc
    Serial Serial -XX:+UseSerialGC
    Parallel Scavenge Serial -XX:+UseParallelGC -XX:-UseParallelOldGC
    Parallel New Serial N/A
    Serial Parallel Old N/A
    Parallel Scavenge Parallel Old -XX:+UseParallelGC -XX:+UseParallelOldGC
    Parallel New Parallel Old N/A
    Serial CMS -XX:-UseSerialGC -XX:+UseConcMarkSweepGC
    Parallel Scavenge CMS N/A
    Parallel New CMS -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
    G1 G1 -XX:+UseG1GC

    乍一看很可怕,这怎么记啊!大多数组合要么无法使用,要么不切合实际(多核处理器环境下,已经有一个回收代用了Parallel GC或者CMS,那另一个也就没必要用Serial GC了),因此只用记住粗体字部分就好,总共4条(分别对应本章开始的3类):

    • Serial GC (-XX:+UseSerialGC)
    • Parallel GC (-XX:+UseParallelGC -XX:+UseParallelOldGC)
    • Mostly Concurrent GC
      • CMS (-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)
      • G1 (-XX:+UseG1GC)
      • ZGC (-XX:+UnlockExperimentalVMOptions -XX:+UseZGC):JDK11加入的新GC,适用场景:超大内存(TB级),极低响应延迟(10ms内),STW时间不随内存增加而增大。JDK12中依然属于实验性质,非常强大的GC,拭目以待吧。

    五、 JVM调优思路

    JVM三大重要指标:

    • 延时(Latency)
    • 吞吐量(Throughput)
    • 内存容量(Footprint)
    延时

    针对注重系统响应时间、延迟低的交互式应用,可以考虑使用CMS(JDK1.8以后不推荐使用)或G1(JDK1.9的默认回收器)。

    吞吐量

    针对注重计算以及CPU利用率的应用,可以考虑使用Parallel GC。

    内存容量

    若并不清楚应用所需的内存容量,可以将该配置交给JVM的Ergonomics机制自动配置(将在1/64 ~ 1/4 的系统内存之间调控

    JDK GC的默认配置(新生代:年老代 = 1:3,Eden : Survivor = 8 : 1),针对的是大量朝生暮死对象场景,若是有大量中期或者长期存活的对象,就不适用了。根据使用场景和对象存活时间调整Young和Tenured的大小比例。

    JVM 的GC日志相关配置:

    JVM options Description
    -Xloggc:${LogFilePath} 重定向GC日志至指定地址
    -XX:+PrintGCDetails 打印GC细节
    -XX:+PrintGCDateStamps 打印GC时间戳
    -XX:+UseGCLogFileRotation GC日志循环打印
    -XX:NumberOfGCLogFiles=${Num} 保存GC日志文件数
    -XX:GCLogFileSize=${LogFileSize} GC日志文件最大值

    ----施工中,未完待续
    GC Root
    - Local variables
    - Active threads
    - Static fields
    - JNI references

    - Serial (STW)
    - Parallel (PSYoung-Ergonomics、 Parallel Old) - 吞吐量优先
    - Mostly Concurrent  (ParNew+CMS、G1) - 延时优先
    - Concurrent (ZGC) - 大内存、低延迟
    
    参考资料

    相关文章

      网友评论

          本文标题:JVM总结

          本文链接:https://www.haomeiwen.com/subject/bgmxoqtx.html