JVM

JAVA

# 一、JVM

# JVM生命周期(类加载过程)

JAVA即是编译型语言,又是解释型语言。

程序源代码(java文件)经过编译器进行编译处理,转换为JAVA字节码文件(class文件)。

JAVA虚拟机(JVM),它的作用是将字节码进行解释和运行——解释为计算机识别的机器码(二进制01010010),再在计算机上运行。

在JVM读取到字节码文件后,调用到哪个类,才会加载该类。经由类加载器(ClassLoader)进行加载,在内存中生成一个代表该类的class对象作为访问入口。

在加载过程中,需要经过验证、准备、解析、初始化后,再进行类的使用。

  • 验证:对字节码文件进行正确性校验。
  • 准备:给类的静态变量分配内存,并赋值默认值。
  • 解析:将符号引用替换为直接引用。例如把静态方法替换为指向数据所存内存的指针。
  • 初始化:对类的静态变量赋初始值,并执行静态代码块。

最后当类方法调用结束,进行卸载。经由垃圾回收器进行垃圾回收,回收那些不在有任何引用的内存。

# 类加载器ClassLoader

JVM有三种预定义类型的类加载器:

  • 引导类加载器(Bootstap):由C++实现,负责加载支撑JVM运行的核心类库(位于JRE的lib目录下的所有包)。
  • 扩展类加载器(ExtClassLoader):负责加载支撑JVM运行的扩展包(位于JRE的lib目录下的ext目录)。
  • 应用程序类加载器(AppClassLoader):负责加载classpath路径下的类包。
  • 自定义类加载器:可以自定义,创建类加载器。需要继承(extends) java.lang.ClassLoader类,重写findClass()空方法。

类加载器之间的关系:

  • 自定义类加载器的父加载器 ——》 是 AppClassLoader
  • AppClassLoader的父加载器(parent属性)——》 是 ExtClassLoader;
  • ExtClassLoader的父加载器 ——》 是 null(即引导类加载器)

# 双亲委派机制

原理:

ClassLoader类的loadClass()方法,实现了双亲委派机制。简单说就是在加载类时,先向上找parent 进行加载,没有再向下自己找。

当调用classLoader.loadClass方法进行类的加载,首先由应用程序类加载器(AppClassLoader)进行加载。

先判断是否加载过该类,若加载过直接返回;若无则判断是否有父加载器parent。

即AppClassLoader 向上委派 扩展类加载器(ExtClassLoader)进行加载。

而ExtClassLoader 再向上委托 引导类加载器进行加载。

若引导类加载器在自己的类路径下找到该类,则直接返回该类;若无则再向下回退至ExtClassLoader 进行查找。

若ExtClassLoader 在自己的类路径下找到该类,则直接返回该类;若无则再向下回退至AppClassLoader进行查找。

目的:

  1. 设计双亲委派机制,可以保证沙箱安全机制,防止核心API库被篡改;
  2. 可以避免类的重复加载,保证被加载类的唯一性。(父类若加载直接返回,不用子类再加载)

打破双亲委派机制:

重写类加载方法loadClass()方法,实现自己的加载逻辑,不再委派给双亲进行加载。

# 对象的创建过程

对象在内存中存储的布局分为了三块区域:对象头、实例数据、对齐填充。

对象的创建过程:

类加载检查——》分配内存——》初始化——》设置对象头——》执行init方法

  • 类加载检查:当执行new指令,先去检查该类是否已被加载、解析、初始化;
  • 分配内存:JVM为新生对象从堆中划分分配内存;
  • 初始化:将分配到的内存空间初始化为零值;
  • 设置对象头:存储类的元数据、对象的哈希码、GC分代年龄等对象自身的运行时数据;
  • 执行init方法:为属性赋值和执行构造方法。

# 对象内存分配

先判断是否需要栈内分配,通过逃逸分析进行判断;

然后判断对象是否属于大对象

  • 若是,直接分配至堆内存的老年代区域;
  • 若否,在堆内存的年轻代的Eden区分配内存空间。

# JVM内存模型

Metaspace 元空间

Stack 栈

heap 堆

Eden Space Eden区

Surviver 0 S0区

Surviver 1 S1区

Old Gen 老年代

Perm Gen 永久代 (Permanent Generation)

  1. 类装载子系统

  2. 字节码执行引擎

  3. 运行时数据区

    运行时数据区分为:方法区(元空间)、线程栈、本地方法栈、堆、程序计数器

    • 方法区

      方法区(元空间),存储常量、静态变量、类信息。

    • 每一个方法分配一块内存空间——栈帧。

    • 栈帧

      局部变量表、操作数栈、动态链接、方法出口;

    • 年轻代老年代1:2

    ​ 年轻代:Eden区Surviver区S0区S1区)(8:1:1

# 垃圾收集算法

  • 标记-复制算法(年轻代)
  • 标记-清除算法(老年代、CMS)
  • 标记-整理算法(老年代、parNew)

# 垃圾回收器

  • 年轻代:Serial、ParNew、Parallel
  • 老年代:Serial Old 、Parallel OldCMS

# 栈内存溢出

栈内存可以分为虚拟机栈(VM Stack)和本地方法栈(Native Method Stack),除了它们分别用于执行Java方法(字节码)和本地方法,其余部分原理是类似的(以虚拟机栈为例说明)。Java虚拟机栈是线程私有的,当线程中方法被调度时,虚拟机会创建用于保存局部变量表、操作数栈、动态连接和方法出口等信息的栈帧(Stack Frame)。

具体来说,当线程执行某个方法时,JVM会创建栈帧并压栈,此时刚压栈的栈帧就成为了当前栈帧。如果该方法进行递归调用时,JVM每次都会将保存了当前方法数据的栈帧压栈,每次栈帧中的数据都是对当前方法数据的一份拷贝。如果递归的次数足够多,多到栈中栈帧所使用的内存超出了栈内存的最大容量,此时JVM就会抛出StackOverflowError

# 堆内存溢出

堆内存的唯一作用就是存放数组和对象实例,即通过new指令创建的对象,包括数组和引用类型。堆内存溢出又分为两种情况:

  • 堆内存溢出:当堆中对象实例所占的内存空间超出了堆内存的最大容量,JVM就会抛出OutOfMemoryError:java heap space异常。
  • 堆内存泄露:当堆中一些对象不再被引用但垃圾回收器无法识别时,这些未使用的对象就会在堆内存空间中无限期存在,不断的堆积就会造成内存泄漏。

如果是因为堆内存空间太小,可以通过改变-Xmx来进行调整,或者分析程序中对象的生命周期和存储结构等信息进行调整;

如果发生了内存泄漏,则可以先找出导致泄漏发生的对象是如何被GC ROOT引用起来的,然后通过分析引用链找到发生泄漏的地方。

# 二、JVM参数设置

# 2.1 参数使用

Spring Boot程序的JVM参数设置格式(或者在Tomcat启动直接加在bin目录下catalina.sh文件里):

java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐jar xxx-xxx‐server.jar
1

# 2.2 栈参数设置

参数 默认值 作用 备注
-Xss 默认1M 栈的内存大小 -Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多。

# 2.3 堆参数设置

参数 作用
-Xms: 初始堆内存大小
-Xmx: 最大堆内存
-Xmn: 年轻代大小
-XX:NewSize=n 设置年轻代大小
-XX:MaxPermSize=n 设置持久代大小
-XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值。
如:n=3,表示Eden:Survivor=3:2,即(Eden : S0 : S1 = 3:1:1)

# 2.4 方法区参数设置

参数 默认值 作用
-XX:MetaspaceSize=N 以字节为单位,默认是21M。 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小),
-XX:MaxMetaspaceSize=N 默认是-1,即不限制, 或者说只受限于本地内存大小。 设置元空间最大值,

# 2.5 收集器参数设置

参数 作用
-XX:+UseSerialGC 设置串行收集器
-XX:+UseParallelGC 设置并行收集器
-XX:+UseParalledlOldGC 设置并行年老代收集器
-XX:+UseConcMarkSweepGC 设置并发收集器

# 2.6 垃圾回收设置

# 2.7 其他参数设置

参数 作用
-XX:+PrintFlagsInitial 打印出所有参数选项的默认值
-XX:+PrintFlagsFinal 打印出所有参数选项在运行程序时生效的值
-XX:+PrintGCDetails 打印出GC详情日志信息
-XX:+HeapDumpOnOutOfMemoryError 设置开启内存溢出自动导出dump文件
-XX:HeapDumpPath=D:\jvm.dump 指定导出dump文件位置

# 2.8 JVM参数默认值

# 堆内存分配

  • JVM初始分配的内存由-Xms指定,默认是物理内存的1/64
  • JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4

内存调整

默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;

空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。

因此服务器一般设置-Xms、-Xmx相等,以避免在每次GC后调整堆的大小。

# 非堆内存分配

  • JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64
  • -XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4

# JVM最大内存

首先JVM内存限制于实际的最大物理内存(废话!呵呵),假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。

  • 32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),
  • 而64bit以上的处理器就不会有限制了。

# 2.9 JVM参数设置建议

前置条件:服务器资源为8G内存。

参数 建议优化值 默认值 作用

# 2.10 查看本机参数配置

# 查看本机最大JVM大小

java -XshowSettings:vm
1

# 查看本机最大metaspace

java -XX:+PrintFlagsInitial | findstr MetaspaceSize
1

# 三、JDK命令

# jps

JDK提供的一个可以列出正在运行的Java虚拟机的进程信息的命令行工具。

它可以显示Java虚拟机进程的执行主类(Main Class,main()函数所在的类)名称、本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)等信息。

jps
1

# jmap

此命令可以用来查看内存信息,实例个数以及占用内存大小。

  1. 查看堆内存信息

    jmap -heap 进程号
    
    1

# jstack

  1. 可以用jstack加进程id查找死锁;

  2. 可以用jstack找出占用cpu最高的线程堆栈信息;

    
    
    1

# jinfo

查看正在运行的Java应用程序的扩展参数。

  1. 查看jvm的参数

    jinfo -flags 进程号
    
    1
  2. 查看java系统参数

    jinfo -sysprops 进程号
    
    1

# jstat

jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。

命令的格式如下:

jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]
1
  1. 垃圾回收统计

    jstat -gc pid 最常用,可以评估程序内存使用及GC压力整体情况。

    jstat -gc pid
    
    1

    展示项说明:

    S0C:第一个幸存区的大小,单位KB

    S1C:第二个幸存区的大小

    S0U:第一个幸存区的使用大小S1U:第二个幸存区的使用大小

    EC:伊甸园区的大小

    EU:伊甸园区的使用大小

    OC:老年代大小

    OU:老年代使用大小

    MC:方法区大小(元空间)

    MU:方法区使用大小

    CCSC:压缩类空间大小

    CCSU:压缩类空间使用大小

    YGC:年轻代垃圾回收次数

    YGCT:年轻代垃圾回收消耗时间,单位s

    FGC:老年代垃圾回收次数

    FGCT:老年代垃圾回收消耗时间,单位s

    GCT:垃圾回收消耗总时间,单位s

  2. 堆内存统计

    jstat -gccapacity pid
    
    1

    展示项说明:

    NGCMN:新生代最小容量

    NGCMX:新生代最大容量

    NGC:当前新生代容量

    S0C:第一个幸存区大小

    S1C:第二个幸存区的大小

    EC:伊甸园区的大小

    OGCMN:老年代最小容量

    OGCMX:老年代最大容量

    OGC:当前老年代大小

    OC:当前老年代大小

    MCMN:最小元数据容量

    MCMX:最大元数据容量

    MC:当前元数据空间大小

    CCSMN:最小压缩类空间大小

    CCSMX:最大压缩类空间大小

    CCSC:当前压缩类空间大小

    YGC:年轻代gc次数

    FGC:老年代GC次数

  3. 新生代垃圾回收统计

    jstat -gcnew pid
    
    1

    展示项说明:

    S0C:第一个幸存区的大小

    S1C:第二个幸存区的大小

    S0U:第一个幸存区的使用大小

    S1U:第二个幸存区的使用大小

    TT:对象在新生代存活的次数

    MTT:对象在新生代存活的最大次数

    DSS:期望的幸存区大小

    EC:伊甸园区的大小

    EU:伊甸园区的使用大小

    YGC:年轻代垃圾回收次数

    YGCT:年轻代垃圾回收消耗时间

  4. 新生代内存统计

    jstat -gcnewcapacity pid
    
    1

    展示项说明:

    NGCMN:新生代最小容量

    NGCMX:新生代最大容量

    NGC:当前新生代容量

    S0CMX:最大幸存1区大小

    S0C:当前幸存1区大小

    S1CMX:最大幸存2区大小

    S1C:当前幸存2区大小

    ECMX:最大伊甸园区大小

    EC:当前伊甸园区大小

    YGC:年轻代垃圾回收次数

    FGC:老年代回收次数

  5. 老年代垃圾回收统计

    jstat -gcold pid
    
    1

    展示项说明:

    MC:方法区大小

    MU:方法区使用大小

    CCSC:压缩类空间大小

    CCSU:压缩类空间使用大小

    OC:老年代大小

    OU:老年代使用大小

    YGC:年轻代垃圾回收次数

    FGC:老年代垃圾回收次数

    FGCT:老年代垃圾回收消耗时间

    GCT:垃圾回收消耗总时间

  6. 老年代内存统计

    jstat -gcoldcapacity pid
    
    1

    展示项说明:

    OGCMN:老年代最小容量

    OGCMX:老年代最大容量

    OGC:当前老年代大小

    OC:老年代大小

    YGC:年轻代垃圾回收次数

    FGC:老年代垃圾回收次数

    FGCT:老年代垃圾回收消耗时间

    GCT:垃圾回收消耗总时间

  7. 元数据空间统计

    jstat -gcmetacapacity pid
    
    1

    展示项说明:

    MCMN:最小元数据容量

    MCMX:最大元数据容量

    MC:当前元数据空间大小

    CCSMN:最小压缩类空间大小

    CCSMX:最大压缩类空间大小

    CCSC:当前压缩类空间大小

    YGC:年轻代垃圾回收次数

    FGC:老年代垃圾回收次数

    FGCT:老年代垃圾回收消耗时间

    GCT:垃圾回收消耗总时间

# 四、jvisualvm

jdk自带的jvisualvm,

# jvisualvm使用

# visual GC插件安装

visual GC使用

# 五、Arthas

Arthas 官方文档:https://alibaba.github.io/arthas (opens new window)

ArthasAlibaba 在 2018 年 9 月开源的 Java 诊断工具

支持 JDK6+, 采用命令行交互模式,可以方便的定位和诊断线上程序运行问题。

# Arthas使用

下载

# github下载arthas
wget https://alibaba.github.io/arthas/arthas‐boot.jar

# 或者 Gitee 下载
wget https://arthas.gitee.io/arthas‐boot.jar
1
2
3
4
5

启动

用java -jar运行即可,可以识别机器上所有Java进程。

执行命令

输入dashboard可以查看整个进程的运行情况,线程、内存、GC、运行环境信息

输入thread可以查看线程详细情况

输入 thread加上线程ID 可以查看线程堆栈

输入 thread -b 可以查看线程死锁

输入 jad加类的全名 可以反编译,这样可以方便我们查看线上代码是否是正确的版本

使用 ognl 命令可以查看线上系统变量的值,甚至可以修改变量的值

更多命令使用可以用help命令查看,或查看文档:https://alibaba.github.io/arthas/commands.html#arthas (opens new window)

# 六、JVM调优方案

# OOM案例

项目描述

编写一个程序,用于进行文件转换(json数据-提取-转换为csv文件)。

步骤

  1. 第一步:读取文件;
  2. 第二步:遍历文件,逐行获取JSON/String数据;
  3. 第三步:筛选,提取所需核心字段数据;
  4. 第四步:转储CSV格式,导出CSV文件;

备注

  1. 每个文件大小大约在2G左右;
  2. 每个文件数据在57万条左右;

抛出异常OutOfMemoryError

Java heap space] with root causejava.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:596)
	at java.lang.StringBuffer.append(StringBuffer.java:367)
	at java.io.BufferedReader.readLine(BufferedReader.java:358)
	at java.io.BufferedReader.readLine(BufferedReader.java:389)
	at com.xxx.tool.util.MyFileUtils.getList(MyFileUtils.java:80)
	at com.xxx.tool.service.JsonTransService.readFile(JsonTransService.java:73)
	at com.xxx.tool.service.JsonTransService.toTrans(JsonTransService.java:49)
	at com.xxx.tool.controller.IndexController.toTrans(IndexController.java:41)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
1
2
3
4
5
6
7
8
9
10
11
12

分析OOM异常原因

  1. 文件内容过大(2G),一次性遍历所有数据,内存过大;而堆内存分配空间不足,导致OOM。

  2. 若已配置了堆内存(-Xmx -Xms -Xmn),则根据生成的错误日志、分析堆内存,查看代码内容进行优化。

    在jar启动程序添加参数,指定发生OOM时输出dump日志;

    -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\jvm.dump
    
    1

    jvisualvm分析dump文件;

    多方结合,判断是在进行ArrayList添加时,文件内容过大导致内存溢出;

解决方案

  1. 在jar包启动添加JVM参数配置(针对主机物理内存8G)。

    java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\jvm.dump -Dfile.encoding=UTF-8 -jar ./jar/xxx-xxx-xxx-0.1.0-SNAPSHOT.jar
    
    1
  2. 当读取大文件时,不要进行多层封装代码存储数据,即用即消。

    就是说不要把文件的所有行一次性地放入内存中,而是在逐行遍历文件时,直接处理相应数据,处理完毕则释放。

# 九、JVM异常汇总

Java中内存溢出常见于如下的几种情形:

  • 栈内存溢出(StackOverflowError)
  • 堆内存溢出(OutOfMemoryError:java heap space)
  • 永久代溢出(OutOfMemoryError:PermGen sapce)

不同的内存溢出错误可能会发生在内存模型的不同区域,因此,我们需要根据出现错误的代码具体分析来找出可能导致错误发生的地方,并想办法进行解决。

# 1 OutOfMemoryError

新产生的对象最初分配在新生代,新生代满后会进行一次 Minor GC ,如果 Minor GC 后空间不足会把该对象和新生代满足条件的对象放入老年代,老年代空间不足时会进行 Full GC ,之后如果空间还不足以存放新对象则抛出 OutOfMemoryError 异常。

常见原因

  1. 内存中加载的数据过多,如一次从数据库中取出过多数据;
  2. 集合对对象引用过多且使用完后没有清空;
  3. 代码中存在死循环或循环产生过多重复对象;
  4. 堆内存分配不合理;

解决

  • 如果是因为堆内存空间太小,可以通过改变-Xmx来进行调整,或者分析程序中对象的生命周期和存储结构等信息进行调整;
  • 如果发生了内存泄漏,则可以先找出导致泄漏发生的对象是如何被GC ROOT引用起来的,然后通过分析引用链找到发生泄漏的地方。

例如,通过-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError来设置堆内存大小为20M,并且设定不支持自动扩展,同时使用-XX:+HeapDumpOnOutOfMemoryError实现当异常抛出时Dump出当前的内存堆转储快照进行分析。

# 2 StackOverflowError

原因

如果线程请求的栈深度大于虚拟机所允许的最大深度, 将抛出StackOverflowError异常。(递归)局部变量表空间、创建线程导致内存溢出异常。

解决

栈容量只能由-Xss参数来设定;

总之,不论是因为栈帧太大还是栈内存太小,当新的栈帧内存无法被分配时,JVM就会抛出StackOverFlowError。通常栈内存可以通过设置-Xss参数来改变大小。