关于JVM,你必须知道的这些知识点

一、一些必知参数

堆的分配参数

  • -Xmx:堆内存的最大大小(max)
  • -Xms:堆内存的初始大小(start)
  • -Xmn:新生代大小(new)
  • -XX:NewRatio
    老年代和新生代(eden+2*s)的比值

    例如:4,表示老年代:新生代=4:1,即新生代占整个堆的1/5
  • -XX:SurvivorRatio(Survivor)

    设置新生代eden区与Survivor区的比值

例如:8,表示 eden:s0:s1 = 8:1:1,即一个Survivor占年轻代的1/10

栈的分配参数

栈的分配参数

  • -Xss

设置栈空间的大小,通常只有几百K。决定了函数调用的深度

举例

-Xmx800M –Xms800M –Xmn300M -Xss256K

堆的大小 = 新生代大小 + 老年代大小,上例中最大堆内存为 800M,初始堆内存为 800M,新生代堆内存为 300M,栈空间大小为 256K

二、JVM内存分配机制

JVM对象分配逻辑顺序如下图

JVM对象分配流程

JVM给对象分配内存时,一般都是分配到eden的。但是由于JVM配置的不同,以及一些优化措施,可能会有一些特殊的逻辑,如:栈上分配、TLAB分配、直接进入老年代等。

1. 栈上分配

什么是栈上分配?为什么会有栈上分配?

栈上分配是JVM的一种优化技术,基本思想是对于那些线程私有的对象,可以将它们打散分配在栈上,而不是分配在堆上。
好处:可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统的性能。
技术基础: 逃逸分析标量替换(允许将对象根据属性打散后分配在栈上)

2. TLAB

什么是TLAB?为什么会有TLAB?

TLAB:Thread Local Allocation Buffer(线程本地分配缓存)是线程私有的堆空间
在堆上分配内存时,由于堆是线程共享的,容易发生冲突(尽管可以使用CAS进行同步,但是仍然可能失败重试很多次)。所以可以优化使用线程自己的TLAB来尝试分配对象,这样可以尽可能避免线程同步

3. 老年代的对象来源

  • 很大的对象,新生代放不下
    JVM参数:-XX:PretenureSizeThreshold,默认为3145728,即 3M
  • 长期存活的对象
    JVM通过对象年龄判断哪些对象需要晋升到老年代。
    JVM参数:-XX:MaxTenuringThreshold,默认为 15
  • 符合动态对象年龄判定规则的对象
    JVM并非 “固执” 的要求对象年龄必须达到 MaxTenuringThreshold 才能晋升到老年代,如果:
    Survivor空间相同年龄所有对象的大小总和 > Survivor空间的一半
    则:
    对象年龄 >= 该年龄的对象 就可以 直接晋升到老年代

    这样做,相当于将Survivor空间一半的空间释放出来了,减少了这些对象在Survivor空间间的来回挪腾

三、垃圾回收

1. 垃圾识别算法

  • 引用计数法
  • 可达性分析算法

    • GC Roots对象种类:

      • 虚拟机栈中本地变量表引用的对象
      • 方法区中,类静态变量/常量引用的对象
      • 本地方法栈中JNI(Native方法)引用的对象

如何记忆GC Roots节点种类?
其实可以回忆一下Java运行时数据区。

  1. 程序计数器显然无法作为根节点
  2. 堆是我们要进行垃圾回收的区域

因此就只剩下虚拟机栈、本地方法栈和方法区这三个地方了。
Java运行时数据区
图为Java运行时数据区

引用的类型有哪些?什么时候需要回收?

Java将引用分为四种类型,分别如下(强度递减):

  • 强引用 (Strong Reference)

    • 最常用的引用类型,默认声明的对象为强引用,如:Object obj = new Object();
    • 使用上述的垃圾识别算法(可达性分析算法)进行是否是垃圾、是否要被回收
  • 软引用 (Soft Reference)

    • 有用但非必需的对象,常用作缓存
    • 当在内存不足时,就会回收(抛OutOfMemoryError异常之前)
  • 弱引用(Weak Reference)

    • 非必需的对象
    • 无论内存是否足够,GC线程都会回收
  • 虚引用(Phantom Reference)

    • 任何时候都能被回收,主要用来接收GC的通知

垃圾分代回收类型

  • Minor GC
    发生在新生代的GC,比较频繁,回收速度也较快
  • Major GC/Full GC
    发生在老年代的GC,一般同时会伴随着至少一次的新生代的GC(Minor GC)。回收速度较慢

    分配担保机制是什么?
    当发生在Minor GC之前,JVM会进行检查内存空间是否足够,默认使用老年代的空间作为内存分配的担保。
    但是如果:
        **老年代连续可用空间 < 新生代对象总大小** 且 **老年代连续可用空间 < 历次晋升的连续大小**

    则会先进行Full GC,清理释放老年代空间。
    分配担保机制的意义在于,保证Minor GC后有内存可以分配

2. 垃圾回收算法

  • 标记-清除算法
  • 复制回收算法
  • 标记-整理算法

3. 垃圾收集器

常见垃圾收集器

常见垃圾收集器
有连线代表可以配合使用

常见垃圾收集器工作流程

常见垃圾收集器工作流程

速记

Java各种垃圾收集器比较

  • 新生代

    1. Serial:

      - 使用复制回收算法
      - STW时,单线程进行回收(STW:Stop The World)    
    2. ParNew:

      - 使用复制回收算法
      - STW时,多线程进行回收
    3. Parallel Scavenge(全局):

      - 使用复制回收算法
      - 关注吞吐量,即 CPU的使用效率,而不是单纯的STW时间长短,适合于后台任务型程序
      - 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
      
  • 老年代

    1. CMS:

      - 使用标记-清除算法
      - 重点:减少回收停顿时间
      - 收集过程:
          ① 初始标记(CMS initial mark)
          ② 并发标记(CMS concurrenr mark)
          ③ 重新标记(CMS remark)
          ④ 并发清除(CMS concurrent sweep)
          
          > 其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。
          
          > 由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
    2. Serial Old:

      - 使用标记-整理算法
      - 是CMS备用方案,在Concurrent Mode Failure时使用(CMS垃圾回收时,但老年代空间不足)
    3. Parallel Old:

      - 使用标记-整理算法
      - 与Parallel Scavenge配合使用,关注吞吐量
      
  • G1收集器

    • 将整个Java堆划分为多个大小相等的独立区域(Region),新生代和老年代不再是物理隔离的了
    • 从整体来看是基于“标记-整理”算法,但从局部来看是基于“复制“算法
    • 收集过程:
      ① 初始标记(Initial Marking)
      ② 并发标记(Concurrent Marking)
      ③ 最终标记(Final Marking)
      ④ 筛选回收(Live Data Counting and Evacuation)

4. GC日志怎么看

Minor GC日志
Minor GC log

Full GC日志
Full GC log

5. 监控

JDK自带的监控工具

  • jmap -heap pid 堆使用情况
  • jstat -gcutil pid 1000
  • jstack 线程dump
  • jvisualvm
  • jconsole

官方说明:
https://docs.oracle.com/javase/8/docs/technotes/tools/windows/toc.html