对于一个初级Java程序员来说,大多数情况下的确是无需对内存的分配、释放做太多考虑,对Jvm也无需有多么深的理解的。但随着业务发展,技术架构复杂度提升,在写程序的过程中却也往往因为这样而造成了一些不容易察觉到的内存问题,并且在内存问题出现的时候,也不能很快的定位并解决。了解并掌握Java的内存管理模型,垃圾收集机制成了Java程序员不得不面对的问题。

JVM虚拟机内存模型

程序计数器Program Counter Register

每个Java虚拟机线程都有其自己的 pc(程序计数器)寄存器,程序计数器是一块较小的内存区,可以看做是当前线程所执行的字节码的行号指示器,可以存储一个returnAdress或者本机指针。如果线程正在执行一个JAVA方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是NATIVE方法,这个计数器值为空(Undefined),此内存区域是唯一一个在JAVA虚拟机规范中没有规定任何OutOfMemoryError的区域
注:这里有问题是计数器值为空,程序怎么往下执行
参考C++理解是:当线程中调用native方法的时候,则重新启动一个新的线程,那么新的线程的计数器为空则不会影响当前线程的计数器,相互独立。

虚拟机栈VM Stack

Oracle关于栈和栈帧提供了如下描述:

每个JVM线程拥有一个私有的 Java虚拟机栈,创建线程的同时栈也被创建。一个JVM栈由许多帧组成,称之为"栈帧"。JVM中的栈和C等常见语言中的栈比较类似,都用于保存局部变量和部分计算结果,并在方法调用和返回中起作用。因为除了推送和弹出帧外,从不直接操纵Java虚拟机堆栈,所以可以为堆分配帧。Java虚拟机堆栈的内存不必是连续的。
在第一版中的Java ®虚拟机规范,Java虚拟机堆被称为Java堆栈。
该规范允许Java虚拟机堆栈具有固定大小,或根据计算要求动态扩展和收缩。如果Java虚拟机堆栈的大小固定,则在创建每个Java虚拟机堆栈时可以独立选择其大小。
Java虚拟机实现可以为程序员或用户提供对Java虚拟机堆栈初始大小的控制,并且在动态扩展或收缩Java虚拟机堆栈的情况下,可以控制最大和最小大小。

栈帧用于存储数据和部分结果,以及执行动态链接,处理方法返回值,并调度异常。每个帧都有其自己的局部变量数组、操作数栈以及对当前方法类的运行时常量池的引用 。
局部变量数组:局部变量数组的长度在编译时确定,存储了编译期可知的各种基本数据类型(boolean, byte, char, short, int, float, double, long), 对象引用(reference类型,可能是指向对象起始地址的引用指针,也可能是句柄或者其他与对象有关的位置)和returnAddress类型(指向一条字节码指令的地址)
操作数栈:是一个后进先出(LIFO)堆栈,称为其操作数堆栈。帧的操作数堆栈的最大深度是在编译时确定的。和局部变量表一样,操作数栈也是一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作压栈/出栈来访问的。
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。
方法出口信息:方法正常执行完成后返回,或者是执行过程遇到未处理异常,以异常方式退出(无返回值)。一般来说,方法正常退出时,调用者程序计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。出栈时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。
线程请求的栈深度不够会报StackOverflowError
栈动态扩展的容量不够会报OutOfMemoryError

栈的大小通过-Xss来设置,当栈中存储数据比较多时,需要适当调大这个值

本地方法栈Native Stack

本地方法栈类似于虚拟机栈,只不过本地方法栈使用的是本地方法,通常在创建每个线程时为每个线程分配本机方法堆栈。

线程请求的栈深度不够会报StackOverflowError
栈动态扩展的容量不够会报OutOfMemoryError

直接内存

java的NIO库允许java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

直接内存相关的几个JVM参数:

参数名 作  用
-XX:MaxDirectMemorySize=<size> 可直接使用操作系统内存大小,默认值为0不设限制

堆Heap

对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。同时它也是GC所管理的主要区域,因此常被称为GC堆。
根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。
如果计算需要的堆内存多于自动存储管理系统可以提供的,则Java虚拟机将抛出一个OutOfMemoryError

随着时间推移JDK发布版本由1至目前的13,HotSpot的垃圾收集器从Serial发展到CMS再到G1(jdk7_up4商用),以及现在的低延迟垃圾收集器Shenandoah(OpenJdk支持,Oracle官方版本中没有该垃圾收集器)和ZGC(JDK 11中发布)。G1开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。在G1之前的内存布局我们姑且成为经典堆布局,新的垃圾收集器堆结构划分更为复杂,在垃圾收集器章节我们做专门介绍,先看下经典堆的结构:

Heap相关的几个JVM参数:

参数名 作  用
-Xms<size> 设置初始 Java 堆大小
-Xmx<size> 设置最大 Java 堆大小,此值可以设置与-Xms相同,以避免每次垃圾回收完成后JVM重新分配内存.
–XX:NewSize=<size> 设置新生代初始大小
–XX:MaxNewSize=<size> 指定新生代最大大小
-Xmn<size> 设置新生代(Eden)大小,这个参数则是对 -XX:newSize、-XX:MaxnewSize两个参数的同时配置,也就是说如果通过-Xmn来配置新生代的内存大小,那么-XX:NewSize = -XX:MaxNewSize=-Xmn,虽然会很方便,但需要注意的是这个参数是在JDK1.4版本以后才使用的
-XX:SurvivorRatio=<ratio> 配置的是在新生代里面Eden和一个Survivor的比例(默认值 8)同-XX:InitialSurvivorRatio
-XX:NewRatio=<ratio> 是年老代 新生代相对的比例,比如NewRatio=2,表明年老代是新生代的2倍。老年代占了heap的2/3,新生代占了1/3(默认值2)

-XX:TargetSurvivorRatio=<ratio>

设定survivor区的目标使用率,默认50。当新生代相同年龄对象总大小/Survivor大小超过该值,则Survivor区中>=该年龄对象,在下一次YGC时会直接晋升老年代。
-XX:MaxTenuringThreshold=<age> 晋升年龄最大阈值,默认15。在新生代中对象(经过Minor GC的次数)后仍然存活,就会晋升到旧生代。
-XX:+PrintTenuringDistribution 打印每次新生代GC后,survivor区中对象的年龄分布
-XX:MinHeapFreeRatio=<ratio>

是一个百分比,所以范围是0~100。决定GC之后相关的堆的有效内存到底要不要扩大,如果GC之后相关的堆的有效内存空闲的比例比MinHeapFreeRatio这个参数小,那相关的堆的有效内存就可能要扩容一下

与Xminf

-XX:MaxHeapFreeRatio=<ratio>

是一个百分比,所以范围是0~100。决定GC之后相关的堆的有效内存是否有必要进行缩容,如果GC之后相关的堆的有效内存的空闲的比例比MaxHeapFreeRatio这个参数大,那么就对相关的堆的有效内存可能会进行缩容

-XX:MinHeapDeltaBytes=<size> 表示当我们要扩容或者缩容的时候,决定是否要做或者尝试扩容的时候最小扩多少,JDK8下ps gc时默认512k,cms gc默认192k

如果指定NewRatio还可以指定NewSize、MaxNewSize、Xmn,如果同时指定了会如何???
-Xmn -XX:NewSize -XX:NewRatio 都可以设置年轻代的大小,三个参数在一起的时候 -XX:NewRatio=2 无效 ,同时 -XX:NewSize=150m, -Xmn100m 靠后的配置有效
堆内存实际设置比例还是设置固定大小,固定大小理论上速度更高。
新生代(Eden)理论越大越好(值越大,GC次数就越少),但是整个Heap大小是有限的,一般年轻代的设置大小不要超过年老代。

方法区(元空间)

方法区同堆一样,是所有线程共享的内存区域,用于存储类信息,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括用于类和实例初始化以及接口初始化的特殊方法。如static修饰的变量加载类的时候就被加载到方法区中。

在老版jdk,方法区也被称为永久代【因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。jdk8真正开始废弃永久代,而使用元空间(Metaspace)】。移除永久代,官方解释说是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代;实际应用过程中永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen

Java虚拟机规范对方法区比较宽松,除了跟堆一样可以不存在连续的内存空间,定义空间和可扩展空间,还可以选择不实现垃圾收集。

如果无法提供方法区域中的内存来满足分配请求,则Java虚拟机将抛出一个OutOfMemoryError

Metaspace相关的几个JVM参数:

参数名 作  用
–XX:MetaspaceSize=<size> 初始化的Metaspace大小,控制Metaspace发生GC的阈值。GC后,动态增加或者降低MetaspaceSize,默认情况下,这个值大小根据不同的平台在12M到20M之间浮动
–XX:MaxMetaspaceSize=<size> 限制Metaspace增长上限,防止因为某些情况导致Metaspace无限使用本地内存,影响到其他程序,默认为4096M
–XX:MinMetaspaceFreeRatio=<ratio> 当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数,那么虚拟机增长Metaspace的大小,默认为40,即70%
–XX:MaxMetaspaceFreeRatio=<ratio> 当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放部分Metaspace空间,默认为70,即70%
–XX:MaxMetaspaceExpanison=<size> Metaspace增长时的最大幅度,默认值为5M
–XX:MinMetaspaceExpanison=<size> Metaspace增长时的最小幅度,默认为330KB
-XX:CompressedClassSpaceSize=<size> Metaspace中专门来存类元数据的klass部分,默认为1G,当启用参数-XX:+UseCompressedClassPointers时,该size设置才能生效。存储klass部分的数据的第一个Metachunk的大小默认大概是384K
-XX:InitialBootClassLoaderMetaspaceSize=<size> Metaspace中BootClassLoader的存储非klass部分的数据的第一个Metachunk的大小,64位下默认4M,32位下默认2200K
-XX:UseCompressedClassPointers 使用32-bit的offset来代表64-bit进程中的class pointer,值为true或者false,默认值为true

JVM内存分配策略

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决两个问题:给对象分配内存以及回收分配给对象的内存。
对象的分配可能有以下几种方式:
1、JIT编译后被拆散为标量类型并间接地栈上分配
2、对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB(Thread Local Allocation Buffer)上分配
3、少数情况下也会直接分配在老年代 
参考下图:

  

分配的细节取决于当前使用哪种垃圾收集器组合,以及JVM中内存相关参数设置,接下来将会讲解几条最普遍的内存分配规则。

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

先来看看两种GC类型的定义

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(Parallel Scavenge里可设置直接进行Major GC,所以并非绝对),Major GC的速度一般会比Minor GC慢10倍以上。 

大对象直接进入老年代

大对象指的是需要大量连续内存空间的Java对象,比如很大的字符串以及数组。为了避免安置不下大对象提前触发GC,避免Eden和两个Survivor区发生大量的内存复制(复制算法),直接放置于老年代。

通过-XX:PretenureSizeThreshold=xxx(默认值是0,单位是字节,意味着任何对象都会现在新生代分配内存),可以指定大于这个值的对象之机进入老年代。

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

既然虚抑机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪此时象应放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age) 计数器。  如果对象在Eden 出生并经过第一次Minor GC 后仍然存后,并且能被Survivor 容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,对象在Survivor 区中每经过一次MinorGC,  年龄就增加1岁,当它的年龄增加到一定程度(默认为15 岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold 设置。

动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold 才能晋升老年代,如果在Survivor 空间中相同年龄所有对象大小的总和大于Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold 中要求的年龄。

空间分配担保

在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么MinorGC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次MinorGC,尽管这次MinorGC是有风险的;如果小于,或者HandlePromotionFail 设置不允许冒险,那这时也要改为进行一次Full GC。

在发生Minor GC(Yong GC)之前,JVM会计算Survivor区移至老年区的对象的平均大小,虚拟机会检查老年代最大可用的连续空间是否大于需要转移的对象大小。

如果大于,则此次Minor GC(Yong GC)是安全的。

如果小于,jdk1.6之前:则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
                如果大于,则尝试进行一次Minor GC(Yong GC),但这次Minor GC(Yong GC)依然是有风险的,失败后会重新发起一次Major GC(Full GC);
                如果小于或者HandlePromotionFailure=false,则改为直接进行一次Major GC(Full GC)。

但是在jdk1.6 update 24之后-XX:-HandlePromotionFailure 不起作用了,只要老年代的连续空间大于新生代对象的总大小或者历次晋升到老年代的对象的平均大小就进行MinorGC,否则FullGC

JVM虚拟机垃圾回收

随着Jvm运行,线程隔离数据区域所占内存在线程执行完成后即释放,垃圾回收发生在堆内存与元空间以及直接内存。垃圾收集器一般必须完成两件事:哪些内存需要回收;如何回收垃圾。

哪些内存需要回收

垃圾检测方法

引用计数法:给对象添加一个引用计数器,每当有一个地方引用它时计数器就+1,当引用失效时计数器就-1,。只要计数器等于0的对象就是不可能再被使用的。
  此算法在大部分情况下都是一个不错的选择,也有一些著名的应用案例。但是Java虚拟机中是没有使用的。
  优点:实现简单、判断效率高。
  缺点:很难解决对象之间循环引用的问题。
可达性分析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有使用任何引用链时,则说明该对象是不可用的。
  主流的商用程序语言(Java、C#等)在主流的实现中,都是通过可达性分析来判定对象是否存活的。
  通过下图来清晰的感受gc root与对象展示的联系。所示灰色区域对象是存活的,Object5/6/7均是可回收的对象

在Java语言中,可作为GC Roots 的对象包括下面几种

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中静态变量引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈(即一般说的 Native 方法)中JNI引用的对象

 优点:更加精确和严谨,可以分析出循环数据结构相互引用的情况;
 缺点:实现比较复杂、需要分析大量数据,消耗大量时间、分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",是垃圾回收重点关注的问题)。

对象死亡判定

 在jdk1.2之后,Java对引用的概念进行了扩充,总体分为4类:强引用、软引用、弱引用、虚引用,这4中引用强度依次逐渐减弱。

  • 强引用:指在代码中普遍存在的,类似 Object obj = new Object(); 这类的引用,只有强引用还存在,GC就永远不会收集被引用的对象。
  • 软引用:指一些还有用但并非必须的对象。直到内存空间不够时(抛出OutOfMemoryError之前),才会被垃圾回收。采用SoftReference类来实现软引用
  • 弱引用:用来描述非必须对象。当垃圾收集器工作时就会回收掉此类对象。采用WeakReference类来实现弱引用。
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响, 唯一目的就是能在这个对象被回收时收到一个系统通知, 采用PhantomRenference类实现

宣告一个对象死亡,至少要经历两次标记。
  1、第一次标记
  如果对象进行可达性分析算法之后没发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选。
  筛选条件:判断此对象是否有必要执行finalize()方法。
  筛选结果:当对象没有覆盖finalize()方法、或者finalize()方法已经被JVM执行过,则判定为可回收对象。如果对象有必要执行finalize()方法,则被放入F-Queue队列中。稍后在JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发这个方法;  
  2、第二次标记
  GC对F-Queue队列中的对象进行二次标记。
  如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。
  3、finalize() 方法
  finalize()是Object类的一个方法、一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;
  特别说明:并不提倡在程序中调用finalize()来进行自救。建议忘掉Java程序中该方法的存在。因为它执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用)。

回收方法区

  方法去的垃圾收集主要分为两部分内容:废弃的常量和不再使用的类。大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这列频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载能力,以保证不会对方法区造成过大的内存压力。

  • 回收废弃常量

  回收废弃常量与Java堆的回收类似。下面举个栗子说明
  假如一个字符串“java”已经进入常量池中,但当前系统没有一个string对象的值是java的,也就是说,没有任何string对象的引用指向常量池中的“java”常量。如果这时发生内存回收,而且垃圾收集器判定确有必要回收,这个常量“java”将会被清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

  • 回收不再使用的类

  需要同时满足下面3个条件的才能算是无用的类。
该类所有的实例都已经被回收,也就是Java堆中无任何该类及其派生子类的实例。
加载该类的ClassLoader已经被回收。如OSGi、JSP的重加载等,类加载器是精心设计的可替换类加载器。
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
  虚拟机可以对同时满足这三个条件的类进行回收,但不是必须进行回收的。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制。还可以使用-verbose:class以及-XX:TraceClassLoading、-XX:TraceClassUnLoading查看类的加载卸载信息

垃圾收集算法

当前虚拟机的垃圾收集器,大都遵循“分代收集”的理论进行设计,分代收集实质上是一套符合大多数程序实际运行情况的经验法则。因分代收集理论常见垃圾收集器都遵循了同一设计原则:收集器应将Java堆划分出不同的区域,然后根据对象年龄分配到不同区域中存储。区域划分后才有了“PartialGC”(部分收集)、“MinorGC”(新生代收集)、“MajorGC”(老年代收集,只有CMS收集器有单独收集老年代行为)、“MixedGc”(混合收集,收集整个新生代及部分老年代。存在于G1收集器)、“FullGc”(整堆收集)的回收类型划分,针对不同区域安排与里面对象存亡特征相匹配的垃圾收集算法。发展出了标记-清除算法、复制算法、标记-整理算法等针对性垃圾收集算法。

标记-清除算法

是最基础的可达性算法,后续的收集算法都是基于这种思想,对其缺点改进而实现的。
主要缺点有以下两个:

  1. 标记和清除效率不高,执行效率随着对象数量增长而降低;
  2. 产生大量不连续的内存碎片,导致创建大对象时找不到连续的空间,不得不提前触发另一次的垃圾回收。

标记-复制算法

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

优点:实现简单,效率高。解决了标记-清除算法导致的内存碎片问题。
缺点:代价太大,可用内存比实际分配内存要小。效率随对象的存活率升高而降低。

IBM研究表明:新生代中98%的对象都是"朝生夕死"; 所以并不需要按1:1比例来划分内存。对于新生代内存划分实现为分配一块较大的Eden空间和两块较小的Survivor空间。每次使用Eden和其中一块Survivor空间,回收时将Eden和Survivor空间中存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和使用过的Survivor空间。Hotspot虚拟机默认Eden和Survivor的大小比例是8:1。如果另一块Survivor空间没有足够内存来存放上一次新生代收集下来的存活对象,那么这些对象则直接通过分配担保机制进入老年代。

标记-整理算法

标记-整理算法是根据老年代的特点应运而生。和标记-清理不同的是,该算法不是针对可回收对象进行清理,而是根据存活对象进行整理。让存活对象都向一端移动,然后直接清理掉边界以外的内存。

优点:不像标记-清除算法那样产生不连续的内存碎片,因该算法实际应用场景有限,对应用程序吞吐量影响有限。
缺点:除了像标记-清除算法的标记过程外,还多了一步整理过程,移动内存操作需要暂停用户程序才能进行(应用卡顿),效率低下。

垃圾收集器介绍

JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。自从JDK10
就像没有最好的算法一样,垃圾收集器也没有最好,只有最合适。我们能做的就是根据具体的应用场景选择最合适的垃圾收集器。

先看下图解HotSpot虚拟机所包含的收集器:

如图所示七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。

Serial收集器

Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。当然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式下 JVM 的默认选项。从年代的角度,通常将其老年代实现单独称作 Serial Old,它采用了标记 - 整理(Mark-Compact)算法。在JDK1.5及之前,Serial Old与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配Parallel Scavenge收集器);Serial Old作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。Serial收集器适用于CPU资源有限,或者是桌面应用环境(内存不大,可在较短时间内完成垃圾收集)。

Serial/Serial Old收集器运行示意图

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,它是许多运行在Server模式下的虚拟机的首要选择,但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。除了Serial收集器外,目前只有它能与CMS收集器配合工作。ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。

ParNew/Serial Old收集器运行示意图

为什么只有ParNew能与CMS收集器配合
CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;
因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;

Parallel Scavenge收集器(吞吐量优先收集器)

Parallel Scavenge收集器是一个与ParNew相似的新生代收集器,它也是使用复制算法的多线程收集器收集器。它的关注点是吞吐量(如何高效率的利用CPU)。Parallel Old是Parallel Scavenge收集器的老年代版本。支持多线程并发收集,采用"标记-整理"算法实现。这个收集器是直到JDK 6时才开始提供的。适用于程序运行在具有多个CPU上,主要在后台进行计算,而不需要与用户进行太多交互,对暂停时间没有特别高的要求时。Parallel Scavenge没有使用原本HotSpot其它GC通用的那个分代GC框架,所以不能跟使用了那个框架的CMS搭配使用。

Parallel Scavenge/Parallel Old收集器运行示意图

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的老年代收集器,关注点是尽可能地缩短垃圾收集时用户线程的停顿时间。CMS是第一款真正意义上的并发(Concurrent)收集器,实现了让垃圾收集线程与用户线程(基本上)同时工作。适用于与用户交互较多的场景(如常见WEB、B/S-浏览器/服务器模式系统的服务器上的应用)。CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为六个步骤:

  • 初始标记(STW initial mark) :在这个阶段,需要虚拟机停顿正在执行的任务。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
  • 并发标记(Concurrent marking) :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记(增量更新)。并发标记阶段,会遍历整个年老代并且标记活着的对象,如果已经被遍历过的对象的引用被用户线程改变(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代),JVM就会标记这个区域为Dirty Card。
  • 并发预清理(Concurrent precleaning) :并发预清理阶段仍然是并发的。在这个阶段,会把并发标记阶段被标记为Dirty Card的对象以及可达的对象重新遍历标记,完成后清楚Dirty Card标记。这个阶段是重复的做相同的事情直到发生aboart的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。减少下一个阶段"重新标记"的工作,因为下一个阶段会产生STW。
  • 重新标记 (STW remark):这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"跟对象"开始向下追溯,并处理对象关联。
  • 并发清理 (Concurrent sweep):清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
  • 并发重置 (Concurrent reset):这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。

CMS收集器运行示意图
GC日志示例:

-- 初始标记
[GC (CMS Initial Mark) [1 CMS-initial-mark: 72805K(87424K)] 77203K(126720K), 0.0011783 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
-- 并发标记
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.054/0.054 secs] [Times: user=0.08 sys=0.00, real=0.05 secs]
-- 并发预清理
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-abortable-preclean-start]
[CMS-concurrent-abortable-preclean: 0.080/0.579 secs] [Times: user=0.28 sys=0.05, real=0.58 secs]
-- 重新标记
[GC (CMS Final Remark) [YG occupancy: 27424 K (39296 K)]
[Rescan (parallel) , 0.0042132 secs]
[weak refs processing, 0.0000628 secs]
[class unloading, 0.0057412 secs]
[scrub symbol table, 0.0059423 secs]
[scrub string table, 0.0005362 secs]
[1 CMS-remark: 72805K(87424K)] 100230K(126720K), 0.0170742 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
-- 并发清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.031/0.031 secs] [Times: user=0.05 sys=0.00, real=0.03 secs]
-- 并发重置
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

G1收集器

G1 (Garbage-First)是一款面向服务器的垃圾收集器,是一种兼顾吞吐量和停顿时间的 GC 实现,使用Mixed GC(面向整个堆内存,不区分分代信息)模式,是 Oracle JDK 9 以后的默认 GC 选项。G1的内存结构不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片。G1 通过建立可预测停顿模型,设定最大停顿时间,在低停顿的同时实现高吞吐量。G1垃圾收集整个过程有三次停顿分为四个步骤:

  • 初始标记(Initial Marking)STW
  • 并发标记(Concurrent Marking使用原始快照(SATB)实现并发)
  • 最终标记(Final Marking更新SATB记录)STW
  • 筛选回收(Live Data Counting and Evacuation)涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

G1收集器运行示意图

Shenandoah收集器

Shenandoah是一款只有OpenJDK才会包含的低延迟垃圾收集器,整个垃圾收集过程有4次停顿分为九个步骤:

  • 初始标记(Initial Marking)这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关。
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)会有一小段短暂的停顿。
  • 并发清理(Concurrent Cleanup)
  • 并发回收(Concurrent Evacuation通过读屏障和转发指针解决对象移动后内存地址变化导致的读取问题)
  • 初始引用更新(Initial Update Reference确保对象移动完成,建立一个线程集合点,会产生短暂停顿)
  • 并发引用更新(Concurrent Update Reference与用户线程一起并发更新引用)
  • 最终引用更新(Final Update Reference修正GCRoots中的引用,停顿时间与GCRoots数量有关)
  • 并发清理(Concurrent Cleanup回收无存活对象的Region内存空间)

Shenandoah收集器的工作过程

ZGC收集器

ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。ZGC的设计理念与Azul System公司的PGC和C4收集器一脉相承,是由Oracle公司研发的,在JDK 11中新加入的具有实验性质的低延迟垃圾收集器。ZGC目前仅适用于Linux / x64,最大管理内存不超过4T(后续有可能扩展)ZGC的Region具有动态性——动态创建和销毁,以及动态的区域容量大小,具有大中小三类容量:

  • 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
  • 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
  • 大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。

ZGC的堆内存布局

ZGC的运作过程大致可划分为以下四个大的阶段。

  • 并发标记(Concurrent Mark)
  • 并发预备重分配(Concurrent Prepare for Relocate)
  • 并发重分配(Concurrent Relocate)ZGC执行过程中的核心阶段,得益于染色指针,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象。
  • 并发重映射(Concurrent Remap)修正整个堆中指向重分配集中旧对象的所有引用。该工作不是迫切需要的,因为即使是旧引用,它也是可以自愈的,最多只是第一次使用时多一次转发和修正操作。重映射清理这些旧引用的主要目的是为了不变慢。ZGC把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,节省了一次遍历对象图的开销。

全部四个阶段都是可以并发执行的,仅是两个阶段中间会存在短暂的停顿小阶段,

ZGC运作过程

ZGC的强项停顿时间测试上,它就毫不留情地与Parallel Scavenge、G1拉开了两个数量级的差距。不论是平均停顿,还是95%停顿、99%停顿、99.9%停顿,抑或是最大停顿时间,ZGC均能毫不费劲地控制在十毫秒之内。

ZGC的停顿时间测试

虚拟机及垃圾收集器日志

垃圾收集器日志在JDK9之前没有任何的“业界标准”可言,每个收集器的日志格式都可能不一样,HotSpot并没有提供统一的日志处理框架,虚拟机各个功能模块的日志开关分布在不同的参数上,日志级别、循环日志大小、输出格式、重定向等设置在不同功能上都要单独解决。直到JDK 9,HotSpot所有功能的日志都收归到了“-Xlog”参数上,这个参数的能力也相应被极大拓展了:

-Xlog[:[selector][:[output][:[decorators][:output-options]]]]

命令行中最关键的参数是选择器(Selector),它由标签(Tag)和日志级别(Level)共同组成。可以使用修饰器(Decorator)来要求每行日志输出都附加上额外的内容,支持附加在日志行上的信息包括:·

  • time:当前日期和时间。
  • uptime:虚拟机启动到现在经过的时间,以秒为单位。
  • timemillis:当前时间的毫秒数,相当于System.currentTimeMillis()的输出。
  • uptimemillis:虚拟机启动到现在经过的毫秒数。
  • timenanos:当前时间的纳秒数,相当于System.nanoTime()的输出。
  • uptimenanos:虚拟机启动到现在经过的纳秒数。
  • pid:进程ID。
  • tid:线程ID。
  • level:日志级别。
  • tags:日志输出的标签集。

如果不指定,默认值是uptime、level、tags这三个。

全部支持的功能模块标签名如下所示:
add,age,alloc,annotation,aot,arguments,attach,barrier,biasedlocking,blocks,bot,breakpoint,bytecode,census,class,classhisto,cleanup,compaction,comparator,constraints,constantpool,coops,cpu,cset,data,defaultmethods,dump,ergo,event,exceptions,exit,fingerprint,freelist,gc,hashtables,heap,humongous,ihop,iklass,init,itables,jfr,jni,jvmti,liveness,load,loader,logging,mark,marking,metadata,metaspace,method,mmu,modules,monitorinflation,monitormismatch,nmethod,normalize,objecttagging,obsolete,oopmap,os,pagesize,parser,patch,path,phases,plab,preorder,promotion,protectiondomain,purge,redefine,ref,refine,region,remset,resolve,safepoint,scavenge,scrub,setting,stackmap,stacktrace,stackwalk,start,startuptime,state,stats,stringdedup,stringtable,subclass,survivor,sweep,system,task,thread,time,timer,tlab,unload,update,verification,verify,vmoperation,vtables,workgang
日志级别从低到高六种级别:
Trace,Debug,Info,Warning,Error,Off

1)查看GC基本信息,在JDK 9之前使用-XX:+PrintGC,JDK 9后使用-Xlog:gc

2)查看GC详细信息,在JDK 9之前使用-XX:+PrintGCDetails,在JDK 9之后使用-Xlog:gc*

3)查看GC前后的堆、方法区可用容量变化,在JDK 9之前使用-XX:+PrintHeapAtGC,JDK 9之后使用-Xlog:gc+heap=debug

4)查看GC过程中用户线程并发时间以及停顿的时间,在JDK 9之前使用-XX:+Print-GCApplicationConcurrentTime以及-XX:+PrintGCApplicationStoppedTime,JDK 9之后使用-Xlog:safepoint

5)查看收集器Ergonomics机制(自动设置堆空间各分代区域大小、收集目标等内容,从Parallel收集器开始支持)自动调节的相关信息。在JDK 9之前使用-XX:+PrintAdaptive-SizePolicy,JDK 9之后使用-Xlog:gc+ergo*=trace

6)查看熬过收集后剩余对象的年龄分布信息,在JDK 9前使用-XX:+PrintTenuring-Distribution,JDK 9之后使用-Xlog:gc+age=trace

JDK 9前后日志参数变化

垃圾收集器参数总结

参数 描述
UseSerialGC 虚拟机运行在Client 模式下的默认值,打开此开关后,使用Serial + Serial Old 的收集器组合进行内存回收
UseParNewGC 打开此开关后,使用ParNew + Serial Old 的收集器组合进行内存回收
UseConcMarkSweepGC 打开此开关后,使用ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为CMS 收集器出现Concurrent Mode Failure失败后的后备收集器使用
UseParallelGC 虚拟机运行在Server 模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收
UseParallelOldGC 打开此开关后,使用Parallel Scavenge + Parallel Old 的收集器组合进行内存回收
SurvivorRatio 新生代中Eden 区域与Survivor 区域的容量比值, 默认为8, 代表Eden :Survivor=8∶1
PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenuringThreshold 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC 之后,年龄就加1,当超过这个参数值时就进入老年代
UseAdaptiveSizePolicy 动态调整Java 堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden 和Survivor 区的所有对象都存活的极端情况
ParallelGCThreads 设置并行GC 时进行内存回收的线程数
GCTimeRatio GC 时间占总时间的比率,默认值为99,即允许1% 的GC 时间。仅在使用Parallel Scavenge 收集器时生效
MaxGCPauseMillis 设置GC 的最大停顿时间。仅在使用Parallel Scavenge 收集器时生效
CMSInitiatingOccupancyFraction 设置CMS 收集器在老年代空间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS 收集器时生效
UseCMSCompactAtFullCollection 设置CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理默认开启。仅在使用CMS 收集器时生效(JDK9之后废弃)
CMSFullGCsBeforeCompaction 设置CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理,默认值为0,表示每次进入Full GC时都进行碎片整理。仅在使用CMS 收集器时生效(JDK9之后废弃)
UseG1GC 使用G1收集器,这个是JDK9后的Server模式默认值
G1HeapRegionSize=n 设置Region大小,并非最终值
MaxGCPauseMillis 设置G1收集过程目标时间,默认值是200ms,不是硬性条件
G1NewSizePercent 新生代最小值,默认值是5%
G1MaxNewSizePercent 新生代最大值,默认值是60%
ParallelGCThreas 用户线程冻结期间并行执行的收集器线程数
ConcGCThreads=n 并发标记、并发整理的执行线程数,对不同的收集器,根据其能够并发的阶段,有不同的含义
InitiatingHeapOccupancyPercent 设置触犯标记周期的Java堆占用率阈值。默认值是45%。这里的Java堆占比指的是non_young_capaticity_bytes,包括old+humongous
UseShenandoahGC 使用Shenandoah收集器。这个选项在OracleJDK中不被支持,只能在OpenJDK12或者某些支持Shenandoah的Backport发行版本使用。目前仍然要配合-XX:+UnlockExperimentalVMOptions使用
ShenandoahGCHeuristics Shenandoah何时启动一次GC过程,其可选值有adaptive、static、compact、passive、aggressive
UseZGC 使用ZGC收集器,目前仍然要配合-XX:+UnlockExperimentalVMOptions使用
UseNUMA 启用NUMA内存分配支持,目前只有Parallel和ZGC支持,以后G1收集器可能也会支持该选项

参考资料

《深入理解Java虚拟机:JVM高级特性与最佳实践》第三版

虚拟机规范

https://docs.oracle.com/javase/specs/index.html

JVM内存模型与GC相关推荐

  1. JVM内存模型与GC回收器

    1.JVM内存模型 JVM内存模型如上图,需要声明一点,这是<Java虚拟机规范(Java SE 7版)>规定的内容,实际区域由各JVM自己实现,所以可能略有不同.以下对各区域进行简短说明 ...

  2. jvm对象从新生代到老年代_深入理解jvm内存模型以及gc原理

    整体架构 Jvm = 类加载器 + 执行引擎 + 运行时数据区域 类加载器 ● 作用 类加载器是将编译好的class文件加载到内存中,并进行验证.初始化等步骤,形成能被jvm直接使用的类型. ● 加载 ...

  3. Java 内存模型及GC原理

    一个优秀Java程序员,必须了解Java内存模型.GC工作原理,以及如何优化GC的性能.与GC进行有限的交互,有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率,才能 ...

  4. 【转】Java 内存模型及GC原理

    一个优秀Java程序员,必须了解Java内存模型.GC工作原理,以及如何优化GC的性能.与GC进行有限的交互,有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率,才能 ...

  5. Java内存模型 gc算法_JVM内存模型及GC回收算法

    该篇博客主要对JVM内存模型以及GC回收算法以自己的理解和认识做以记录. 内存模型 GC垃圾回收 1.内存模型 从上图可以看出,JVM分为 方法区,虚拟机栈,本地方法栈,堆,计数器 5个区域.其中最为 ...

  6. JVM内存模型与垃圾回收GC

    Java开发有个很基础的问题,虽然我们平时接触的不多,但是了解它却成为Java开发的必备基础--这就是JVM.在C++中我们需要手动申请内存然后释放内存,否则就会出现对象已经不再使用内存却仍被占用的情 ...

  7. JVM(Java虚拟机)详解(JVM 内存模型、堆、GC、直接内存、性能调优)

    JVM(Java虚拟机) JVM 内存模型 结构图 jdk1.8 结构图(极简) jdk1.8 结构图(简单) JVM(Java虚拟机): 是一个抽象的计算模型. 如同一台真实的机器,它有自己的指令集 ...

  8. JDK5.0中JVM堆模型、GC垃圾收集详细解析 .

    前段时间在一个项目的性能测试中又发生了一次OOM(Out of swap sapce),情形和以前网店版的那次差不多,比上次更奇怪的是,此次搞了几天之后啥都没调整系统就自动好了,死活没法再重现之前的O ...

  9. Java JVM内存模型

    简述JVM内存模型 线程私有的运行时数据区: 程序计数器.Java 虚拟机栈.本地方法栈. 线程共享的运行时数据区:Java 堆.方法区. 简述程序计数器 程序计数器表示当前线程所执行的字节码的行号指 ...

最新文章

  1. 读书:历史 -- 奥斯曼帝国六百年
  2. linux命令冒号加叹号,Linux中的叹号命令
  3. Mysql数据类型之字符串的案例介绍(含latin1下varchar的最大长度是65532还是65533)
  4. 机器学习中qa测试_如何对机器学习做单元测试
  5. 美团都在用的实时应用监控平台,到底有多好用?
  6. 文件管理浏览器组件支持doc、excel、ppt、txt格式文件的预览和编辑,支持ofd、pdf文件的预览。
  7. UniWebView3.8
  8. 【FPGA目标跟踪】基于FPGA的帧差法和SAD匹配算法的目标跟踪实现
  9. Eterm连接不上-10001:登录失败
  10. 【云速建站】网站的基本设置
  11. ASP.NET DATETIME
  12. 「Thymeleaf页面在浏览器加载不出来」
  13. stm32F103+EncEthernet+ENC28J60驱动+ping
  14. 网络分流器-TCP报文重组和会话规则-网络分流器
  15. Typhon爬取图片
  16. 零基础能不能学习web前端开发?【爱创课堂专业前端培训】
  17. DM36x 接入 AR0130 sensor
  18. 【多目标进化优化】MOPSO 原理与代码实现
  19. QWT坐标刻度设置时的2个细节
  20. Altium Designer 21的使用(四):排针类元件模型的创建

热门文章

  1. 360 度评估中的提问示范
  2. 常见21种漏洞编码安全规范及解决方案
  3. Ajax技术 实例篇
  4. SUBMAIL短信平台API接口-Message/xsend
  5. mysql存储过程list参数_mysql存储过程教程(1)
  6. 深入jvm 讲得比较清楚
  7. linux tomcat定时重启服务
  8. 13.用两个栈实现队列
  9. ios助手开发系列(三):打开设备连接,获取设备基本信息
  10. 收藏!27 个为什么,帮助你更好的理解Python