[关闭]
@songhanshi 2020-11-16T17:20:03.000000Z 字数 34194 阅读 732

JVM

Java学习


笔记资料来源:

  1. Java虚拟机3
  2. 《实战JAVA虚拟机》
  3. 极客-《Java性能调优实战》-刘超

是什么 what
为什么 why
如何使用 how
使用出现的问题 issue
对问题的量化 evaluate
造成问题的因素 why
如何优化 optimize
版本迭代 iterate

第二部分

即时编译器JIT

  1. 运行时编译
    说到编译,我猜你一定会想到 .java 文件被编译成 .class文件的过程,这个编译我们一般称为前端编译。Java的编译和运行过程非常复杂,除了前端编译,还有运行时编译。由于机器无法直接运行 Java生成的字节码,所以在运行时,JIT 或解释器会将字节码转换成机器码,这个过程就叫运行时编译。

  2. 类编译加载执行过程

面试题 (sxt2)

调参数

  1. 你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值 (sxt2)

    • JVM参数类型(3种)

      • 标配参数
        1、-version、 -help、 java -showversion
      • x参数(了解)
        -Xint 解释执行
        -Xcomp 第一次使用就编译成本地代码
        -Xmixed 混合模式
      • xx参数 ★
        1、Boolean类
        1)公式:-XX:+或者-某个属性值;+表示开启,-表示关闭
        2)case:
        a. 是否打印GC的收集细节
    • 盘点家底查看JVM默认值

  2. 你平时工作用过的JVM常用基本配置参数有哪些 (sxt2)

  3. 强引用、软饮用、弱引用、虚引用分别是什么 (sxt2)

  4. 谈谈你对OOM的认识 (sxt2)

  5. GC垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈 (sxt2)

  6. 怎么查看服务器默认的垃圾回收器是哪个 (sxt2)
    生产上如何配置垃圾回收器的
    谈谈你对垃圾回收器的理解

  7. G1 垃圾回收器 (sxt2)

  8. 生产环境服务器变慢,诊断思路和性能评估谈谈 (sxt2)

  9. 假如生产环境出现CPU占用过高,谈谈你的分析思路和定位 (sxt2)

  10. 对于JDK自带的JVM监控和性能分析工具用过哪些?一般你是怎么用的? (sxt2)

第一部分

一、走近Java

1 走近Java

1.1 Java技术体

  1. JCP官方定义Java技术体系组成:
    · Java程序设计语言
    · 各种硬件平台上的Java虚拟机实现
    · Class文件格式
    · Java类库API
    · 来自商业机构和开源社区的第三方Java类库

1.2 JDK、JRE

在这里插入图片描述

  1. 一些概念?

    • Java ME(Micro Edition):支持Java程序运行在移动终端(手机、PDA)上的平台,对Java API 有所精简,并加入了移动终端的针对性支持。
    • Java SE(Standard Edition):支持面向桌面级应用(如Windows下的应用程序)的Java平台,提供了完整的Java核心API。
    • Java EE(Enterprise Edition):支持使用多层架构的企业应用(如ERP、MIS、CRM应用)的 Java平台,
  2. JDK(Java Development Kit)?

    • Java程序设计语言、Java虚拟机、Java类库这三部分统称为JDK
    • JDK 常来代指整个Java技术体系。
    • JDK是用于支持Java程序开发的最小环境。
  3. JRE(Java Runtime Environment)?

    • Java类库API中的Java SE API子集和Java虚拟机这两部分统称为JRE
    • JRE是支持Java程序运行的标准环境。

1.2 JVM是什么

  1. Java虚拟机是什么

    • 基本概念
      虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行
  2. Java虚拟机是什么,操作系统层面(进程角度)

    • 现象:

      1. // C语言运行程序
      2. // 编译:
      3. gcc HelloWorld.c -o HelloWorld
      4. // 运行C:
      5. zhangjg@linux:/deve/workspace/HelloWorld/src$ ./HelloWorld
      6. hello world
      7. // Java程序运行
      8. // 编译:
      9. zhangjg@linux:/deve/workspace/HelloJava/src$ javac HelloWorld.java
      10. zhangjg@linux:/deve/workspace/HelloJava/src$ ls
      11. HelloWorld.class HelloWorld.java
      12. // 运行:
      13. zhangjg@linux:/deve/workspace/HelloJava/src$ java -classpath . HelloWorld
      14. HelloWorld
    • 问题:
      Java版运行时执行的不是 ./HelloWorld.class class文件并不是可以直接被操作系统识别的二进制可执行文件 。
      而“java”这个命令说明首先启动的是一个叫做java的程序,这个java程序在运行起来之后就是一个JVM进程实例。

    • 流程
      -- java命令首先启动虚拟机进程,虚拟机进程成功启动后,读取参数“HelloWorld”,把他作为初始类加载到内存,对这个类进行初始化和动态链接(关于类的初始化和动态链接会在后面的博客中介绍),然后从这个类的main方法开始执行。也就是说我们的.class文件不是直接被系统加载后直接在cpu上执行的,而是被一个叫做虚拟机的进程托管的。首先必须虚拟机进程启动就绪,然后由虚拟机中的类加载器加载必要的class文件,包括jdk中的基础类(如String和Object等),然后由虚拟机进程解释class字节码指令,把这些字节码指令翻译成本机cpu能够识别的指令,才能在cpu上运行。
      -- 从这个层面上来看,在执行一个所谓的java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程,而不是我们写的一个个的class文件。这个叫做虚拟机的进程处理一些底层的操作,比如内存的分配和释放等等。我们编写的class文件只是虚拟机进程执行时需要的“原料”。这些“原料”在运行时被加载到虚拟机中,被虚拟机解释执行,以控制虚拟机实现我们java代码中所定义的一些相对高层的操作,比如创建一个文件等,可以将class文件中的信息看做对虚拟机的控制信息,也就是一种虚拟指令
    • 一个Java虚拟机实例在运行过程中有三个子系统来保障它的正常运行,分别是类加载器子系统, 执行引擎子系统和垃圾收集子系统。
  3. 参考
    https://blog.csdn.net/zhangjg_blog/article/details/20380971

  4. 主流:HotSpot VM

    • Sun/OracleJDK和OpenJDK中的默认Java虚拟机,也是目前使用范围最广的Java虚拟机。
    • HotSpot虚拟机中含有两个即时编译器
      -- 编译耗时短但输出代码优化程度较低的客户端编译器(简称为C1)
      -- 编译耗时长但输出代码优化质量也更高的服务端编译器(简称为C2)
      -- 在分层编译机制下与解释器互相配合来共同构成HotSpot虚拟机的执行子系统
  5. 提前编译(Ahead of Time Compilation,AOT)

    • 提前编译是相对于即时编译的概念,提前编译能带来的最大好处是Java虚拟机加载这些已经预编译成二进制库之后就能够直接调用,而无须再等待即时编译器在运行时将其编译成二进制机器码。
    • 缺点:
      -- 破坏了Java“一次编写,到处运行”的承诺,必须为每个不同的硬件、操作系统去编译对应的发行包;
      -- 也显著降低了Java链接过程的动态性,必须要求加载的代码在编译期就是全部已知的,而不能在运行期才确定,否则就只能舍弃掉已经提前编译好的版本,退回到 原来的即时编译执行状态。
    • Substrate VM-提前编译

二、自动内存管理

2.Java内存区域与内存溢出异常

2.1 Java虚拟机内存的各个区域

(jvm3:介绍完Java虚拟机的运行时数据区域之后,我们大致明白了Java虚拟机内存模型的概况。)

1. 运行时数据区域
  1. 运行时数据区域?
    Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。(运行时数据区域,强调对内存空间的划分

  2. 程序计数器?

    • 空间较小
    • 线程私有,生命周期与线程相同;
    • 当前线程所执行的字节码的行号指示器;
    • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是程序控制流的指示器;
    • 辅助完成分支、循环、跳转、异常处 理、线程恢复等基础功能;
    • 线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器;
    • 线程Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,
      执行的是本地(Native)方法,这个计数器值则应为空(Undefined)
    • 唯一无OutOfMemoryError情况的
  3. Java虚拟机栈?

    • 线程私有,生命周期与线程相同,同程序计数器
    • 作用:
      描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储数据。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    • 存储:
      栈帧(Stack Frame)存储:局部变量表、操作数栈、动态连接、方法出口等信息
      局部变量表存储:
      ① 编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、
      ② 对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)
      ③ returnAddress类型(指向了一条字节码指令的地址)
      局部变量表存储空间:
      局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个
    • 异常:
      StackOverflowError异常:如果线程请求的栈深度大于虚拟机所允许的深度
      OutOfMemoryError异常:如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存时
  4. 本地方法栈

    • 与虚拟机栈作用相似,
    • 与虚拟机栈区别:
      只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
    • 异常:
      与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
  5. Java堆(Java Heap)

    • 虚拟机所管理的内存中空间最大的,所有线程共享的一块内存区域,在虚拟机启动时创建;
    • 存储:对象实例(几乎所有;
    • 垃圾收集器管理的内存区域;
    • 分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区 (TLAB),以提升对象分配时的效率;
    • Java堆可被实现成固定大小的或可扩展的,当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定);
    • 划分目的:
      不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存。
    • 异常:
      OutOfMemoryError异常:如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
  6. 方法区(Method Area)

    • 堆的一个逻辑部分
    • 存储:已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。(JDK7起,原本存放在永久代的字符串常量池被移至Java堆之中
    • 永久代概念:用永久代来实现方法区
    • JDK6,逐步改为采用本地内存(Native Memory)来实现方法区
    • JDK8,完全废弃永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta- space)来代替。
    • OutOfMemoryError异常:方法区无法满足新的内存分配需求时
  7. 方法区-运行时常量池(Runtime Constant Pool)

    • 方法区的一部分
    • 存储:编译期生成的各种字面量与符号引用、由符号引用翻译出来的直接引用
      -- Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
      -- 除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中
    • 异常:
      和方法区一样受方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
  8. 直接内存(Direct Memory)

    • 非虚拟机运行时数据区的一部分以及非内存区域
    • 放这里的原因:这部分内存也被频繁地使用,也可能导致OutOfMemoryError异常
    • OutOfMemoryError异常:各个内存区域总和大于物理内存限制,

2.2 HotSpot虚拟机对象

1. 对象的创建
  1. 语言层:创建对象

    • 语言层面上,创建对象通常(例外:复制、反序列化)仅仅是一个new关键字。
  2. 虚拟机层:创建对象

    • 虚拟机中,对象(此处对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样一个过程呢?
  3. 创建过程

    • Java虚拟机遇到一条字节码new指令
    • 首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程;
    • 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
      对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。
      -- 假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer)。
      -- 但如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称 为“空闲列表”(Free List)。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。
      -- 因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。
    • 内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
    • 接下来,Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
    • 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始——构造函数,即Class文件中的()方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说(由字节码流中new指令后面是否跟随invokespecial指令所决定,Java编译器会在遇到new关键字的地方同时生成这两条字节码指令,但如果直接通过其他方式产生的则不一定如此),new指令之后会接着执行()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。
2. 对象的内存布局
  1. 存储布局

    • HotSpot VM中,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
  2. 对象头?

    • 对象头信息1:
      用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它 为“Mark Word”。
      在这里插入图片描述
    • 对象头信息2:
      另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针 来确定该对象是哪个类的实例。
      并不是所有的虚拟机实现都必须在对象数据上保留类型指针,即,查找对象的元数据信息并不一定要经过对象本身。
      如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。
  3. 实例数据

    • 概念:存储对象真正的有效信息,即在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。
    • HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)
    • 以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
    • HotSpot虚拟机的+XX:CompactFields参数值为true(默认就为true),即子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。
  4. 对齐填充

    • 非必需,仅作占位符
    • 由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
3. 对象的访问定位
  1. Java角度

    • Java程序会通过栈上的reference数据来操作堆上的具体对象;
    • reference类型只规定了一个指向对象的引用;
    • 对象访问方式由虚拟机实现而定的;
    • 主流的两种访问方式主要:使用句柄和直接指针;
    • HotSpot,主要使用直接指针访问的方式进行对象访问。
  2. 使用句柄访问对象

    • Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
    • 优点:reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
      在这里插入图片描述
  3. 直接指针访问对象
    Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。

    • 优点:速度更快,节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本。
      在这里插入图片描述

2.3 OOM

参数:https://www.cnblogs.com/anyehome/p/9071619.html
1. OOM
* 概念:内存溢出(OutOfMemory,OOM):通常出现在某一块内存空间块耗尽的时候。(jvms)

  1. 相关参数设置
    • 执行时需要设置的虚拟机启动参数(注释中“VM Args”后面跟着的参 数)
    • 使用控制台命令来执行程序,直接跟在Java命令之后书写就可以。
    • Eclipse,在Debug/Run页签中的设置,其他IDE工具均有类似的设置。
    • 此处基于OpenJDK 7中的HotSpot虚拟机
0. 参数(jvms)版本不同,指令不同,关注
  1. 跟踪垃圾回收-读懂虚拟机日志

    指令 作用
    -XX:+PrintGC 启动JVM后,只要遇到GC就会打印日志
    -XX:+PrintGCDetails 使JVM在退出前打印堆的信息,详细描述了当前堆的各个区间的使用情况
    -XX:+PrintHeapAtGC 输出更全面的堆信息
    -XX:+PrintGCTimeStamps 在每次GC发生时,额外输出GC发生的时间,该输出时间为JVM启动后的时间偏移量
    -XX:+PrintGCApplicationConcurrentTime 打印应用程序的执行时间
    -XX:+PrintGCApplicationStopedTime 打印应用程序由于GC而产生的停顿时间
    -XX:+PrintReferenceGC 可以跟踪系统内的软引用、弱引用、虚引用和Finallize队列
    -Xloggc JVM将日志以文件形式输出(默认GC日志在控制台输出);如用-Xloggc:log/gc.log启动JVM,log文件夹下的gc.log记录所有GC文件
  2. 类加载/卸载的跟踪

    指令 作用
    -verbose:class 跟踪类的加载和卸载
    -XX:+TraceClassLoading 单独跟踪加载
    -XX:+TraceClassUnloading 单独跟踪类的卸载
    -XX:+PrintClassHistogram 在运行时打印、查看系统中类的分布情况;会显示当前的类信息柱状图
  3. 系统参数查看

    指令 作用
    -XX:+PrintVMOptions 可以在程序运行时,打印JVM接受到的命令行显式参数
    -XX:+PrintCommandLineFlags 打印传递给JVM的显示和隐式参数,参数可能是通过命令行直接给出的,也可以是JVM启动时设置的
    -XX:+PrintFlagsFinal 打印所有系统参数的值
  4. 堆的参数配置

    • 最大堆和初始堆的设置
    指令 作用
    -Xms 指定初始堆空间的大小
    -Xmx 指定最大堆空间,为最大可用内存;初始堆空间耗尽,JVM对堆空间扩展,扩展上限为最大堆空间
    -XX:MaxHeapSize -XX:MaxHeapSize=20971520,当前总内存不小于-Xms的设定,当前总内存在-Xms和-Xmx之间,从-Xms开始根据需求向上增长;当前空闲内存为当前总内存减去当前已使用的空间
    • 新生代的配置
    指令 作用
    -Xmn 设置新生代的大小。设置较大新生代会减少老年代的大小,影响系统性能以及GC行为。一般设置为整个堆空间的1/3-1/4
    -Xmx 指定最大堆空间,为最大可用内存;初始堆空间耗尽,JVM对堆空间扩展,扩展上限为最大堆空间
    -XX:SurvivorRation -XX:SurvivorRation=eden/from=eden/to,设置新生代的eden空间和from/to空间的比例关系;如-XX:SurvivorRation=2,(eden=512k)/(from=256
    -XX:NewRatio -XX:NewRatio=老年代/新生代;同-Xmn功能,用来设置新生代和老年代的比例

    在这里插入图片描述

  5. 堆溢出处理

    • OOM时导出堆信息
    指令 作用
    -XX:+HeapDumpOnOutOfMemoryError 在内存溢出时导出整个堆信息
    -XX:HeapDumpPath 指定导出堆的存放路径;如,-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
    • OOM执行脚本文件
      -作用:用于崩溃程序的自救、报警或者通知,也可以帮助RD获取更多系统信息,如完整的线程转存(即Thread Dump或Core Dump)文件。
      -例子:发生OOM时导出线程转存
      1. // printstack.bat文件
      2. D:/tool/jdk.7_40/bin/jstack -F %1 > D:/a.txt // D:/tool/jdk.7_40 jdk目录
      3. // 完整参数配置
      4. -Xmx20m -Xms5m "-XX:OnOutOfMemoryError=D:/tool/jdk.7_40/bin/printstack.bat %p" -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
  6. 非堆内存的参数配置-方法区

    • 方法区:主要存放类的元信息
    指令 作用
    jdk1.6、jdk1.7 -XX:PermSize、-XX:MaxPermSize配置永久区大小。-XX:PermSize表示初始永久区大小,-XX:MaxPermSize表示最大永久区
    jdk1.8 永久区被删除,元空间存放类的元数据,默认元空间值受系统可用内存的限制,但依然可以使用-XX:MaxMetaspaceSize指定永久区的最大可用值
  7. 非堆内存的参数配置-栈

    指令 作用
    -Xss 指定线程的栈大小。栈是每个线程私有的内存空间
  8. 非堆内存的参数配置-直接内存

    • 直接内存跳过了Java堆,使Java程序可以直接访问原生堆空间,一定程愫加快了内存空间的访问速度,但不可滥用。
    • 直接内存的访问速度(读或者写)会快于堆内存
    • 堆内存申请内存空间的速度远远高于直接内存
    指令 作用
    -XX:MaxDirectMemorySize 最大可用直接内存,不设置则默认为最大堆空间,即-Xmx。直接内存使用量达到-XX:MaxDirectMemorySize,会触发GC,如果GC不能有效释放足够空间,直接内存溢出会引起系统OOM
  9. 虚拟机工作模式

    • JVM支持Client和Server两种运行模式。
    指令 作用
    -client 指定使用Client模式
    -server 指定使用Server模式
    -version 查看当前模式
1. Java堆溢出
  1. 内存溢出

    • Java堆用于储存对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
    • 原因
      大量对象占据了堆空间,而这些对象都持有强引用,导致无法回收,当对象大小之和大于由Xmx参数指定的堆空间大小时,溢出错误就自然而然地发生了。(jvms)
  2. 例子

    • 参数设置

      VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

    • 限制Java堆的大小为20MB,并不可扩展
      (实现:将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),
    • -XX:+HeapDumpOnOutOf-MemoryError :让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析
    • 代码

      1. public class HeapOOM {
      2. static class OOMObject {}
      3. public static void main(String[] args) {
      4. List<OOMObject> list = new ArrayList<OOMObject>();
      5. while (true) {
      6. // 一个ArrayList对象总是持有OOMObject对象的强引用,导致其无法回收
      7. list.add(new OOMObject());
      8. }
      9. }
      10. }
      11. // 结果
      12. //java.lang.OutOfMemoryError:
      13. // Java heap space Dumping heap to java_pid3404.hprof ...
      14. // Heap dump file created [22045981 bytes in 0.663 secs]
    • 堆内存溢出
      出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Java heap space”,表示一次堆空间的溢出

    • 缓解堆溢出错误:(jvms)
      一方面可以使用-Xmx指定一个更大的堆空间,另一方面,由于堆空间不可能无限增长,通过工具分析找到大量占用堆空间的对象,并优化。
  3. 常规的处理方法

    • 首先通过内存映像分析工具对Dump出来的堆转储快照进行分析。
      第一步首先应确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
    • 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
    • 如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
2. 虚拟机栈和本地方法栈溢出
  1. 参数设置

    • HotSpot虚拟机中并不区分虚拟机栈和本地方法栈
    • HotSpot,栈容量只能由-Xss参数来设定,-Xoss参数(设置本地方法栈大小)虽存在,但无任何效果。
  2. 异常类型

    • 在《Java虚拟机规范》中描述了两种异常:
      1)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
      2)如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。
    • 原因:
      HotSpot虚拟机不支持扩展支持栈的动态扩展,只会在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,也只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。
  3. 出现异常的情况(单线程)

    • 使用-Xss参数减少栈内存容量。 结果:抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。
    • 定义了大量的本地变量,增大此方法帧中本地变量表的长度。 结果:抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。
  4. 例子2

    • 代码

      1. public class JavaVMStackSOF {
      2. private static int stackLength = 0;
      3. public static void test() {
      4. long unused1, unused2, unused3, unused4, unused5, unused6, unused7, unused8, unused9, unused10, unused11, unused12, unused13, unused14, unused15,
      5. unused16, unused17, unused18, unused19, unused20, unused21, unused22, unused23, unused24, unused25, unused26, unused27, unused28, unused29,
      6. unused30, unused31, unused32, unused33, unused34, unused35, unused36, unused37, unused38, unused39, unused40, unused41, unused42, unused43,
      7. unused44, unused45, unused46, unused47, unused48, unused49, unused50, unused51, unused52, unused53, unused54, unused55, unused56, unused57,
      8. unused58, unused59, unused60, unused61, unused62, unused63, unused64, unused65, unused66, unused67, unused68, unused69, unused70, unused71,
      9. unused72, unused73, unused74, unused75, unused76, unused77, unused78, unused79, unused80, unused81, unused82, unused83, unused84, unused85,
      10. unused86, unused87, unused88, unused89, unused90, unused91, unused92, unused93, unused94, unused95, unused96, unused97, unused98, unused99,
      11. unused100;
      12. stackLength++;
      13. test();
      14. unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7 = unused8 = unused9 = unused10 = unused11 = unused12 = unused13 = unused14
      15. = unused15 = unused16 = unused17 = unused18 = unused19 = unused20 = unused21 = unused22 = unused23 = unused24 = unused25 = unused26 = unused27
      16. = unused28 = unused29 = unused30 = unused31 = unused32 = unused33 = unused34 = unused35 = unused36 = unused37 = unused38 = unused39 = unused40
      17. = unused41 = unused42 = unused43 = unused44 = unused45 = unused46 = unused47 = unused48 = unused49 = unused50 = unused51 = unused52 = unused53
      18. = unused54 = unused55 = unused56 = unused57 = unused58 = unused59 = unused60 = unused61 = unused62 = unused63 = unused64 = unused65 = unused66
      19. = unused67 = unused68 = unused69 = unused70 = unused71 = unused72 = unused73 = unused74 = unused75 = unused76 = unused77 = unused78 = unused79
      20. = unused80 = unused81 = unused82 = unused83 = unused84 = unused85 = unused86 = unused87 = unused88 = unused89 = unused90 = unused91 = unused92
      21. = unused93 = unused94 = unused95 = unused96 = unused97 = unused98 = unused99 = unused100 = 0;
      22. }
      23. public static void main(String[] args) {
      24. try {
      25. test();
      26. } catch (Error e) {
      27. System.out.println("stack length:" + stackLength);
      28. throw e;
      29. }
      30. }
      31. }
      32. // 结果
      33. // stack length:5675 Exception in thread "main"
      34. // java.lang.StackOverflowError at org.fenixsoft.oom.
      35. // JavaVMStackSOF.leak(JavaVMStackSOF.java:27) at org.fenixsoft.oom.
      36. // JavaVMStackSOF.leak(JavaVMStackSOF.java:28) at org.fenixsoft.oom.
      37. // JavaVMStackSOF.leak(JavaVMStackSOF.java:28)
      38. // ……后续异常堆栈信息省略
    • 实验结果表明:无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。

    • (如果在允许动态扩展栈容量大小的虚拟机上,相同代码则会导致不一样的情况,相同的代码在Classic虚拟机中成功产生了OutOfMemoryError而不是StackOverflowError异 常。)
  5. 出现异常的情况(多线程)

    • 问题
      通过不断建立线程的方式,在HotSpot上也是可以产生内存溢出异常的,但是这样产生的内存溢出异常和栈空间是否足够并不存在任何直接的关系,主要取决于操作系统本身的内存使用状态。甚至可以说,在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。
    • 原因
      原因其实不难理解,操作系统分配给每个进程的内存是有限制的,譬如32位Windows的单个进程 最大内存限制为2GB。HotSpot虚拟机提供了参数可以控制Java堆和方法区这两部分的内存的最大值,那剩余的内存即为2GB(操作系统限制)减去最大堆容量,再减去最大方法区容量,由于程序计数器消耗内存很小,可以忽略掉,如果把直接内存和虚拟机进程本身耗费的内存也去掉的话,剩下的内存就由虚拟机栈和本地方法栈来分配了。因此为每个线程分配到的栈内存越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
  6. 多线程例子

    • 参数

      VM Args:-Xss2M (这时候不妨设大些,请在32位系统下运行)

    • 代码

      1. public class JavaVMStackOOM {
      2. private void dontStop() {
      3. while (true) {
      4. }
      5. }
      6. public void stackLeakByThread() {
      7. while (true) {
      8. Thread thread = new Thread(new Runnable() {
      9. @Override
      10. public void run() {
      11. dontStop();
      12. }
      13. });
      14. thread.start();
      15. }
      16. }
      17. public static void main(String[] args) throws Throwable {
      18. JavaVMStackOOM oom = new JavaVMStackOOM();
      19. oom.stackLeakByThread();
      20. }
      21. }
      22. // 在32位操作系统下的运行结果:
      23. // Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
    • 如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。这种通过“减少内存”的手段来解决内存溢出的方式.

3. 方法区和运行时常量池溢出
  1. 解决永久区溢出,从以下几个方面考虑(jvms)
    • 增加MaxPermSize的值
    • 减少系统需要的类的数量
    • 使用ClassLoader合理地装载各个类,并定期进行回收

// 字符串常量池

  1. 背景

    • 问题
      HotSpot从JDK 7开始逐步“去永久代”的计划,并在JDK8中完全使用元空间来代替永久代的背景,在此以测试代码来观察一下,使用“永久代”还是“元空间”来实现方法区,对程序的实际影响。
    • String::intern()
      如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
    • intern()中的“java”:https://www.zhihu.com/question/51102308/answer/124441115
  2. 例子jdk6

    • JDK6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中
      通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量;

      VM Args:-XX:PermSize=6M -XX:MaxPermSize=6M

    • 以JDK6来运行代码:

      1. public class RuntimeConstantPoolOOM {
      2. public static void main(String[] args) {
      3. // 使用Set保持着常量池引用,避免Full GC回收常量池行为
      4. Set<String> set = new HashSet<String>();
      5. // 在short范围内足以让6MB的PermSize产生OOM了
      6. short i = 0;
      7. while (true) {
      8. set.add(String.valueOf(i++).intern());
      9. }
      10. }
      11. }
      12. // 运行结果
      13. // Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
      14. // at java.lang.String.intern(Native Method)
      15. // at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)
    • 运行结果表明:运行时常量池溢出时,在OutOfMemoryError异常后面跟随的提示信息是“PermGen space”,说明运行时常量池的确是属于方法区(即JDK6的HotSpot虚拟机中的永久代)的一部分。

    • 溢出异常,循环将一直进行下去,永不停歇。
  3. 例子jdk7或+

    • 把方法区容量同样限制在6MB:
      -- JDK7使用-XX:MaxPermSize参数;
      -- JDK8及以上使用-XX:MaxMeta-spaceSize;
    • 结果:不会出现溢出异常,循环将一直进行下去,永不停歇;
    • 原因:JDK7起,原本存放在永久代的字符串常量池被移至Java堆之中
    • 结论:JDK7及以上版本,限制方法区的容量对本测试用例无意义。
    • 把Java堆容量同样限制在6MB:
      -- 用-Xmx参数限制最大堆到6MB;
      -- 获得如下两种运行结果之一,具体取决于哪里的对象分配时产生了溢出:
      1. // OOM异常一:
      2. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
      3. at java.base/java.lang.Integer.toString(Integer.java:440)
      4. at java.base/java.lang.String.valueOf(String.java:3058)
      5. at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12)
      6. // OOM异常二:
      7. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
      8. at java.base/java.util.HashMap.resize(HashMap.java:699)
      9. at java.base/java.util.HashMap.putVal(HashMap.java:658)
      10. at java.base/java.util.HashMap.put(HashMap.java:607)
      11. at java.base/java.util.HashSet.add(HashSet.java:220)
      12. at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java from InputFile-Object:14)
  4. 字符串常量池的实现在哪里出现问题?

    • 示例代码

      1. public class RuntimeConstantPoolOOM {
      2. public static void main(String[] args) {
      3. String str1 = new StringBuilder("计算机").append("软件").toString();
      4. System.out.println(str1.intern() == str1);
      5. String str2 = new StringBuilder("ja").append("va").toString();
      6. System.out.println(str2.intern() == str2);
      7. }
      8. }
    • 问题:
      在JDK6中运行,会得到两个false,而在JDK7中运行,会得到一个true和一个false;

    • 原因:
      -- JDK6中,intern()把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回永久代里面这个字符串实例的引用,StringBuilder创建的字符串对象实例在Java堆上,所以必然不可能是同一个引用,结果将返回false。
      -- JDK7(以及部分其他虚拟机,例如JRockit)中,intern()方法实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个。
      对str2比较返回false,这是因为“java”这个字符串在执行StringBuilder.toString()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到”的原则,“计算机软件”这个字符串则是首次出现的,因此结果返回true。
      (知乎:“java”已存在字符串常量池中
      https://www.zhihu.com/question/51102308/answer/124441115)

// 其他部分

  1. 背景:

    • 存储:
      方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
    • 测试思路:
      运行时产生大量的类去填满方法区,直到溢出为止。
    • 参数配置:

      VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M

    • 借助CGLib直接操作字节码运行时生成了大量的动态类,使得方法区出现内存溢出异常:

      1. public class JavaMethodAreaOOM {
      2. public static void main(String[] args) {
      3. while (true) {
      4. Enhancer enhancer = new Enhancer();
      5. enhancer.setSuperclass(OOMObject.class);
      6. enhancer.setUseCache(false);
      7. enhancer.setCallback(new MethodInterceptor() {
      8. public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
      9. return proxy.invokeSuper(obj, args);
      10. }
      11. });
      12. enhancer.create();
      13. }
      14. }
      15. static class OOMObject {
      16. }
      17. }
      18. // 在JDK7中的运行结果:
      19. Caused by: java.lang.OutOfMemoryError: PermGen space
      20. at java.lang.ClassLoader.defineClass1(Native Method)
      21. at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
      22. at java.lang.ClassLoader.defineClass(ClassLoader.java:616) ... 8 more
    • 此处模拟的场景并非纯粹是一个实验,类似这样的代码确实可能会出现在实际应用中:
      -- Spring、Hibernate很多主流框架对类进行增强时,都会使用到CGLib这类字节码技术,当增强的类越多,就需要越大的方法区以保证动态生成的新类型可以载入内存。
      -- 另外,很多运行于Java虚拟机上的动态语言(例如Groovy等)通常都会持续创建新类型来支撑语言的动态性,会遇到相似溢出场景

  2. tips

    • 生成动态类:
      -- 使用Java SE API也可以动态产生类(如反射时的 GeneratedConstructorAccessor和动态代理等);借助了CGLib操作字节码运行时生成了大量的动态类(CGLib字节码增强和动态语言外)
      -- 常见的还有:大量JSP或动态产生JSP 文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同 的加载器加载也会视为不同的类)等。
  3. 发展,参数,jdk8后
    在JDK8以后,永久代便完全退出了历史舞台,元空间作为其替代者登场。使用者有预防实际应用里出现类似于代码清单2-9那样的破坏性的操作,HotSpot还是提供了一 些参数作为元空间的防御措施,主要包括: ·-XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
    ·-XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
    ·-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。

4. 本机直接内存溢出
  1. 直接内存参数

    • -XX:MaxDirectMemorySize
      指定直接内存的容量大小,默认与Java堆最大值(由-Xmx指定)一致
  2. 代码示例

    • 参数

      VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M

    • 越过了DirectByteBuffer类直接通 过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法指定只有引导类加载器才会返回实例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe的功能,在JDK 10时才将Unsafe的部分功能通过VarHandle开放给外部使用),因为虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存的方法是Unsafe::allocateMemory()。
    • 使用unsafe分配本机内存:

      1. public class DirectMemoryOOM {
      2. private static final int _1MB = 1024 * 1024;
      3. public static void main(String[] args) throws Exception {
      4. Field unsafeField = Unsafe.class.getDeclaredFields()[0];
      5. unsafeField.setAccessible(true);
      6. Unsafe unsafe = (Unsafe) unsafeField.get(null);
      7. while (true) {
      8. unsafe.allocateMemory(_1MB);
      9. }
      10. }
      11. }
      12. // 运行结果
      13. Exception in thread "main" java.lang.OutOfMemoryError
      14. at sun.misc.Unsafe.allocateMemory(Native Method)
      15. at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)
    • 由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了

3. 垃圾收集器与内存分配策略

  1. 垃圾收集(Garbage Collection,GC)
  2. 垃圾收集需要完成的三件事情:
    · 哪些内存需要回收?
    · 什么时候回收?
    · 如何回收?
  3. Java内存运行时区域
    • 程序计数器、虚拟机栈、本地方法栈3个区域
      -- 随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着回收了。
    • Java堆、方法区
      -- 这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。

3.1 对象已死?

  1. “死去”即不可能再被任何途径使用的对象。
1. 引用计数算法
  1. 引用计数算法(Reference Counting)

    • 概念
      在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;
      当引用失效时,计数器值就减一;
      任何时刻计数器为零的对象就是不可 能再被使用的
    • 优点
      会占用了一些额外的内存空间来进行计数,原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。
    • 缺点
      必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
    • 循环引用例子
      对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也 就无法回收它们。
      1. /*** testGC()方法执行后,objA和objB会不会被GC呢? * @author zzm */
      2. public class ReferenceCountingGC {
      3. public Object instance = null;
      4. private static final int _1MB = 1024 * 1024;
      5. /*** 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过 */
      6. private byte[] bigSize = new byte[2 * _1MB];
      7. public static void testGC() {
      8. ReferenceCountingGC objA = new ReferenceCountingGC();
      9. ReferenceCountingGC objB = new ReferenceCountingGC();
      10. objA.instance = objB;
      11. objB.instance = objA;
      12. objA = null;
      13. objB = null;
      14. // 假设在这行发生GC,objA和objB是否能被回收?
      15. System.gc();
      16. }
      17. }
      18. //运行结果:
      19. //看到内存回收日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就放弃回收它们,
      20. //这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象 是否存活的。
2. 可达性分析算法
  1. 可达性分析(Reachability Analysis)算法
    • 概念
      通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
    • 固定可作为GC Roots的对象:
      -- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
      -- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
      -- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
      -- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
      -- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
      -- 所有被同步锁(synchronized关键字)持有的对象。
      -- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
    • 临时GC Roots
      -- 根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。
      -- 如后文将会提到的分代收集和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集),必须考虑到内存区域是虚拟机自己的实现细节(在用户视角里任何内存区域都是不 可见的),更不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GCRoots集合中去,才能保证可达性分析的正确性。
3. 引用
  1. 引用-JDK1.2版之前

    • 概念:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。
  2. 引用-JDK1.2版之后

    • 目的:为描述当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象
    • 将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
  3. 4种引用的概念

    • 强引用:最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。
    • 软引用:用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2版之后提供了SoftReference类来实现软引用。
    • 弱引用:也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2版之后提供了WeakReference类来实现弱引用。
    • 虚引用:也称为“幽灵引用”或者“幻影引用”,是最弱的一种引用关系。一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK1.2版之后提供 了PhantomReference类来实现虚引用。
4. 两次标记对象-finalize()
  1. 垃圾的两次标记?

    • 宣告一个对象死亡,至少要经历两次标记过程:
    • 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是 否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
  2. 对象执行finalize()方法

    • 该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。
    • finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
  3. finalize()工程使用?

    • 运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为 不推荐使用的语法。尽量避免使用它,finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及时
  4. 一次对象自我拯救的演示

    • 代码
      1. /*** 此代码演示了两点:
      2. * 1.对象可以在被GC时自我拯救。
      3. * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
      4. */
      5. public class FinalizeEscapeGC {
      6. public static FinalizeEscapeGC SAVE_HOOK = null;
      7. public void isAlive() {
      8. System.out.println("yes, i am still alive :)");
      9. }
      10. @Override
      11. protected void finalize() throws Throwable {
      12. super.finalize();
      13. System.out.println("finalize method executed!");
      14. // (重新与引用链上的任何一个对象建立关联,譬如把自己 (this关键字)赋值给某个类变量或者对象的成员变量,
      15. // (那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了
      16. FinalizeEscapeGC.SAVE_HOOK = this;//
      17. }
      18. public static void main(String[] args) throws Throwable {
      19. SAVE_HOOK = new FinalizeEscapeGC();
      20. //对象第一次成功拯救自己
      21. SAVE_HOOK = null;
      22. System.gc();
      23. // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
      24. Thread.sleep(500);
      25. if (SAVE_HOOK != null) {
      26. SAVE_HOOK.isAlive();
      27. } else {
      28. System.out.println("no, i am dead :(");
      29. }
      30. // 下面这段代码与上面的完全相同,但是这次自救却失败了(因为任何一个对象的finalize()方法都只会被系统自动调用一次
      31. SAVE_HOOK = null;
      32. System.gc();
      33. // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
      34. Thread.sleep(500);
      35. if (SAVE_HOOK != null) {
      36. SAVE_HOOK.isAlive();
      37. } else {
      38. System.out.println("no, i am dead :(");
      39. }
      40. }
      41. }
      42. // 结果:
      43. // finalize method executed!
      44. // yes, i am still alive :)
      45. // no, i am dead :(
5. 方法区回收
  1. 回收的内容?

    • 方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
  2. 回收-废弃常量?

    • 判定一个常量是否“废弃”
      与回收Java堆中的对象非常类似。举个常量池中字面量回收的例子,假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。
  3. 回收-类型

    • 要判定一个类型是否属于“不再被使用的类”需要同时满足下面三个条件:
      -- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
      -- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
      -- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    • 是否要对类型回收
      -- HotSpot VM
      a. -Xnoclassgc参数进行控制;
      b. -verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息;
      其中,-verbose:class、-XX:+TraceClassLoading可以在Product版的虚拟机中使用;
      -XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。
    • 在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

3.2 垃圾收集算法

  1. 算法分类

    • 从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”(Reference Counting GC)和“追踪式垃圾收集”(Tracing GC)两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。
    • 由于引用计数式垃圾收集算法在本书讨论到的主流Java虚拟机中均未涉及,本节介绍的所有算法均属于追踪式垃圾收集的范畴。
  2. 名词概念

    • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,又分为:
      ■ 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
      ■ 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
      ■ 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
    • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
1. 分代收集理论
  1. 分代假说:
    1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
    2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
    3)跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

  2. 垃圾收集器设计原则:
    基于分代假说,收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

  3. 跨代引用

    • 在新生代上建立一个全局的数据结构(该结构被称 为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会 存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
2. 标记-清除算法
  1. “标记-清除”(Mark-Sweep)算法
    算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

  2. 主要的两个缺点:

    • ① 执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
    • ② 内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
  3. 示意图
    在这里插入图片描述

3. 标记-复制算法
  1. 标记-复制算法

    • 简称复制算法
    • 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
  2. 优缺点

    • 优点:
      对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效。
    • 缺点:这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。
  3. 图示
    在这里插入图片描述

  4. 复制-新生代应用

    • IBM公司曾有一项专门研究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。
    • Appel式回收:针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策 略。
      HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设 计新生代的内存布局
    • Appel式回收的具体做法:
      把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新 生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会 被“浪费”的。
4. 标记-整理算法
  1. “标记-整 理”(Mark-Compact)算法

    • 应用-老年代
    • 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
  2. 示意图
    在这里插入图片描述

  3. 移动问题-Stop The World

    • 概念:
      移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新 所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用 程序才能进行,这就更加让使用者不得不小心翼翼地权衡其弊端了,像这样的停顿被最初的虚拟机 设计者形象地描述为“Stop The World”。

3.3 HotSpot的算法细节实现

1-根节点枚举
  1. 根节点枚举现象-STW?

    • 根节点枚举必须在一个能保障一致性的快照中才得以进行。
    • “一致性”的意思是整个枚举期间执行子系统看起来就像被冻结在某个时间点上,不会出现分析过程中,根节点集合的对象引用关系还在不断变化 的情况,保证分析结果准确性。这是垃圾收集过程必须停顿所有用户线程的一个重要原因。
    • 停顿时间可控,或者(几乎)不会发生停顿的CMS、G1、ZGC等收集器,枚举根节点时也是必须要停顿的。
    • 总:所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,会面临相似的“Stop The World”的困扰。
  2. OopMap的数据结构解决的问题?

    • 问题:
      当用户线程停顿下来之后,虚拟机可以直接得到哪些地方存放着对象引用的,不需要一个不漏地从方法区等GC Roots开始查找,检查完所有执行上下文和全局的引用位置。
    • HotSpot:
      使用一组称为OopMap的数据结构来快速准确地完成GC Roots枚举。
      一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。收集器在扫描时就可以直接得知这些信息了。
2-安全点
  1. GCRoots枚举带来的问题?

    • HotSpot在OopMap的协助下快速准确地完成GCRoots枚举的过程,可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外存储空间,增加垃圾收集的空间成本。
  2. 什么是安全点?

    • GCRoots枚举是在“特定的位置”记录了这些信息,这些位置被称为安全点(Safepoint)。
    • 强制要求必须执行到达安全点后才能够暂停,进行开始垃圾收集。
    • 安全点的选定既不能太少以至于让收集器等待时间过长,也不能太过频繁以至于过分增大运行时的内存负荷。
    • 标准:“是否具有让程序长时间执行的特征”;
    • 因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这样的原因而长时间执行,“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。
  3. 如何在垃圾收集发生时让所有线程(这里其实不包括执行JNI调用的线程)都跑到最近的安全点,然后停顿下来?

    • 这里有两种方案可供选择:抢先式中断 (Preemptive Suspension)和主动式中断(Voluntary Suspension)
    • 抢先式中断
      不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。
    • 主动式中断
      当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。
3-安全区域
4-记忆集与卡表
5-写屏障
6-并发的可达性分析

3.4 经典垃圾收集器

  1. 垃圾收集器
    在这里插入图片描述

  2. 并行、并发

    • 并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线 程在协同工作,通常默认此时用户线程是处于等待状态。
    • 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾 收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。
CMS收集器
  1. 概念

    • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
    • “Mark Sweep”,可以看出CMS收集器是基于标记-清除算法实现的。
  2. 运作过程的四个步骤?
    1)初始标记(CMS initial mark)
    2)并发标记(CMS concurrent mark)
    3)重新标记(CMS remark)
    4)并发清除(CMS concurrent sweep)

    • 初始标记、重新标记:这两个步骤仍然需要“Stop The World”。
    • 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;
    • 并发标记阶段:就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
    • 重新标记阶段:为修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;
    • 并发清除阶段:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
  3. 示意图

    • 整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
      在这里插入图片描述
  4. 优缺点

    • 优点:并发收集、低停顿。并发低停顿收集器”(Concurrent Low Pause Collector)
    • 缺点
      1)CMS收集器对处理器资源非常敏感。
      2)由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
      3)CMS是一款基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。
  5. 缺点引发问题

    • 1)CMS收集器对处理器资源非常敏感。
      事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的 处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时, CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。为了缓解这种情况,虚拟机提供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种,所做的事情和以前单核处理器年代PC机操作系统靠抢占式多任务来模拟多核并行多任务的思想一样,是在并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得较少一些,直观感受是速度变 慢的时间更多了,但速度下降幅度就没有那么明显。实践证明增量式的CMS收集器效果很一般,从 JDK 7开始,i-CMS模式已经被声明为“deprecated”,即已过时不再提倡用户使用,到JDK 9发布后i- CMS模式被完全废弃。
    • 2)由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
      在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。在JDK 5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快,可以适当调高参数-XX:CMSInitiatingOccu-pancyFraction的值来提高CMS的触发百分比,降低内存回收频率,获取更好的性能。到了JDK6时,CMS收集器的启动阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集, 但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置。
    • 3)CMS是一款基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。
      空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找 到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,此参数从 JDK9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个 内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。这样空间碎片问题是解 决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBefore- Compaction(此参数从JDK9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量 由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表 示每次进入Full GC时都进行碎片整理)。
G1收集器
  1. Garbage First(简称G1)收集器概念

    • 收集器面向局部收集的设计思路和基于Region的内存布局形式。
    • 全功能的垃圾收集器
    • 可以由用户指定期望的停顿时间
      通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。
  2. G1收集器的运作过程大致可划分为以下四个步骤
    1)初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
    2)并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
    3)最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
    4)筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

  3. 示意图
    在这里插入图片描述

  4. 优缺点

    • 优点
      -- 可以指定最大停顿时间、分Region的内存布局、按收益动态确定回收集,
      -- G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。
      有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。

3.5 实战:内存分配与回收策略

4.虚拟机性能监控、故障处理工具

5.调优案例分析与实战

三、虚拟机执行子系统

四、程序编译与代码优化

1.3 JIT(Just In Time)即时编译器

五、高效并发

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注