JVM入门到入土

JVM

  • 1 字节码篇
    • 1.1 JVM概述
      • 1.1.1 Java语言及Java生态圈
      • 1.1.2 JVM架构与知识脉络图
    • 1.2 字节码文件概述
      • 1.2.1字节码文件—跨平台
      • 1.2.2什么是字节码指令?
    • 1.3 Class文件结构细节
    • 1.4 官方反解析工具—javap
    • 1.5 字节码指令集与解析
      • 1.5.1 字节码与数据类型
      • 1.5.2 指令分类
  • 2 类的加载篇
    • 2.1 类的加载过程(生命周期)
      • 2.1.1 说说类加载分几步?
      • 2.1.2 Loading阶段
      • 2.1.3 Linking阶段:
      • 2.1.4 Initialization阶段
      • 2.1.5 类的使用
      • 2.1.6 类的卸载
    • 2.2 类的加载器
      • 2.2.1 作用
      • 2.2.2 类的加载是唯一的吗?
      • 2.2.3 类加载机制的基本特征
    • 2.3类的加载器分类
      • 2.3.1 子父类加载器的关系
      • 2.3.2 具体类的加载器介绍
      • 2.3.3 用户自定义类加载器
    • 2.4 ClassLoader源码分析
    • 2.5 相关机制
      • 2.5.1 双亲委派机制
      • 2.5.2 沙箱安全机制
  • 3 运行时内存篇
    • 3.1 JVM内存布局
    • 3.2 程序计数器
    • 3.3 虚拟机栈
      • 3.3.1 虚拟机栈概述
      • 3.3.2 栈的单位—栈帧
      • 3.3.3 栈的内部结构
    • 3.4 本地方法栈
    • 3.5 堆
      • 3.5.1 堆的内部结构
      • 3.5.2 如何设置堆内存大小
      • 3.5.2 对象分配
      • 3.5.3 MinorGC、MajorGC、FullGC
      • 3.5.4 OOM如何解决
      • 3.5.5 快速分配策略:TLAB
      • 3.5.6 方法区
    • 3.6 永久代与元空间
    • 3.7 StringTable
  • 4 对象布局篇
    • 4.1 对象的实例化
    • 4.2 对象的内存布局
    • 4.3 对象的访问定位
  • 5 执行引擎篇
    • 5.1 执行引擎的作用
    • 5.2 代码编译和执行的过程
    • 5.3 JIT编译器
    • 5.4 HotSpot VM执行方式
  • 6 垃圾回收篇
    • 6.1 概述
    • 6.2 垃圾回收算法
      • 6.2.1 垃圾判别阶段算法(垃圾标记阶段)
      • 6.2.2 垃圾清除阶段算法
    • 6.3 相关概念
      • 6.3.1 System.gc()和finalize()方法详解
      • 6.3.2 STW
      • 6.3.3 垃圾回收的并行与并发
      • 6.3.4 五种引用
    • 6.4 垃圾回收器
      • 6.4.1 GC分类
      • 6.4.2 GC评估指标
      • 6.4.3 垃圾回收器有哪些?
      • 6.4.4 GC使用场景
  • 7 性能监控篇
    • 7.1 JVM监控及诊断工具—命令行
    • 7.2 JVM监控及诊断工具—GUI
    • 7.3 JVM参数
      • 7.3.1 JVM类型选项参数
      • 7.3.2 常用的JVM参数选项
  • 8 性能调优篇
    • 8.1 概述篇
    • 8.2 OMM案例
      • 8.2.1 堆溢出
      • 8.2.2 元空间溢出
      • 8.2.3 GC overhead limit exceeded
      • 8.2.4 线程溢出
    • 8.3 性能优化案例
    • 8.3 性能优化案例

1 字节码篇

1.1 JVM概述

1.1.1 Java语言及Java生态圈

  • Oracle JDK与Open JDK的关系:OpenJDK完全开源免费,版本更新速度快,而Oracle JDK绝大部分功能开源免费,版本更新速度较慢

  • JDK JRE JVM之间的关系:

    • Java Development Kit Java开发工具包 包括JRE和一些Java基础类库(供开发者使用的类库)
    • Java Runtime Environment Java运行环境,只能运行字节码文件,包括JVM和Java核心类库(JVM工作所需要的类库)
    • Java Virtual Machine Java虚拟机 是java跨平台特性的核心,通过JVM屏蔽了底层系统(windows、linux、Max等等)的差异,实现一次编译,到处运行。
  • 说说你认识的JVM

    • SUN公司的 HotSpot VM
    • BEA 的 JRockit --> 不包含解释器,服务器端,JMC
    • IBM 的 J9
  • JVM的生命周期

    • 虚拟机的启动:引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的
    • 虚拟机的退出:当线程调用Runtime类或System类的exit方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止
  • 重点说一下hotspot?

    • JDK 1开始使用hotspot虚拟机,使用JIT(Just in Time)编译器
    • Java原先是把源代码编译为字节码在虚拟机执行,这样执行速度较慢。而HotSpot将常用的部分代码编译为本地(原生,native)代码
    • HotSpot JVM 参数可以分为规则参数(standard options)和非规则参数(non-standard options)。 规则参数相对稳定,在JDK未来的版本里不会有太大的改动。 非规则参数则有因升级JDK而改动的可能。

1.1.2 JVM架构与知识脉络图

  • JVM架构图

    • 最上层:类加载器系统,分为引导类加载器,扩展类加载器,系统类加载器,把javac编译器将编译好的字节码class文件,通过java 类装载器执行机制,把对象或class文件存放在 jvm划分内存区域。装载->链接(验证,准备,解析)->初始化

    • 中间层:Runtime Data Area

      • 方法区和堆(线程共享)
      • 虚拟机栈,本地方法区,程序计数器
    • 最下层:执行引擎

      • 解释器
      • JIT(just in time)编译器
      • GC(Garbage Collection,垃圾回收器)
  • JVM知识脉络

    类加载->内存结构与分配<-(执行引擎)——>GC——>性能监控与调优

1.2 字节码文件概述

1.2.1字节码文件—跨平台

  • class文件里是什么?

    字节码是一种二进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码

  • 介绍一下生成的class文件的编译器?(前端编译器)

    • javac是一种能够将Java源码编译为字节码的前端编译器
    • 内置在Eclipse中的ECJ (Eclipse Compiler for Java)编译器。
    • 默认情况下,IntelliJ IDEA 使用 javac 编译器。(还可以自己设置为AspectJ编译器 ajc)
  • 哪些类型对应有Class的对象?class、interface、[]:数组、enum:枚举、annotation:注解@interface、primitive type:基本数据类型、void

1.2.2什么是字节码指令?

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码

1.3 Class文件结构细节

魔数,Class文件版本,常量池,访问标识,类索引,父类索引,接口索引集合,字段表集合,方法表集合,属性表集合

  • 魔数:class文件的标志

  • 常量池:存放所有常量,可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一,常量池表项中,用于存放编译时期生成的各种字面量和符号引用

  • 访问标识:在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等

  • 类索引,父类索引,接口索引集合

    • 类索引提供了类的全限定名
    • 父类索引当前类的父类的全限定名
    • 接口索引集合提供了一个符号引用到所有已实现的接口
  • 字段表集合:用于描述接口或类中声明的变量

  • 方法表集合:用于表示当前类或接口中某个方法的完整描述

  • 属性表集合:指的是class文件所携带的辅助信息

1.4 官方反解析工具—javap

javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息

1.5 字节码指令集与解析

1.5.1 字节码与数据类型

在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如,iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据

1.5.2 指令分类

加载与存储指令、算术指令、类型转换指令、对象的创建与访问指令、方法调用与返回指令、操作数栈管理指令、控制转移指令、异常处理指令、同步控制指令

  • 加载与存储指令:加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递。

    • 局部变量压栈指令:iload_0
    • 常量入栈指令:sipush,iconst_
    • 出栈装入局部变量表指令:istore_0
  • 算数指令:算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新压入操作数栈

  • 类型转换指令:类型转换指令可以将两种不同的数值类型进行相互转换

  • 创建和访问指令

    • 创建指令:创建类实例的指令—new,创建数组的指令—newarray

    • 字段访问指令:getstatic、putstatic、getfield、putfield

    • 数组操作指令:iaload、iastore

    • 类型检查指令

  • 方法调用与返回指令:

    • 方法调用指令:invokevirtual、invokeinterface、invokespecial、invokestatic 、invokedynamic
    • 方法返回指令:ireturn
  • 操作数栈管理指令:pop,dup

  • 控制转移指令

  • 异常处理指令

  • 同步控制指令

2 类的加载篇

2.1 类的加载过程(生命周期)

2.1.1 说说类加载分几步?

加载—>链接(验证、准备、解析)—>初始化

2.1.2 Loading阶段

将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。

  • 类模板对象(类的数据结构)存储的位置:方法区
  • 类实例对象存储位置:堆

2.1.3 Linking阶段:

  1. 验证阶段:目的是保证加载的字节码是合法、合理并符合规范的。
  2. 准备阶段:为类的静态变量分配内存,并将其初始化为默认值。(static final不会,因为final在编译的时候就会分配了,准备阶段会显式赋值)
  3. 解析阶段:将类、接口、字段和方法的符号引用转为直接引用,会将基本数据类型常量,String类型字面量的定义方式的常量显示赋值

2.1.4 Initialization阶段

为类的静态变量赋予正确的初始值。(显式初始化)

  1. 子类加载前先加载父类
  2. 当类中包含static类型的变量会生成clinit方法,但是如果有final static的类不会生成
  3. 普通基本数据类型和引用类型(即使是常量)的静态变量,是需要额外调用putstatic等JVM指令的,这些是在显式初始化阶段执行
  4. 类的主动使用和被动使用
    • 主动使用:
      • 创建一个类的实例,调用类的静态方法,
      • 使用类、接口的静态字段时,当使用java.lang.reflect包中的方法反射类的方法
      • 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
    • 被动使用:
      • 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化,引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了
      • 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化

2.1.5 类的使用

开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。

2.1.6 类的卸载

JVM入门到入土_第1张图片

如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载

2.2 类的加载器

2.2.1 作用

ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例(只负责loading过程)

2.2.2 类的加载是唯一的吗?

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等

2.2.3 类加载机制的基本特征

  • 双亲委派模型。但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如JDK内部的ServiceProvider/ServiceLoader机制,用户可以在标准API框架上,提供自己的实现,JDK也需要提供些默认的参考实现。例如,Java 中JNDI、JDBC、文件系统、Cipher等很多方面,都是利用的这种机制,这种情况就不会用双亲委派模型去加载,而是利用所谓的上下文加载器。

  • 可见性。子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。

  • 单一性。由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见。(邻居间具有隔离性)

2.3类的加载器分类

在程序中最常见的类加载器结构:

JVM入门到入土_第2张图片

2.3.1 子父类加载器的关系

  • 除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加载器。
  • 不同类加载器看似是继承(Inheritance)关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用。

2.3.2 具体类的加载器介绍

  1. 启动类加载器:用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容),Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类,用于提供JVM自身需要的类。
  2. 扩展类加载器:从JDK的安装目录的jre/lib/ext子目录下加载类库
  3. 系统类加载器
    • 继承于ClassLoader类,应用程序中的类加载器默认是系统类加载器。
    • 是用户自定义类加载器的默认父加载器,通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器

2.3.3 用户自定义类加载器

  • 通过类加载器可以实现非常绝妙的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
  • 自定义加载器能够实现应用隔离(不同的应用能够加载不同的类仅供自己使用)
  • 所有用户自定义类加载器通常需要继承于抽象类java.lang.ClassLoader。
    • 方式一:重写loadClass()方法
    • 方式二:重写findClass()方法 -->推荐

2.4 ClassLoader源码分析

  • loadClass方法(遵循双亲委派机制)
protected Class<?> loadClass(String name, boolean resolve) //resolve:true-加载class的同时进行解析操作。
    throws ClassNotFoundException{
    synchronized (getClassLoadingLock(name)) { //同步操作,保证只能加载一次。
        //首先,在缓存中判断是否已经加载同名的类。
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
               //获取当前类加载器的父类加载器。
                if (parent != null) {
                    //如果存在父类加载器,则调用父类加载器进行类的加载
                    c = parent.loadClass(name, false);
                } else { //parent为null:父类加载器是引导类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) { //当前类的加载器的父类加载器未加载此类 or 当前类的加载器未加载此类
                // 调用当前ClassLoader的findClass()
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {//是否进行解析操作
            resolveClass(c);
        }
        return c;
    }
}
 

  • Class.forName() 与 ClassLoader.loadClass()对比
    • Class.forName() :是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个 Class 对象。该方法在将 Class 文件加载到内存的同时,会执行类的初始化。
    • ClassLoader.loadClass():这是一个实例方法,需要一个 ClassLoader 对象来调用该方法。该方法将 Class 文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个 ClassLoader 对象,所以可以根据需要指定使用哪个类加载器。

2.5 相关机制

2.5.1 双亲委派机制

定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。

本质:规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。

JVM入门到入土_第3张图片

破坏双亲委派机制

  1. 双亲委派机制出现在JDK1.2之后,为了兼容该版本之前的用户已有的自定义类加载器代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。

  2. 线程上下文类加载器:父类加载器去请求子类加载器完成类加载的行为,默认上下文加载器就是应用类加载器,这样以上下文加载器为中介,使得启动类加载器中的代码也可以访问应用类加载器中的类。
    JVM入门到入土_第4张图片

  3. 是由于用户对程序动态性的追求而导致的。如:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等

Tomcat 如何实现自己独特的类加载机制

Tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类,各个web应用自己的类加载器(WebAppClassLoader)会优先查看自己的仓库加载,加载不到时再交给commonClassLoader走双亲委托。

JVM入门到入土_第5张图片

从图中的委派关系中可以看出:

  • CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
  • WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
  • 而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

JVM入门到入土_第6张图片

违背双亲委派原因

双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由委派自己的父类加载器加载。很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。

2.5.2 沙箱安全机制

沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问。通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。

3 运行时内存篇

3.1 JVM内存布局

JVM入门到入土_第7张图片

3.2 程序计数器

基本特征:

  • JVM中的程序计数寄存器源于CPU的寄存器,JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟
  • 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。不会随着程序的运行需要更大的空间。
  • 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
  • 它是唯一一个在Java 虚拟机规范中没有规定任何OutOtMemoryError 情况的区域。

作用:

  • 为了保证程序(在操作系统中理解为进程)能够连续地执行下去,CPU必须具有某些手段来确定下一条指令的地址。而程序计数器正是起到这种作用,所以通常又称为指令计数器。
  • 在程序开始执行前,必须将它的起始地址,即程序的一条指令所在的内存单元地址送入PC,因此程序计数器(PC)的内容即是从内存提取的第一条指令的地址。当执行指令时,CPU将自动修改PC的内容,即每执行一条指令PC增加一个量,这个量等于指令所含的字节数,以便使其保持的总是将要执行的下一条指令的地址。

3.3 虚拟机栈

3.3.1 虚拟机栈概述

Java 中堆和栈有什么区别?

  • 角度一:GC;OOM
  • 角度二:栈、堆执行效率
  • 角度三:内存大小;数据结构
  • 角度四:栈管运行;堆管存储。

如何设置栈的大小?-Xss size (即:-XX:ThreadStackSize)
一般默认为512k-1024k,取决于操作系统。栈的大小直接决定了函数调用的最大可达深度。

3.3.2 栈的单位—栈帧

方法和栈桢之间存在怎样的关系?

  • 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

JVM入门到入土_第8张图片

3.3.3 栈的内部结构

  1. 局部变量表:定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型(8种)、对象引用(reference),以及returnAddress类型。

    • 局部变量表不存在线程安全问题,因为虚拟机栈是线程私有的

    • Slot槽位是可以重复利用的

    • 静态变量与局部变量的对比:静态变量在准备阶段和初始化阶段能够被两次初始化,而局部变量必须手动赋值,否则会报错

    • 局部变量表中的变量是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

  2. 操作数栈:

    • 操作数栈,在方法执行过程中,根据字节码指令,并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作,往栈中写入数据或提取数据来完成一次数据访问
    • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
  3. 动态链接:

    • 常量池的作用:就是为了提供一些符号和常量,便于指令的识别。
    • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令
    • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
  4. 方法返回地址

3.4 本地方法栈

  • 本地方法栈,也是线程私有的。
  • 许被实现成固定或者是可动态扩展的内存大小。
  • 本地方法是使用C语言实现的。

3.5 堆

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。

  • Java 堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。

  • 堆内存的大小是可以调节的。

  • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

  • 堆,是GC ( Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。

  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

3.5.1 堆的内部结构

现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:

JVM入门到入土_第9张图片

新生代和老年代:

JVM入门到入土_第10张图片

  • 几乎所有的Java对象都是在Eden区被new出来的。
  • 绝大部分的Java对象的销毁都在新生代进行了。
  • IBM 公司的专门研究表明,新生代中 80% 的对象都是“朝生夕死”的。

3.5.2 如何设置堆内存大小

  • “-Xms”用于表示堆区的起始内存,等价于-XX:InitialHeapSize
  • “-Xmx”则用于表示堆区的最大内存,等价于-XX:MaxHeapSize

通常会将 -Xms 和 -Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能

  • heap默认最大值计算方式:如果物理内存少于192M,那么heap最大值为物理内存的一半。如果物理内存大于等于1G,那么heap的最大值为物理内存的1/4。
  • heap默认最小值计算方式:最少不得少于8M,如果物理内存大于等于1G,那么默认值为物理内存的1/64,即1024/64=16M。最小堆内存在jvm启动的时候就会被初始化。

设置新生代与老年代的比例:

  • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
  • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5

设置Eden、幸存者比例:

  • 在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1
  • 当然开发人员可以通过选项“-XX:SurvivorRatio”调整这个空间比例。比如-XX:SurvivorRatio=8

参数设置小结

  1. -Xms -Xmx:初始内存 (默认为物理内存的1/64;最大内存(默认为物理内存的1/4)

  2. -Xmn:设置新生代的大小。(初始值及最大值)

  3. -XX:NewRatio:默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3

  4. -XX:SurvivorRatio:Eden空间和另外两个Survivor空间缺省所占的比例是8:1

  5. -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄。超过此值,仍未被回收的话,则进入老年代,默认值为15

  6. -XX:+PrintGCDetails:输出详细的GC处理日志

  7. -XX:HandlePromotionFailure:在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间(空间分配担保策略

    • 如果大于,则此次Minor GC是安全的

    • 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。

    • 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。

3.5.2 对象分配

  • 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to.
  • 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集

JVM入门到入土_第11张图片

对象分配过程

  1. new的对象先放伊甸园区。此区有大小限制。
  2. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC/YGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
  3. 然后将伊甸园中的剩余对象移动到幸存者0区。
  4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
  5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
  6. 啥时候能去养老区呢?可以设置次数。默认是15次。-XX:MaxTenuringThreshold= 设置对象晋升老年代的年龄阈值。
  7. 在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理。
  8. 若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常

JVM入门到入土_第12张图片

内存分配策略

如果对象在Eden 出生并经过第一次MinorGC 后仍然存活,并且能被Survivor 容纳的话,将被移动到Survivor 空间中,并将对象年龄设为1 。对象在Survivor 区中每熬过一次MinorGC , 年龄就增加1岁,当它的年龄增加到一定程度(默认为15 岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中。

内存分配原则

针对不同年龄段的对象分配原则如下所示:

  • 优先分配到Eden
  • 大对象直接分配到老年代
  • 长期存活的对象分配到老年代
  • 如果Survivor 区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。

3.5.3 MinorGC、MajorGC、FullGC

概述:

  • 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
    • 新生代收集(Minor GC / Young GC):只是新生代(Eden\S0,S1)的垃圾收集
    • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。
      • 目前,只有CMS GC会有单独收集老年代的行为。
      • 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
  • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
    • 目前,只有G1 GC会有这种行为
  • 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集

年轻代GC(Minor GC)触发机制:

  • 当年轻代空间不足时,就会触发Minor GC。这里的年轻代满指的是Eden区满,Survivor满不会引发GC。(每次 Minor GC 会清理年轻代的内存。)
  • 因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
  • Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

老年代GC(Major GC/Full GC)触发机制:

  • 指发生在老年代的GC,对象从老年代消失时,我们说“Major GC”或“Full GC”发生了。
    • 出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
    • 也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC
  • Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。
  • 如果Major GC 后,内存还不足,就报OOM了。

Full GC触发机制:

  • 调用System.gc()时,系统建议执行Full GC,但是不必然执行

  • 老年代空间不足

  • 方法区空间不足

  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存

  • 由Eden区、survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大

3.5.4 OOM如何解决

  1. 要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
  2. 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。
  3. 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

3.5.5 快速分配策略:TLAB

为什么需要快速分配策略?

  • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

什么是TLAB?

JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。

3.5.6 方法区

用于存储已被虚拟机加载的类型信息、域信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等。

JVM入门到入土_第13张图片

常量池有什么?

  • 字符串
  • 符号引用(类引用、字段引用、方法引用)

3.6 永久代与元空间

HotSpot中永久代的变化:

  • jdk1.6及之前:有永久代(permanent generation)
  • jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中
  • jdk1.8及之后: 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池仍在堆

元空间1.8使用的是本地内存,不再是占用JVM的内存

静态变量存放在堆中

方法区存在GC吗?回收的都是什么

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

  • 常量池中废弃的常量

    • 字面量:文本字符串、被声明为final的常量值
    • 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
  • 不再使用的类型:类被卸载

3.7 StringTable

String的实例化方式:

  • 方式一:通过字面量定义的方式(直接存入常量池中)
  • 方式二:通过new + 构造器的方式(一个是堆空间中new结构,另一个是char[]对应的常量池中的数据:“abc”)

String intern()方法:是当前的字符对象(通过new出来的对象)可以使用intern方法从常量池中获取,如果常量池中不存在该字符串,那么就新建一个这样的字符串放到常量池中。

public class StringTest4 {
    public static void main(String[] args) {
        String s = new String("1");
        s.intern();
        String s2 = "1";
        System.out.println(s == s2);//

        String s3 = new String("1") + new String("1");
        s3.intern();
        String s4 = "11";
        System.out.println(s3 == s4);//
    }
}

JDK1.6:false;false

JDK1.7:false;true(字符串常量池从方法区搬到了堆中)

4 对象布局篇

4.1 对象的实例化

创建对象的方法

  • new
  • Class的newInstance():反射的方式,只能调用空参的构造器,权限必须是public
  • Constructor的newInstance(Xxx):反射的方式,可以调用空参、带参的构造器,权限没有要求,实用性更广
  • 使用clone():不调用任何构造器,当前类需要实现Cloneable接口,实现clone(),默认浅拷贝
  • 使用反序列化:从文件中、数据库中、网络中获取一个对象的二进制流,反序列化为内存中的对象

对象的创建过程

  1. 判断对象对应的类是否加载、链接、初始化
  2. 为对象分配内存
    • 指针碰撞
    • 空闲列表
  3. 处理并发安全问题,new对象非常频繁,需要处理分配内存空间的原子性,不冲突
    • AS ( Compare And Swap )失败重试、区域加锁:保证指针更新操作的原子性;
    • TLAB 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区,(TLAB ,Thread Local Allocation Buffer)虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
  4. 初始化分配的内存空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值
  5. 设置对象头
  6. 执行init方法进行初始化:初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

4.2 对象的内存布局

  • 对象头

    • markword:hash值,GC分代年龄,锁状态标志,线程偏向锁ID
    • 类型指针
  • 实例数据

  • padding对齐填充

JVM入门到入土_第14张图片

4.3 对象的访问定位

  • 方式一:句柄访问

JVM入门到入土_第15张图片

  • 方式二:直接使用指针访问

5 执行引擎篇

5.1 执行引擎的作用

执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。

5.2 代码编译和执行的过程

  • 过程一 javac.exe的执行:Java代码编译是由Java源码编译器来完成

JVM入门到入土_第16张图片

  • java.exe的执行:Java字节码的执行是由JVM执行引擎来完成

JVM入门到入土_第17张图片

解释器的工作机制

  • 解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。从这个角度说,java是解释语言。

  • 当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。

为什么说Java是半编译半解释型语言

Java代码的执行分类:

  • 第一种是将源代码编译成字节码文件,然后在运行时通过解释器将字节码文件转为机器码执行
  • 第二种是编译执行(直接编译成机器码)。现代虚拟机为了提高执行效率,会使用即时编译技术(JIT,Just In Time)将方法编译成机器码后再执行

现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。

JVM入门到入土_第18张图片

5.3 JIT编译器

JIT(Just In Time Compiler)编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言

为什么还保留解释器执行方式?

尽管JRockit VM中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。在此模式下,当Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。

5.4 HotSpot VM执行方式

解释器与JIT编译器并存的架构:

  • -Xint:完全采用解释器模式执行程序;
  • -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。
  • -Xmixed:采用解释器+即时编译器的混合模式共同执行程序。(默认情况)

两个编译器Client Compiler和Server Compiler

  • -client:指定Java虚拟机运行在Client模式下,并使用C1编译器;

    • C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。
    • client启动快,占用内存小,执行效率没有server快,默认情况下不进行动态编译,适用于桌面应用程序
  • -server:指定Java虚拟机运行在Server模式下,并使用C2编译器。

    • C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。
    • server启动慢,占用内存多,执行效率高,适用于服务器端应用;

6 垃圾回收篇

6.1 概述

什么是垃圾?

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

Java中垃圾回收的重点区域是?

  • 垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收。

    • 其中,Java堆是垃圾收集器的工作重点。
  • 从次数上讲

    • 频繁收集Young区
    • 较少收集Old区
    • 基本不动Perm区(或元空间)

6.2 垃圾回收算法

6.2.1 垃圾判别阶段算法(垃圾标记阶段)

  • 引用计数算法:对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。

    • 原理:对于一个对象A,只要有任何一个对象引用了A ,则A 的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A 的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
    • 优缺点:
      • 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性
      • 缺点:增加了额外空间和时间开销、无法处理循环引用的情况,出现内存泄漏(可以手动解除和使用弱引用)
  • 可达性分析算法

    • 原理:将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots,然后跟踪引用链条,如果一个对象和GC Roots之间不可达,也就是不存在引用链条,那么即可认为是可回收对象。
    • GC roots:
      • 虚拟机栈中引用的对象
      • 本地方法栈内JNI(通常说的本地方法)引用的对象
      • 类静态属性引用的对象
      • 方法区中常量引用的对象,比如字符串常量池
      • 所有被同步锁synchronized持有的对象

6.2.2 垃圾清除阶段算法

  • 标记清除算法:产生内存碎片
    • 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
    • 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

JVM入门到入土_第19张图片

  • 复制算法:将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
    • 优点:实现简单,运行高效,复制过去以后保证空间的连续性,不会出现“碎片”问题
    • 缺点:需要两倍的内存空间,需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小
    • 应用场景:新生代适合使用复制算法,因为新生代需要回收的垃圾更多,留下的存活对象更少

JVM入门到入土_第20张图片

  • 标记压缩算法:从根节点开始标记所有被引用对象,然后将所有的存活对象压缩到内存的一端,按顺序排放,最后清理边界外所有的空间

    • 优点:消除了标记清除产生碎片的缺点以及消除了复制算法的内存翻倍的缺点
    • 缺点:
      • 从效率上来说,标记-压缩算法要低于复制算法,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
      • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
      • 移动过程中,需要全程暂停用户应用程序
    • 应用场景:适用于老年代
  • 分代收集算法:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

    • 年轻代(Young Gen):区域相对老年代较小,对象生命周期短、存活率低,回收频繁。使用于使用复制算法
    • 老年代(Tenured Gen):区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
  • 增量收集算法:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

    • 优缺点:使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
  • 分区算法:G1 GC使用的算法 将整个堆空间划分成连续的不同小区间。

    • 每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
      一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

JVM入门到入土_第21张图片

6.3 相关概念

6.3.1 System.gc()和finalize()方法详解

  • System.gc():通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收
  • finalize方法:finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。
    • 不建议用finalize方法完成“非内存资源”的清理工作,但建议用于:
      • ① 清理本地对象(通过JNI创建的对象);
      • ② 作为确保某些非内存资源(如Socket、文件等)释放的一个补充:在finalize方法中显式调用其他资源释放方法。

6.3.1 内存溢出和内存泄漏

  • 内存溢出:没有空闲内存,并且垃圾收集器也无法提供更多内存。

    • 内存溢出的原因:
      • Java虚拟机的堆内存设置不够
      • 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
    • OOM前必有GC吗?通常情况下会,但是我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutOfMemoryError
  • 内存泄漏:只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”。

    • 内存泄露的八中情况:
      • 静态集合类
      • 单例模式
      • 内部类持有外部类
      • 数据库连接,网络连接,IO连接
      • 变量不合理的作用域
      • 改变Hash值
      • 缓存泄漏
      • 监听器和回调

6.3.2 STW

Stop-the-World :简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。

6.3.3 垃圾回收的并行与并发

  • 并发:多个线程在同一个cpu上在同一时间段执行,需要发生时间片的轮转
  • 并行:多个线程在不同的cpu上同一时刻执行

安全点与安全区域:

  • 安全点:程序执行时并非在所有地方都能停顿下来开始 GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint)”
  • 安全区域:安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的,也可以把 Safe Region 看做是被扩展了的 Safepoint

6.3.4 五种引用

  • 强引用—不回收:默认的引用类型,强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用— 内存不足即回收
  • 弱引用—发现即回收
  • 虚引用—对象回收跟踪:通过虚引用的get()方法取得对象时,总是null,为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
  • 终结器引用:

6.4 垃圾回收器

6.4.1 GC分类

  • 串行vs并行:

    • 串行回收器:在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
    • 并行回收器:并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式
  • 并发式与独占式:

    • 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
    • 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
  • 压缩式与非压缩式:

    • 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。再分配对象空间使用:指针碰撞
    • 非压缩式的垃圾回收器不进行这步操作。再分配对象空间使用:空闲列表
  • 年轻代与老年代:工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。

6.4.2 GC评估指标

  • 吞吐量:程序的运行时间(程序的运行时间+内存回收的时间)。
    • 吞吐量优先:单位时间内,STW的时间最短:0.2 + 0.2 = 0.4
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
    • 响应时间优先:尽可能让单次STW的时间最短,追求低延时
  • 内存占用: Java 堆区所占的内存大小。

6.4.3 垃圾回收器有哪些?

  • 七种GC组合关系

JVM入门到入土_第22张图片

  • 最为常见的组合:

    • Serial GC(新生代)与Serial Old GC(老年代)

      • 只会使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)。
      • JVM入门到入土_第23张图片
    • ParNew GC(新生代)与CMS GC(老年代)

      • ParNew收集器则是Serial收集器的多线程版本

      • JVM入门到入土_第24张图片

      • CMS(低延时):是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

      • JVM入门到入土_第25张图片

        • 使用的是标记-清除算法
        • 收集过程
          • 初始标记:主要任务仅仅只是标记出GC Roots能直接关联到的对象(第一次STW)
          • 并发标记:用户线程与垃圾收集线程一起并发运行,从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程
          • 重新标记:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(比如:由不可达变为可达对象的数据)(第二次STW)
          • 并发清除:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
        • 优缺点:
          • 优点:并发收集,低延迟
          • 缺点:会产生内存碎片,若干次GC后进行一次碎片整理,总吞吐量会降低,无法处理浮动垃圾(由于并发标记阶段会产生新的垃圾,只能到下一GC阶段清除)
    • Parallel Scanvenge GC与 Parallel Old GC(吞吐量优先)

      • HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收和”Stop the World”机制

      • 对比ParNew GC

        • Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。

        • 自适应调节策略也是Parallel Scavenge与ParNew一个重要区别

          JVM入门到入土_第26张图片

    • G1 GC:目标是在延迟可控的情况下获得尽可能高的吞吐量,担当起“全功能收集器”的重任与期望

      • 为什么叫G1(Garbage First)

        • G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代等。
        • G1 GC有计划地避免在整个Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。(垃圾优先)
      • 针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。

        JVM入门到入土_第27张图片

      • 特点:

        • 并行与并发:

          • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
          • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
        • 分代收集:

          • 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。
          • 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。和之前的各类回收器不同,它同时兼顾年轻代和老年代。
        • 空间整合:

          • 内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法
        • 可预测的停顿时间模型:能够设置最大的停顿时间

  • 如何查看默认GC

    • -XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
    • 使用命令行指令:jinfo –flag 相关垃圾回收器参数 进程ID

6.4.4 GC使用场景

JVM入门到入土_第28张图片

7 性能监控篇

7.1 JVM监控及诊断工具—命令行

  • jps(Java Process Status):查看正在运行的Java进程
  • jstat:查看JVM统计信息,用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,常用于检测垃圾回收问题以及内存泄漏问题
  • jinfo:查看虚拟机配置参数信息,也可用于调整虚拟机的配置参数,例如jinfo -flag 具体参数 PID,查看某个java进程的具体参数的值
  • jmap:导出内存映像文件&内存使用情况
    • jmap -dump pid:生成Java堆转储快照:dump文件
    • jmap -heap pid:输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等
    • jmap -histo pid:输出堆中对象的统计信息,包括类、实例数量和合计容量
    • jstack:
      • 用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。 线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合。
      • 可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack显示各个线程调用的堆栈情况。

7.2 JVM监控及诊断工具—GUI

  • jconsole:JDK自带的可视化监控工具。查看Java应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等
  • Visual VM:是一个功能强大的多合一故障诊断和性能监控的可视化工具。
  • eclipse MAT(Memory Analyzer Tool):工具是一款功能强大的Java堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。
  • Idea JProfiler:等同于上
  • Arthas:在线排查问题,无需重启;动态跟踪Java代码;实时监控JVM状态。

7.3 JVM参数

7.3.1 JVM类型选项参数

  • 类型一:标准参数 以-开头

  • 类型二:-x非标准化参数

    • JVM的JIT编译模式相关的选项

      • -Xint
      • -Xcomp
      • -Xmixed
    • 设置栈堆大小

      • -Xms 设置初始 Java 堆大小,等价于-XX:InitialHeapSize
      • -Xmx 设置最大 Java 堆大小,等价于-XX:MaxHeapSize
      • -Xss 设置 Java 线程堆栈大小,-XX:ThreadStackSize
  • 类型三 -xx非标准化参数:用于开发和调试JVM

7.3.2 常用的JVM参数选项

  • 打印设置的XX选项及值:例如-XX:+PrintFlagsFinal,表示打印出XX选项在运行程序时生效的值

  • 堆、栈、方法区等内存大小设置:

    • 栈 -Xss128k等价于-XX:ThreadStackSize=128k

      • 设置初始堆大小:-Xms3550m等价于-XX:InitialHeapSize,设置JVM初始堆内存为3550M

      • 设置最大堆大小:-Xmx3550m等价于-XX:MaxHeapSize,设置JVM最大堆内存为3550M

      • 设置年轻代:年轻代初始大小-Xmn1g等价于-XX:NewSize=1024m -XX:MaxNewSize=1024m

      • -XX:SurvivorRatio=8

      • -XX:+UseAdaptiveSizePolicy

      • -XX:NewRatio=4

      • -XX:PretenureSizeThreadshold=1024 设置让大于此阈值的对象直接分配在老年代,单位为字节

      • -XX:MaxTenuringThreshold=15 新生代每次MinorGC后,还存活的对象年龄+1,当对象的年龄大于设置的这个值时就进入老年代

    • 永久代

      • -XX:PermSize=256m(初始) -XX:MaxPermSize=256m(最大值)
    • 元空间

      • -XX:MetaspaceSize

8 性能调优篇

8.1 概述篇

  1. 生产环境中的问题:

    JVM入门到入土_第29张图片

  2. 调优基本问题:

    JVM入门到入土_第30张图片

  3. 调优监控的依据

    • 运行日志
    • 异常堆栈
    • GC日志
    • 线程快照
    • 堆转储快照
  4. 性能调优的步骤

    JVM入门到入土_第31张图片

  5. 性能评价指标

    • 响应时间(停顿时间)
    • 吞吐量
    • 内存占用

8.2 OMM案例

8.2.1 堆溢出

  1. 报错信息 java.lang.OutOfMemoryError: Java heap space
  2. 原因
    • 代码中可能存在大对象分配
    • 可能存在内存泄漏,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。
  3. 解决办法
    • 检查是否存在大对象的分配,最有可能的是大数组分配
    • 通过jmap命令,把堆内存dump下来,使用MAT等工具分析一下,检查是否存在内存泄漏的问题
    • 如果没有找到明显的内存泄漏,使用 -Xmx 加大堆内存
    • 还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性
/**
 * 模拟线上环境OOM
 */
@RequestMapping("/add")
public void addObject(){
    ArrayList<People> people = new ArrayList<>();
    while (true){
        people.add(new People());
    }
}
 
@Data
public class People {
    private String name;
    private Integer age;
    private String job;
    private String sex;
}

-XX:+PrintGCDetails -XX:MetaspaceSize=60m -XX:MaxMetaspaceSize=60m -Xss512K -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/heapdumpMeta.hprof  -XX:SurvivorRatio=8 -XX:+TraceClassLoading -XX:+TraceClassUnloading -XX:+PrintGCDateStamps  -Xms60M  -Xmx60M -Xloggc:log/gc-oomMeta.log

8.2.2 元空间溢出

  1. 元空间数据类型:用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据

  2. 报错信息:java.lang.OutOfMemoryError: Metaspace

    -XX:+PrintGCDetails -XX:MetaspaceSize=60m -XX:MaxMetaspaceSize=60m -Xss512K -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/heapdumpMeta.hprof  -XX:SurvivorRatio=8 -XX:+TraceClassLoading -XX:+TraceClassUnloading -XX:+PrintGCDateStamps  -Xms60M  -Xmx60M -Xloggc:log/gc-oomMeta.log
    
    /**
     * 案例2:模拟元空间OOM溢出
     */
    @RequestMapping("/metaSpaceOom")
    public void metaSpaceOom(){
        ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
        while (true){
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(People.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
                System.out.println("我是加强类哦,输出print之前的加强方法");
                return methodProxy.invokeSuper(o,objects);
            });
            People people = (People)enhancer.create();
            people.print();
            System.out.println(people.getClass());
            System.out.println("totalClass:" + classLoadingMXBean.getTotalLoadedClassCount());
            System.out.println("activeClass:" + classLoadingMXBean.getLoadedClassCount());
            System.out.println("unloadedClass:" + classLoadingMXBean.getUnloadedClassCount());
        }
    }
     
     
    public class People {
        public void print(){
           System.out.println("我是print本人");
        }
    }
    
  3. 原因:

    • 运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
    1. 应用长时间运行,没有重启
    2. 元空间内存设置过小
  4. 解决办法

    • 检查是否永久代空间或者元空间设置的过小
    1. 检查代码中是否存在大量的反射操作
    2. dump之后通过mat检查是否存在大量由于反射生成的代理类

8.2.3 GC overhead limit exceeded

  • 原因:系统在频繁性的做FULL GC,但是却没有回收掉多少空间,那么引起的原因可能是因为内存不足,也可能是存在内存泄漏的情况,这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出
  • 解决办法:
    • 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
    1. 添加参数 -XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
    2. dump内存,检查是否存在内存泄漏,如果没有,加大内存。

8.2.4 线程溢出

  1. 报错信息:java.lang.OutOfMemoryError : unable to create new native Thread
  2. 原因:创建了大量的线程导致的
  3. 解决办法:
    • 如果程序确实需要大量的线程,现有的设置不能达到要求,那么可以通过修改MaxProcessMemory,JVMMemory,ThreadStackSize这三个因素,来增加能创建的线程数。
    • MaxProcessMemory 使用64位操作系统
    • JVMMemory 减少JVMMemory的分配
    • ThreadStackSize 减小单个线程的栈大小

8.3 性能优化案例

  • 性能优化案例1:调整堆大小提高服务的吞吐量

  • 性能优化案例2:JVM优化之JIT优化

    • 逃逸分析:通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。

      • 全局变量赋值逃逸
      • 方法返回值逃逸
      • 实例引用发生逃逸
      • 线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量
    • 代码优化:

      • 栈上分配
      • 同步省略
      • 标量替换
  • 性能优化案例3:合理配置堆内存

    • Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。
    • 方法区(永久代 PermSize和MaxPermSize 或 元空间 MetaspaceSize 和 MaxMetaspaceSize)设置为老年代存活对象的1.2-1.5倍。
    • 年轻代Xmn的设置为老年代存活对象的1-1.5倍。
    • 老年代的内存大小设置为老年代存活对象的2-3倍。
      • 如何计算机老年代存活对象
        • 查看日志:JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。
        • 强制触发Full GC:根据多次FullGC之后的老年代内存情况来预估FullGC之后的老年代存活对象大小
  • 性能优化案例3:CPU占用很高

    • 场景:死锁
    • 解决办法:
      • 首先jps查看java进程ID
      • top -Hp 进程pid 检查当前使用异常线程的pid
      • 把线程pid变为16进制如 31695 —>7bcf 然后得到0x7bcf
      • jstack 进程的pid | grep -A20 0x7bcf 得到相关进程的代码
  • 性能优化案例4:调整垃圾回收器提高服务的吞吐量
    .OutOfMemoryError : unable to create new native Thread

  1. 原因:创建了大量的线程导致的
  2. 解决办法:
    • 如果程序确实需要大量的线程,现有的设置不能达到要求,那么可以通过修改MaxProcessMemory,JVMMemory,ThreadStackSize这三个因素,来增加能创建的线程数。
    • MaxProcessMemory 使用64位操作系统
    • JVMMemory 减少JVMMemory的分配
    • ThreadStackSize 减小单个线程的栈大小

8.3 性能优化案例

  • 性能优化案例1:调整堆大小提高服务的吞吐量

  • 性能优化案例2:JVM优化之JIT优化

    • 逃逸分析:通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。

      • 全局变量赋值逃逸
      • 方法返回值逃逸
      • 实例引用发生逃逸
      • 线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量
    • 代码优化:

      • 栈上分配
      • 同步省略
      • 标量替换
  • 性能优化案例3:合理配置堆内存

    • Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。
    • 方法区(永久代 PermSize和MaxPermSize 或 元空间 MetaspaceSize 和 MaxMetaspaceSize)设置为老年代存活对象的1.2-1.5倍。
    • 年轻代Xmn的设置为老年代存活对象的1-1.5倍。
    • 老年代的内存大小设置为老年代存活对象的2-3倍。
      • 如何计算机老年代存活对象
        • 查看日志:JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。
        • 强制触发Full GC:根据多次FullGC之后的老年代内存情况来预估FullGC之后的老年代存活对象大小
  • 性能优化案例3:CPU占用很高

    • 场景:死锁
    • 解决办法:
      • 首先jps查看java进程ID
      • top -Hp 进程pid 检查当前使用异常线程的pid
      • 把线程pid变为16进制如 31695 —>7bcf 然后得到0x7bcf
      • jstack 进程的pid | grep -A20 0x7bcf 得到相关进程的代码
  • 性能优化案例4:调整垃圾回收器提高服务的吞吐量

你可能感兴趣的:(Java,jvm,java)