20210615 JVM 优化 - 拉勾教育
JVM 优化
JVM回顾
JVM 是 Java Virtual Machine ( Java 虚拟机)的缩写, JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
主流虚拟机名称 | 介绍 |
---|---|
HotSpot | Oracle/Sun JDK 和 OpenJDK 都使用 HotSpot VM 的相同核心 |
J9 | J9 是 IBM 开发的高度模块化的 JVM |
JRockit | JRockit 与 HotSpot 同属于 Oracle ,目前为止 Oracle 一直在推进 HotSpot 与 JRockit 两款各有优势的虚拟机进行融合互补 |
Zing | 由 Azul Systems 根据 HostPot 为基础改进的高性能低延迟的 JVM |
Dalvik | Android 上的 Dalvik 虽然名字不叫 JVM ,但骨子里就是不折不扣的 JVM |
JVM 与操作系统
为什么要在程序和操作系统中间添加一个 JVM
Java 是一门抽象程度特别高的语言,提供了自动内存管理等一系列的特性。这些特性直接在操作系统上实现是不太可能的,所以就需要 JVM 进行一番转换。
有了 JVM 这个抽象层之后, Java 就可以实现跨平台了。 JVM 只需要保证能够正确执行 .class 文件,就可以运行在诸如 Linux 、 Windows 、 MacOS 等平台上了。
应用程序、JVM、操作系统之间的关系
我们用一句话概括 JVM 与操作系统之间的关系:JVM 上承开发语言,下接操作系统,它的中间接口就是字节码。
JVM、JRE、JDK 的关系
JVM 是 Java 程序能够运行的核心。但是需要注意, JVM 自己什么也干不了,你需要给它提供生产原料( .class 文件) 。
仅仅是 JVM ,是无法完成一次编译,处处运行的。它需要一个基本的类库,比如怎么操作文件、怎么连接网络等。而 Java 体系很慷慨,会一次性将 JVM 运行所需的类库都传递给它。 JVM 标准加上实现的一大堆基础类库,就组成了 Java 的运行时环境,也就是我们常说的 JRE ( Java Runtime Environment )
对于 JDK 来说,就更庞大了一些。除了 JRE , JDK 还提供了一些非常好用的小工具,比如 Javac 、 Java 、 jar 等。它是 Java 开发的核心,让外行也可以炼剑! JDK(Java Development Kit)
JVM 、 JRE 、 JDK 它们三者之间的关系,可以用一个包含关系表示。
Java 虚拟机规范和 Java 语言规范的关系
左半部分是 Java 虚拟机规范,其实就是为输入和执行字节码提供一个运行环境。右半部分是我们常说的 Java 语法规范,比如 switch 、 for 、泛型、 lambda 等相关的程序,最终都会编译成字节码。而连接左右两部分的桥梁依然是 Java 的字节码。
如果 .class
文件的规格是不变的,这两部分是可以独立进行优化的。但 Java 也会偶尔扩充一下 .class
文件的格式,增加一些字节码指令,以便支持更多的特性。
我们可以把 Java 虚拟机可以看作是一台抽象的计算机,它有自己的指令集以及各种运行时内存区域
我们简单看一下一个 Java 程序的执行过程,它到底是如何运行起来的
public class HelloWorld {
public static void main(String[] args) throws InterruptedException {
System.out.println("Hello World");
}
}
javac HelloWorld.java
javap -c HelloWorld.class
public class com.lagou.unit.HelloWorld {
public com.lagou.unit.HelloWorld();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
Java 虚拟机采用基于栈的架构,其指令由操作码和操作数组成。这些 字节码指令 ,就叫作 opcode。其中,getstatic
、ldc
、invokevirtual
、return
等,就是 opcode,可以看到是比较容易理解的。
JVM 就是靠解析这些 opcode 和操作数来完成程序的执行的。当我们使用 Java 命令运行 .class
文件的时候,实际上就相当于启动了一个 JVM 进程。
然后 JVM 会翻译这些字节码,它有两种执行方式。常见的就是 解释执行 ,将 opcode + 操作数翻译成机器代码;另外一种执行方式就是 JIT,也就是我们常说的 即时编译 ,它会在一定条件下将字节码编译成机器码之后再执行。
Java 虚拟机的内存管理
JVM 整体架构
根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
名 称 | 特征 | 作用 | 配置参数 | 异常 |
---|---|---|---|---|
程 序 计 数 器 | 占用内存小,线程私有,生命周期与线程相同 | 大致为字节码行号指示器 | 无 | 无 |
虚 拟 机 栈 | 线程私有,生命周期与线程相 同,使用连续的内存空间 | Java 方法执行的内存模 型,存储局部变量表、 操作栈、动态链接、方 法出口等信息 | -Xss |
StackOverflowError OutOfMemoryError |
堆 | 线程共享,生命周期与虚拟机相 同,可以不使用连续的内存地址 | 保存对象实例,所有对 象实例(包括数组)都 要在堆上分配 | -Xms -Xsx -Xmn |
OutOfMemoryError |
方 法 区 | 线程共享,生命周期与虚拟机相 同,可以不使用连续的内存地址 | 存储已被虚拟机加载的 类信息、常量、静态变 量、即时编译器编译后 的代码等数据 | -XX:PermSize:16M -XX:MaxPermSize64M - XX:MetaspaceSize=16M -XX:MaxMetaspaceSize=64M |
OutOfMemoryError |
本 地 方 法 栈 | 线程私有 | 为虚拟机使用到的 Native 方法服务 | 无 | StackOverflowError OutOfMemoryError |
JVM 分为五大模块: 类装载器子系统 、 运行时数据区 、 执行引擎 、 本地方法接口 和 垃圾收集模块 。
JVM 运行时内存
Java 7 和 Java8 内存结构的不同主要体现在方法区的实现
方法区是 Java 虚拟机规范中定义的一种概念上的区域,不同的厂商可以对虚拟机进行不同的实现。
我们通常使用的 Java SE 都是由 Oracle JDK 和 OpenJDK 所提供,这也是应用最广泛的版本。而该版本使用的 VM 就是 HotSpot VM 。通常情况下,我们所讲的 Java 虚拟机指的就是 HotSpot 的版本。
JDK 7 和 JDK 8 变化小结
对于 Java 8 , HotSpots 取消了永久代,那么是不是就没有方法区了呢?当然不是,方法区只是一个规范,只不过它的实现变了。
在 Java 8 中,元空间( Metaspace )登上舞台,方法区存在于元空间( Metaspace )。同时,元空间不再与堆连续,而且是存在于本地内存( Native memory )。
方法区 Java 8 之后的变化:
- 移除了永久代(PermGen),替换为元空间(Metaspace)
- 永久代中的 class metadata(类元信息)转移到了 native memory(本地内存,而不是虚拟机)
- 永久代中的 interned Strings(字符串常量池) 和 class static variables(类静态变量)转移到了 Java heap
- 永久代参数(PermSize MaxPermSize)-> 元空间参数(MetaspaceSize MaxMetaspaceSize)
Java 8 为什么要将永久代替换成 Metaspace ?
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
- Oracle 可能会将 HotSpot 与 JRockit 合二为一, JRockit 没有所谓的永久代。
PC 程序计数器
程序计数器( Program Counter Register ):也叫 PC 寄存器,是一块较小的内存空间,它可以看做是当前线程所执行的 字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
虚拟机的 pc 寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址。当虚拟机正在执行的方法是一个本地( native )方法的时候, JVM 的 pc 寄存器存储的值是 undefined
。
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError
情况的区域
虚拟机栈
Java 虚拟机栈( Java Virtual Machine Stacks )也是线程私有的,即生命周期和线程相同。 Java 虚拟机栈和线程同时创建,用于存储栈帧。每个方法在执行时都会创建一个栈帧( Stack Frame ),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
栈帧( Stack Frame )是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
设置虚拟机栈的大小:-Xss
为 jvm 启动的每个线程分配的内存大小
局部变量表( Local Variable Table )是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括 8 种基本数据类型、对象引用( reference 类型)和 returnAddress 类型(指向一条字节码指令的地址)。其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间( Slot ),其余的数据类型只占用 1 个。
操作数栈( Operand Stack )也称作操作栈,是一个后入先出栈( LIFO )。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈 / 入栈操作。
Java 虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的 动态链接( Dynamic Linking )。动态链接的作用:将符号引用转换成直接引用。
方法返回地址 存放调用该方法的 PC 寄存器的值。一个方法的结束,有两种方式:正常地执行完成,出现未处理的异常非正常的退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
本地方法栈
本地方法栈( Native Method Stacks ) 与虚拟机栈所发挥的作用是非常相似的, 其区别只是虚拟机栈为虚拟机执行 Java 方法(也就是字节码) 服务, 而本地方法栈则是为虚拟机使用到的本地( Native ) 方法服务。
堆
对于 Java 应用程序来说, Java 堆( Java Heap ) 是虚拟机所管理的内存中最大的一块。 Java 堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例, Java 世界里“几乎”所有的对象实例都在这里分配内存。
“几乎”是指从实现角度来看, 随着 Java 语 言的发展, 现在已经能看到些许迹象表明日后可能出现值类型的支持, 即使只考虑现在, 由于即时编译技术的进步, 尤其是逃逸分析技术的日渐强大, 栈上分配、 标量替换优化手段已经导致一些微妙的变化悄然发生, 所以说 Java 对象实例都分配在堆上也渐渐变得不是那么绝对了。
堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer ( TLAB )
Java 堆是垃圾收集器管理的主要区域
很多时候 Java 堆也被称为 GC 堆 ( Garbage Collected Heap )。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆还可以细分为:新生代和老年代;新生代又可以分为: Eden 空间、 FromSurvivor 空间、 To Survivor 空间。
Java 堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过 -Xms
和 -Xmx
控制)
设置堆空间大小:-Xmx
/ -Xms
堆的分类
JVM 中存储 Java 对象可以被分为两类:
- 年轻代( Young Gen ):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成 1 个 Eden Space 和 2 个 Suvivor Space ( from 和 to )。
- 年老代( Tenured Gen ):年老代主要存放 JVM 认为生命周期比较长的对象(经过几次的 Young Gen 的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。
配置新生代和老年代堆结构占比:默认 -XX:NewRatio=2
, 标识新生代占 1 , 老年代占 2 ,新生代占整个堆的 1/3 ,Eden 空间和另外两个 Survivor 空间占比默认分别为 8:1:1
,可以通过操作选项 -XX:SurvivorRatio
调整这个空间比例。 比如 -XX:SurvivorRatio=8
无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10
( 即 90% )的新生代空间。
对象分配过程
- new 的对象先放在伊甸园区。该区域有大小限制
- 当伊甸园区域填满时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园预期进行垃圾回收(Minor GC),将伊甸园区域中不再被其他对象引用的对象进行销毁,再加载新的对象放到伊甸园区
- 然后将伊甸园区中的剩余对象移动到幸存者 0 区
- 如果再次触发垃圾回收,此时上次幸存下来的放在幸存者 0 区的,如果没有回收,就会放到幸存者 1 区
- 如果再次经历垃圾回收,此时会重新返回幸存者 0 区,接着再去幸存者 1 区。
- 如果累计次数到达默认的
15
次,这会进入养老区。可以通过设置参数,调整阈值-XX:MaxTenuringThreshold=N
- 养老区内存不足时,会再次触发 GC ( Major GC ) 进行养老区的内存清理
- 如果养老区执行了 Major GC 后仍然没有办法进行对象的保存,就会报 OOM 异常
堆 GC
Java 中的堆也是 GC 收集垃圾的主要区域。 GC 分为两种:一种是部分收集( Partial GC )另一类是整堆收集( Full GC )
- 部分收集: 不是完整收集 Java 堆的的收集器,它又分为:
- 新生代收集(Minor GC / Young GC): 只是新生代的垃圾收集
- 老年代收集 (Major GC / Old GC): 只是老年代的垃圾收集 (CMS GC 单独回收老年代)
- 混合收集(Mixed GC): 收集整个新生代及老年代的垃圾收集 ( G1 GC 会混合回收, region 区域回收)
- 整堆收集( Full GC ):收集整个 Java 堆和方法区的垃圾收集器
年轻代 GC 触发条件:
- 年轻代空间不足,就会触发 Minor GC , 这里年轻代指的是 Eden 代满, Survivor 代满不会引发 GC
- Minor GC 会引发 STW ( stop the world ) ,暂停其他用户的线程,等垃圾回收接收,用户的线程才恢复 .
老年代 GC ( Major GC )触发机制:
- 老年代空间不足时,会尝试触发 MinorGC. 如果空间还是不足,则触发 Major GC
- 如果 Major GC , 内存仍然不足,则报错 OOM
- Major GC 的速度比 Minor GC 慢 10 倍以上
Full GC 触发机制:
- 调用
System.gc()
, 系统会执行 Full GC ,不是立即执行 - 老年代空间不足
- 方法区空间不足
- 通过 Minor GC 进入老年代平均大小大于老年代可用内存
元空间
元空间(Metaspace) 与永久代(PermGen)有什么不同的:
- 存储位置不同:永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。
- 存储内容不同:在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。
Metaspace 相关参数
-XX:MetaspaceSize
,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时 GC 会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize
时,适当提高该值。-XX:MaxMetaspaceSize
,最大空间,默认是没有限制的。如果没有使用该参数来设置类的元数据的大小,其最大可利用空间是整个系统内存的可用空间。 JVM 也可以增加本地内存空间来满足类元数据信息的存储。但是如果没有设置最大值,则可能存在 bug 导致 Metaspace 的空间在不停的扩展,会导致机器的内存不足;进而可能出现 swap 内存被耗尽;最终导致进程直接被系统直接 kill 掉。如果设置了该参数,当 Metaspace 剩余空间不足,会抛出:java.lang.OutOfMemoryError:Metaspace space
-XX:MinMetaspaceFreeRatio
,在 GC 之后,最小的 Metaspace 剩余空间容量的百分比,减少为分配空间所导致的垃圾收集-XX:MaxMetaspaceFreeRatio
,在 GC 之后,最大的 Metaspace 剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
方法区
方法区( Method Area ) 与 Java 堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载 的类型信息、常量、 静态变量、 即时编译器编译后的代码缓存等数据。
《 Java 虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但些简单的实现可能不会选择去进行垃圾收集或者进行压缩”。对 HotSpot 而言,方法区还有一个别名叫做 Non-Heap (非堆),的就是要和堆分开。
元空间、永久代是方法区具体的落地实现。方法区看作是一块独立于 Java 堆的内存空间,它主要是用来存储所加载的类信息的
创建对象各数据区域的声明:
方法区中存储的内容:
- 类型信息(域信息、方法信息)
- 运行时常量池
运行时常量池
常量池 vs 运行时常量池:
- 字节码文件中,内部包含了常量池
- 方法区中,内部包含了运行时常量池
- 常量池:存放编译期间生成的各种字面量与符号引用
- 运行时常量池:常量池表在运行时的表现形式
编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过 ClassLoader 将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。
理解为字节码中的常量池 Constant pool 只是文件信息,它想要执行就必须加载到内存中。而 Java 程序是靠 JVM ,更具体的来说是 JVM 的执行引擎来解释执行的。执行引擎在运行时常量池中取数据,被加载的字节码常量池中的信息是放到了方法区的运行时常量池中。它们不是一个概念,存放的位置是不同的。一个在字节码文件中,一个在方法区中。
直接内存
直接内存( Direct Memory ) 并不是虚拟机运行时数据区的一部分。在 JDK 1.4 中新加入了 NIO ( New Input/Output ) 类, 引入了一种基于通道( Channel ) 与缓冲区 ( Buffer ) 的 I/O 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后通过一个存储在 Java 堆里面的 DirectByteBuwer
对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了 在 Java 堆和 Native 堆中来回复制数据。
NIO 的 Buffer
提供一个可以直接访问系统物理内存的类—— DirectBuffer
。 DirectBuffer
类继承自 ByteBuffer
,但和普通的 ByteBuffer
不同。普通的 ByteBuffer
仍在 JVM 堆上分配内存,其最大内存受到最大堆内存的 限制。而 DirectBuffer
直接分配在物理内存中,并不占用堆空间。在访问普通的 ByteBuffer
时,系统总是会使用一个“内核缓冲区”进行操作。而 DirectBuffer
所处的位置,就相当于这个“内核缓冲区”。因此,使用 DirectBuffer
是一种更加接近内存底层的方法,所以它的速度比普通的 ByteBuffer
更快
实战 OutOfMemoryError 异常
Java 堆溢出
java.lang.OutOfMemoryError: Java heap space
的信息,说明在堆内存空间产生内存溢出的异常
java.lang.StackOverflowError
栈溢出
java.lang.OutOfMemoryError: GC overhead limit exceeded
根据 Oracle 官方文档,默认情况下,如果 Java 进程花费 98% 以上的时间执行 GC ,并且每次只有不到 2% 的堆被恢复,则 JVM 抛出此错误
由直接内存导致的内存溢出, 一个明显的特征是在 Heap Dump 文件中不会看见有什么明显的异常 情况, 如果发现内存溢出之后产生的 Dump 文件很小, 而程序中又直接或间接使用了 DirectMemory
(典型的间接使用就是 NIO ) ,那就可以考虑重点检查一下直接内存方面的原因了
JVM 加载机制详解
类加载子系统
- 类加载子系统负责从文件系统或是网络中加载
.class
文件, class 文件在文件开头有特定的文件标识。 - 把加载后的 class 类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是 Class 文件中常量池部分的内存映射);
- ClassLoader 只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定;
- 如果调用构造器实例化对象,则该对象存放在堆区;
类加载器 ClassLoader 角色
- class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到 JVM 当中来根据这个文件实例化出 n 个一模一样的实例。
- class file 加载到 JVM 中,被称为 DNA 元数据模板。
- 在 .class 文件 --> JVM --> 最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
类加载的执行过程
类从被加载到虚拟机内存中开始,到卸载出内存,它的整个生命周期包括:加载( Loading )、验证( Verification )、准备( Preparation )、解析( Resolution )、初始化( Initiallization )、使用( Using )和卸载( Unloading )这 7 个阶段。其中验证、准备、解析 3 个部分统称为连接( Linking ),这七个阶段的发生顺序如下图:
加载
有两种时机会触发类加载:
-
预加载
虚拟机启动时加载,加载的是
JAVA_HOME/lib/
下的rt.jar
下的.class
文件,这个 jar 包里面的内容是程序运行时非常常常用到的,像java.lang.*
、java.util.
、java.io.
等等,因此随着虚拟机一起加载 -
运行时加载
虚拟机在用到一个
.class
文件的时候,会先去内存中查看一下这个.class
文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。
加载阶段做了有三件事情:
- 获取
.class
文件的二进制流 - 将类信息、静态变量、字节码、常量这些
.class
文件中的内容放入方法区中 - 在内存中生成一个代表这个
.class
文件的java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。一般这个 Class 是在堆里的,不过 HotSpot 虚拟机比较特殊,这个 Class 对象是放在方法区中的
链接
链接包含三个步骤: 分别是 验证Verification , 准备Preparation , 解析Resolution 三个过程
验证 Verification
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备 Preparation
准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配。
- 这时候进行内存分配的仅仅是类变量(被
static
修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在 Java 堆中 - 这个阶段赋初始值的变量指的是那些不被
final
修饰的static
变量,比如public static int value = 123
,value
在准备阶段过后是0
而不是123
,给value
赋值为123
的动作将在 初始化阶段 才进行;比如public static final int value = 123;
就不一样了,在准备阶段,虚拟机就会给value
赋值为123
。
各个数据类型的零值如下表:
数据类型 | 零值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
chart | \u0000 |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
类变量有两次赋初始值的过程,一次在准备阶段,赋予初始值(也可以是指定值);另外一次在初始化阶段,赋予程序员定义的值。因此,即使程序员没有为类变量赋值也没有关系,它仍然有一个默认的初始值。
局部变量不像类变量那样存在准备阶段,如果没有给它赋初始值,是不能使用的。
解析 Resolution
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。
符号引用包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
符号引用 是对于类、变量、方法的描述。符号引用和虚拟机的内存布局是没有关系的,引用的目标未必已经加载到内存中了。
直接引用 可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经存在在内存中了。
解析阶段负责把整个类激活,串成一个可以找到彼此的网,大体可以分为:
- 类或接口的解析
- 类方法解析
- 接口方法解析
- 字段解析
初始化
类的初始化阶段是类加载过程的最后一个步骤, 之前介绍的几个类加载的动作里, 除了 在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与 外, 其余动作都完全由 Java 虚拟机来主导控制。 直到初始化阶段, Java 虚拟机才真正开始执行类中编写的 Java 程序代码, 将主导权移交给应用程序。
初始化阶段就是执行类构造器 <cinit>
方法的过程。<cinit>
方法并不是程序员在 Java 代码中直接编写的方法, 它是 javac
编译器的自动生成物。
<cinit>
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块( static {}
块) 中的语句合并产生的, 编译器收集的顺序是由语句在源文件中出现的顺序决定的, 静态语句块中只能访问到定义在静态语句块之前的变量, 定义在它之后的变量, 在前面的静态语句块可以赋值, 但是不能访问
<cinit>
方法与类的构造函数(即在虚拟机视角中的实例构造器 <init>
方法) 不同, 它不需要显式地调用父类构造器, Java 虚拟机会保证在子类的 <cinit>
方法执行前, 父类的 <cinit>
方法已经执行 完毕。 因此在 Java 虚拟机中第一个被执行的 <cinit>
方法的类型肯定是 java.lang.Object
。由于父类的 <cinit>
方法先执行, 也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
<cinit>
方法对于类或接口来说并不是必需的, 如果一个类中没有静态语句块, 也没有对变量的赋值操作, 那么编译器可以不为这个类生成 <cinit>
方法。 接口中不能使用静态语句块, 但仍然有变量初始化的赋值操作, 因此接口与类一样都会生成 <cinit>
方法。
但接口与类不同的是, 执行接口的 <cinit>
方法不需要先执行父接口的 <cinit>
方法, 因为只有当父接口中定义的变量被使用时, 父接口才会被初始化。 此外, 接口的实现类在初始化时也 一样不会执行接口的 <cinit>
方法。
Java 虚拟机必须保证一个类的 <cinit>
方法在多线程环境中被正确地加锁同步, 如果多个线程同 时去初始化一个类, 那么只会有其中一个线程去执行这个类的 <cinit>
方法, 其他线程都需要阻塞等 待, 直到活动线程执行完毕 <cinit>
方法。 如果在一个类的 <cinit>
方法中有耗时很长的操作, 那就 可能造成多个进程阻塞, 在实际应用中这种阻塞往往是很隐蔽的
<cinit>
与 <init>
类的初始化和对象的初始化
- 方法
<cinit>
的执行时期:类初始化阶段 (该方法只能被 JVM 调用, 专门承担类变量的初始化工作) ,只执行一次 - 方法
<init>
的执行时期:对象的初始化阶段
类加载器
类的加载指的是将类的 .class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后再创建一个 java.lang.Class
对象,用来封装类在方法区内的数据结构。
JVM 主要在程序第一次主动使用类的时候,才会去加载该类,也就是说, JVM 并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。
类加载器分类:
- JVM 支持两种类型的加载器,分别是引导类加载器和 自定义加载器
- 引导类加载器是由 C/C++ 实现的,自定义加载器是由 Java 实现的
- JVM 规范定义自定义加载器是指派生于抽象类
ClassLoder
的类加载器。 - 按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器
BootStrapClassLoader
、自定义类加载器( Extension Class Loader 、 System Class Loader 、 User-Defined ClassLoader )
上图中的加载器划分为包含关系而并非继承关系
类加载器 | 描述 |
---|---|
启动类加载器 | 这个类加载器使用 C/C++ 实现,嵌套在 JVM 内部 它用来加载 Java 的核心类库( JAVA_HOME/jre/lib/rt.jar 、 resource.jar 或 sun.boot.class.path 路径下的内容),用于提供 JVM 自身需要的类并不继承自 java.lang.ClassLoader ,没有父加载器 |
扩展类加载器 | Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载;派生于 ClassLoader 父类加载器为启动类加载器 |
系统类加载器 | Java 语言编写,由 sun.misc.Lanucher$AppClassLoader 实现该类加载是程序中默认的类加载器,一般来说, Java 应用的类都是由它来完成加载的,它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库;派生于 ClassLoader 父类加载器为扩展类加载器 通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器 |
用户自定义类加载器 | 在日常的 Java 开发中,类加载几乎是由三种加载器配合执行的,在必要时我们还可以自定义类加载器,来定制类的加载方式 |
双亲委派模型
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即 ClassNotFoundException
),子加载器才会尝试自己去加载。
如何实现双亲委派模型:java.lang.ClassLoader#loadClass(java.lang.String)
自定义类加载器
为什么要自定义类加载器:
-
隔离加载类
模块隔离,把类加载到不同的应用选中。比如 Tomcat 这类 web 应用服务器,内部自定义了好几中类加载器,用于隔离 web 应用服务器上的不同应用程序。
-
修改类加载方式
除了 Bootstrap 加载器外,其他的加载并非一定要引入。根据实际情况在某个时间点按需进行动态加载。
-
扩展加载源
比如还可以从数据库、网络、或其他终端上加载
-
防止源码泄漏
Java 代码容易被编译和篡改,可以进行编译加密,类加载需要自定义还原加密字节码。
自定义函数调用过程
public class MyClassLoader extends ClassLoader {
//1.定义字节码文件的路径
private String codePath;
//2.定义构造方法
public MyClassLoader(ClassLoader parent, String codePath) {
super(parent);
this.codePath = codePath;
}
public MyClassLoader(String codePath) {
this.codePath = codePath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//声明输入流
BufferedInputStream bis = null;
//声明输出流
ByteArrayOutputStream baos = null;
//字节码路径
try {
String file = codePath + name + ".class";
//初始化输入流
bis = new BufferedInputStream(new FileInputStream(file));
//初始化输出流
baos = new ByteArrayOutputStream();
//io读写操作
int len;
byte[] data = new byte[1024];
while ((len = bis.read(data)) != -1) {
baos.write(data, 0, len);
}
//获取内存中的字节数组
byte[] bytes = baos.toByteArray();
//调用defineClass 将字节数组转成Class实例
Class<?> clazz = defineClass(null, bytes, 0, bytes.length);
//返回class对象
return clazz;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
垃圾回收机制及算法
垃圾收集需要完成的三件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
垃圾回收-对象是否已死
判断对象是否存活 - 引用计数算法
引用计数算法可以这样实现:给每个创建的对象添加一个引用计数器,每当此对象被某个地方引用时,计数值 +1 ,引用失效时 -1 ,所以当计数值为 0 时表示对象已经不能被使用。
优点:实现简单,执行效率高,很好的和程序交织。
缺点:无法检测出循环引用。
判断对象是否存活 - 可达性分析算法
在主流的商用程序语言如 Java 、 C #等的主流实现中,都是通过可达性分析( Reachability Analysis )来判断对象是否存活的。
此算法的基本思路就是通过一系列的 GC Roots 的对象作为起始点,从起始点开始向下搜索到对象的路径。搜索所经过的路径称为 引用链( Reference Chain ),当一个对象到任何 GC Roots 都没有引用链时,则表明对象“不可达”,即该对象是不可用的。
在 Java 语言中,可作为 GC Roots 的对象包括下面几种:
- 栈帧中的局部变量表中的 reference 引用所引用的对象
- 方法区中 static 静态引用的对象
- 方法区中 final 常量引用的对象
- 本地方法栈中 JNI ( Native 方法)引用的对象
- Java 虚拟机内部的引用, 如基本数据类型对应的
Class
对象, 一些常驻的异常对象(比如NullPointExcepiton
、OutOfMemoryError
) 等, 还有系统类加载器。 - 所有被同步锁(
synchronized
关键字) 持有的对象。 - 反映 Java 虚拟机内部情况的 JMXBean 、 JVMTI 中注册的回调、 本地代码缓存等。
JVM 之判断对象是否存活
finalize()
方法最终判定对象是否存活:
Finalizer
线程去执行它们的 finalize()
方法, 这里所说的“执行”是指虚拟机会触发这个方法开始运行, 但并不承诺一定会等待它运行结束。 这样做的原因是, 如果某个对象的 finalize()
方法执行缓慢, 或者更极端地发生了死循环, 将很可能导致 F-Queue 队列中的其他对象永久处于等待, 甚至导致整个内存回收子系统的崩溃。
再谈引用
引用类型 | 描述 |
---|---|
强引用(StrongReference) | 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足, Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 ps :强引用其实也就是我们平时 A a = new A() 这个意思。 |
软引用(SoftReference) | 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。 软引用可以和一个引用队列( ReferenceQueue )联合使用,如果软引用所引用的对象被垃圾回收器回收, Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。 |
弱引用(WeakReference) | 用来描述那些非必须对象, 但是它的强度比软引用更弱一些, 被弱引用关联的对象只能生存到下一次垃圾收集发生为止。 当垃圾收集器开始工作, 无论当前内存是否足够, 都会回收掉只被弱引用关联的对象。 在 JDK 1.2 版之后提供了 WeakReference 类来实现弱引用。 弱引用可以和一个引用队列( ReferenceQueue )联合使用,如果弱引用所引用的对象被垃圾回收, Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 |
虚引用(PhantomReference) | 是最弱的一种引用关系。如果一个对象仅持有虚引用,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。 |
弱引用与软引用的区别在于:
- 更短暂的生命周期;
- 一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
虚引用与软引用和弱引用的一个区别在于:
- 虚引用必须和引用队列 (ReferenceQueue)联合使用。
- 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
垃圾收集算法
分代收集理论
思想也很简单,就是根据对象的生命周期将内存划分,然后进行分区管理。 当前商业虚拟机的垃圾收集器, 大多数都遵循了“分代收集”( Generational Collection )的理论进 行设计, 分代收集名为理论, 实质是一套符合大多数程序运行实际情况的经验法则, 它建立在两个分代假说之上:
- 弱分代假说( Weak Generational Hypothesis ) : 绝大多数对象都是朝生夕灭的。
- 强分代假说( Strong Generational Hypothesis ) : 熬过越多次垃圾收集过程的对象就越难以消亡。
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则: 收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数) 分配到不同的区域之中存储。
- 部分收集(Partial GC) : 指目标不是完整收集整个 Java 堆的垃圾收集, 其中又分为:
- 新生代收集(Minor GC/Young GC): 指目标只是新生代的垃圾收集。
- 老年代收集( Major GC/Old GC ): 指目标只是老年代的垃圾收集,目前只有 CMS 收集器会有单 独收集老年代的行为。
- 混合收集( Mixed GC ): 指目标是收集整个新生代以及部分老年代的垃圾收集。 目前只有 G1 收集器会有这种行为。
- 整堆收集(Full GC) : 收集整个Java堆和方法区的垃圾收集
标记 - 清除算法
算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回 收的对象, 在标记完成后,统一回收掉所有被标记的对象, 也可以反过来, 标记存活的对象, 统一回收所有未被标记的对象。
标记-清除算法有两个不足之处:
- 第一个是执行效率不稳定, 如果 Java 堆中包含大量对 象, 而且其中大部分是需要被回收的, 这时必须进行大量标记和清除的动作, 导致标记和清除两个过 程的执行效率都随对象数量增长而降低;
- 第二个是内存空间的碎片化问题, 标记、 清除之后会产生大 量不连续的内存碎片, 空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记 - 复制算法
将可用内存按容量划分为大小相等的两块, 每次只使用其中的一块。 当这一块的内存用完了, 就将还存活着的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理掉。
缺点:
- 需要提前预留一半的内存区域用来存放存活的对象(经过垃圾收集后还存活的对象),这样导致可用的对象区域减小一半,总体的 GC 更加频繁了
- 如果出现存活对象数量比较多的时候,需要复制较多的对象,成本上升,效率降低
- 如果 99% 的对象都是存活的(老年代),那么老年代是无法使用这种算法的。
现在的商用 Java 虚拟机大多都优先采用了这种收集算法去回收新生代
标记 - 整理算法
标记 - 复制算法在对象存活率较高时就要进行较多的复制操作, 效率将会降低。 更关键的是, 如果不想浪费 50% 的空间, 就需要有额外的空间进行分配担保, 以应对被使用的内存中所有对象都 100% 存活的极端情况, 所以在老年代一般不能直接选用这种算法。
标记过程仍然与 标记-清除 算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存 。
垃圾收集器
垃圾收集器概述
垃圾回收算法分类两类,第一类算法判断对象生死算法,如引用计数法、可达性分析算法 ;第二类收集死亡对象方法有四种,如标记 - 清除算法、标记 - 复制算法、标记 - 整理算法。
一般的实现采用分代回收算法,根据不同代的特点应用不同的算法。
垃圾回收算法是内存回收的方法论。垃圾收集器是算法的落地实现。
和回收算法一样,目前还没有出现完美的收集器,而是要根据具体的应用场景选择最合适的收集器,进行分代收集。
垃圾回收器 | 描述 |
---|---|
串行垃圾回收(Serial) | 串行垃圾回收是为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程,不适合交互性强的服务器环境 |
并行垃圾回收(Parallel) | 多个垃圾收集器线程并行工作,同样会暂停用户线程,适用于科学计算、大数据后台处理等多交互场景。 |
并发垃圾回收(CMS) | 用户线程和垃圾回收线程同时执行,不一定是并行的,可能是交替执行,可能一边垃圾回收,一边运行应用线程,不需要停顿用户线程,互联网应用程序中经常使用,适用对响应时间有要求的场景。 |
G1垃圾回收器 | G1垃圾回收器将堆内存分割成不同的区域然后并发地对其进行垃圾回收。 |
根据分代思想,我们有 7 种主流的垃圾回收器:
- 新生代垃圾收集器:
Serial
、ParNew
、Parallel Scavenge
- 老年代垃圾收集器:
Serial Old
、Parallel Old
、CMS
- 整理收集器:
G1
垃圾收集器的组合关系:
JDK 8 中默认使用组合是:Parallel Scavenge GC
、Parallel Old GC
JDK 9 默认是用 G1
为垃圾收集器
JDK 14 弃用了: Parallel Scavenge GC
、 Parallel Old GC
JDK 14 移除了 CMS GC
GC 性能指标:
- 吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99%
- 暂停时间:执行垃圾回收时,程序的工作线程被暂停的时间
- 内存占用: Java 堆所占内存的大小
- 收集频率:垃圾收集的频次
Serial 收集器
单线程收集器,“单线程”的意义不仅仅说明它只会使用一个 CPU 或一个收集线程去完成垃圾收集工作;更重要的是它在垃圾收集的时候,必须暂停其他工作线程,直到垃圾收集完毕;
使用方式:-XX:+UseSerialGC
ParNew 收集器
ParNew 收集器实质上是 Serial 收集器的多线程并行版本
使用方式:-XX:+UseParNewGC
设置线程数:XX:ParllGCThreads
Parallel Scavenge 收集器
又称为 吞吐量优先收集器,和 ParNew 收集器类似,是一个 新生代收集器。使用复制算法的并行多线程收集器。 Parallel Scavenge 是 Java1.8 默认的收集器,特点是并行的多线程回收,以吞吐量优先。
特点:
- Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量
- 自适应调节策略,自动指定年轻代、 Eden 、 Suvisor 区的比例
使用方式:
- 使用方式:
-XX:+UseParallelGC
- 控制最大垃圾收集停顿时间:
-XX:MaxGCPauseMillis
- 控制吞吐量大小:
-XX:GCTimeRatio
- 设置年轻代线程数:
-XX:ParllGCThreads
- 根据系统运行情况进行自适应调节新生代区域比例:
-XX:+UseAdaptiveSizePolicy
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本, 它同样是一个单线程收集器, 使用 标记-整理算法 。
使用方式:-XX:+UseSerialGC
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 支持多线程并发收集, 基于 标记 - 整理 算法实现。
指定使用 Parallel Old 收集器:-XX:+UseParallelOldGC
CMS 收集器
CMS ( concurrent mark sweep )是以获取最短垃圾收集停顿时间为目标的收集器, CMS 收集器的关注点尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短就越适合与用户交互的程序,目前很大一部分的 Java 应用几种在互联网的 B/S 系统服务器上,这类应用尤其注重服务器的响应速度,系统停顿时间最短,给用户带来良好的体验, CMS 收集器使用的算法是 标记 - 清除算法 实现的;
G1 收集器
Garbage First 是一款 面向服务端应用 的垃圾收集器,主要针对配备多核 CPU 及大容量内存的机器,以极高概率满足 GC 停顿时间的同时,还兼具高吞吐量的性能特征。
G1 常用参数:
参数/默认值 | 含义 |
---|---|
-XX:+UseG1GC |
使用 G1 垃圾收集器 |
-XX:MaxGCPauseMillis=200 |
设置期望达到的最大 GC 停顿时间指标( JVM 会尽力实现,但不保 证达到) |
-XX:InitiatingHeapOccupancyPercent=45 |
mixed gc 中也有一个阈值参数 ,当老年代大小占整个堆大小百分 比达到该阈值时,会触发一次 mixed gc 。默认值为 45 |
-XX:NewRatio=n |
新生代与老生代( new/old generation )的大小比例( Ratio )。默认值为 2 |
-XX:SurvivorRatio=n |
eden/survivor 空间大小的比例( Ratio )。默认值为 8 |
-XX:MaxTenuringThreshold=n |
提升年老代的最大临界值( tenuring threshold )。默认值为 15 |
-XX:ParallelGCThreads=n |
设置垃圾收集器在并行阶段使用的线程数,默认值随 JVM 运行的平台不同而不同 |
-XX:ConcGCThreads=n |
并发垃圾收集器使用的线程数量。默认值随 JVM 运行的平台不同而不同 |
-XX:G1ReservePercent=n |
设置堆内存保留为假天花板的总量,以降低提升失败的可能性。默认 值是 10 |
-XX:G1HeapRegionSize=n |
使用 G1 时 Java 堆会被分为大小统一的的区( region )。此参数可以指 定每个 heap 区的大小。默认值将根据 heap size 算出最优解。最小值 为 1Mb , 最大值为 32Mb. |
常用指令与可视化调优工具
常用指令
jps
jps
是( Java process Status Tool ), Java 版的 ps 命令,查看 Java 进程及其相关的信息,如果你想找到一个 Java 进程的 pid ,那可以用 jps 命令替代 Linux 中的 ps 命令了,简单而方便。
jps [options] [hostid]
options 参数解释:
-l : 显示进程id,显示主类全名或jar路径
-q : 显示进程id
-m : 显示进程id, 显示JVM启动时传递给main()的参数
-v : 显示进程id,显示JVM启动时显示指定的JVM参数
jinfo
jinfo
是用来查看 JVM 参数和动态修改部分 JVM 参数的命令
jinfo [option] <pid>
options 参数解释:
no options 输出所有的系统属性和参数
-flag 打印指定名称的参数
-flag [+|-] 打开或关闭参数
-flag = 设置参数
-flags 打印所有参数
-sysprops 打印系统配置
# 查看JVM参数和系统配置
jinfo 11666
jinfo -flags 11666
jinfo -sysprops 11666
# 查看打印GC日志参数
jinfo -flag PrintGC 11666
jinfo -flag PrintGCDetails 1166
# 打开GC日志参数
jinfo -flag +PrintGC 11666
jinfo -flag +PrintGCDetails 11666
# 关闭GC日志参数
jinfo -flag -PrintGC 11666
jinfo -flag -PrintGCDetails 11666
jstat
jstat
命令是使用频率比较高的命令,主要用来查看 JVM 运行时的状态信息,包括内存状态、垃圾回收等
jstat [option] VMID [interval] [count]
VMID是进程id,interval是打印间隔时间(毫秒),count是打印次数(默认一直打印)
option 参数解释:
-class class loader的行为统计
-compiler HotSpt JIT编译器行为统计
-gc 垃圾回收堆的行为统计
-gccapacity 各个垃圾回收代容量(young,old,perm)和他们相应的空间统计
-gcutil 垃圾回收统计概述
-gccause 垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因
-gcnew 新生代行为统计
-gcnewcapacity 新生代与其相应的内存空间的统计
-gcold 年老代和永生代行为统计
-gcoldcapacity 年老代行为统计
-printcompilation HotSpot编译方法统计
jstat -gcutil 11666 1000 3
jstack
jstack
是用来查看 JVM 线程快照的命令,线程快照是当前 JVM 线程正在执行的方法堆栈集合。使用 jstack
命令可以定位线程出现长时间卡顿的原因,例如死锁,死循环等。 jstack
还可以查看程序崩溃时生成的 core 文件中的 stack
信息。
jstack [options]
options 参数解释:
-F 当使用jstack 无响应时,强制输出线程堆栈。
-m 同时输出java堆栈和c/c++堆栈信息(混合模式)
-l 除了输出堆栈信息外,还显示关于锁的附加信息
jmap
jmap
可以生成 Java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader
的信息以及 finalizer
队列
jmap [option] (连接正在执行的进程)
option 参数解释:
如果使用不带选项参数的jmap打印共享对象映射,将会打印目标虚拟机中加载的每个共享对象的起始 地址、映射大小以及共享对象文件的路径全称。
-heap 打印java heap摘要
-histo[:live] 打印堆中的java对象统计信息
-clstats 打印类加载器统计信息
-finalizerinfo 打印在f-queue中等待执行finalizer方法的对象
-dump: 生成java堆的dump文件
dump-options:
live 只转储存活的对象,如果没有指定则转储所有对象
format=b 二进制格式
file= 转储文件到
# 把java堆中的存活对象信息转储到dump.bin文件
jmap -dump:live,format=b,file=dump.bin 11666
# 当前没有在等待执行finalizer方法的对象
jmap -finalizerinfo 11666
# 输出堆的详细信息
jmap -heap 11666
# 输出存活对象统计信息
jmap -histo:live 11666 | more
jhat
jhat
是用来分析 jmap
生成 dump 文件的命令, jhat
内置了应用服务器,可以通过网页查看 dump 文件分析结果, jhat
一般是用在离线分析上。
jhat [option][dumpfile]
option 参数解释:
-stack false: 关闭对象分配调用堆栈的跟踪
-refs false: 关闭对象引用的跟踪
-port : HTTP服务器端口,默认是7000
-debug : debug级别
-version 分析报告版本
jhat dump.bin
JVM 常用工具
Jconsole 监控管理工具
Jconsole ( Java Monitoring and Management Console )是从 Java5 开始,在 JDK 中自带的 Java 监控和管理控制台,用于对 JVM 中内存,线程和类等的监控,是一个基于 JMX ( Java management extensions )的 GUI 性能监测工具。 jconsole 使用 JVM 的扩展机制获取并展示虚拟机中运行的应用程序的性能和资源消耗等信息。
直接在 jdk/bin
目录下点击 jconsole.exe
即可启动
VisualVM 可视化优化工具
VisualVM 是一个工具,它提供了一个可视界面,用于查看 Java 虚拟机 ( Java Virtual Machine , JVM ) 上运行的基于 Java 技术的应用程序( Java 应用程序)的详细信息。 VisualVM 对 Java Development Kit ( JDK ) 工具所检索的 JVM 软件相关数据进行组织,并通过一种使您可以快速查看有关多个 Java 应用程序的数据的方式提供该信息。您可以查看本地应用程序以及远程主机上运行的应用程序的相关数据。此外,还可以捕获有关 JVM 软件实例的数据,并将该数据保存到本地系统,以供后期查看或与其他用户共享。
GC 日志分析
GC 日志参数
参数 | 说明 |
---|---|
-XX:+PrintGC |
打印简单GC日志。 类似:-verbose:gc |
-XX:+PrintGCDetails |
打印GC详细信息 |
-XX:+PrintGCTimeStamps |
输出GC的时间戳(以基准时间的形式) |
-XX:+PrintGCDateStamps |
输出GC的时间戳(以日期的形式) |
-XX:+PrintHeapAtGC | 在进行GC的前后打印出堆的信息 |
-Xloggc:../logs/gc.log |
指定输出路径收集日志到日志文件 |
常用垃圾收集器参数
参数 | 描述 |
---|---|
UseSerialGC | 虚拟机在运行在 Client 模式下的默认值,打开此开关后,使用 Serial+Serial Old 收集器组合进行内存回收 |
UseParNewGC | 使用 ParNew + Serial Old 收集器组合进行内存回收 |
UseConcMarkSweepGC | 使用 ParNew + CMS + Serial Old 的收集器组合尽心内存回收,当 CMS 出现 Concurrent Mode Failure 失败后会使用 Serial Old 作为备用收集器 |
UseParallelOldGC | 使用 Parallel Scavenge + Parallel Old 的收集器组合 |
UseParallelGC | 使用 Parallel Scavenge + Serial Old (PS MarkSweep)的收集器组合 |
SurvivorRatio | 新生代中 Eden 和任何一个 Survivor 区域的容量比值,默认为 8 |
PretenureSizeThreshold | 直接晋升到老年代对象的大小,单位是Byte |
UseAdaptiveSizePolicy | 动态调整 Java 堆中各区域的大小以及进入老年代的年龄 |
ParallelGCThreads | 设置并行 GC 时进行内存回收的线程数 |
GCTimeRatio | GC 时间占总时间的比率,默认值为99,只在 Parallel Scavenge 收集器的 时候生效 |
MaxGCPauseMillis | 设置 GC 最大的停顿时间,只在 Parallel Scavenge 收集器的时候生效 |
CMSInitiatingOccupancyFraction | 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认是 68%,仅在 CMS 收集器上生效 |
CMSFullGCsBeforeCompaction | 设置 CMS 收集器在进行多少次垃圾回收之后启动一次内存碎片整理 |
UseG1GC | 使用 G1 (Garbage First) 垃圾收集器 |
MaxGCPauseMillis | 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(sox goal), JVM 会尽量去达成这个目标. |
G1HeapRegionSize | 使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个 heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值 为 32Mb. |
GC 日志分析
[GC (Allocation Failure) [PSYoungGen: 6146K->904K(9216K)] 6146K->5008K(19456K), 0.0038730 secs] [Times: user=0.08 sys=0.00, real=0.00 secs]
将上面 GC 日志抽象为各个部分,然后我们再分别说明各个部分的含义
[a(b)[c:d->e(f), g secs] h->i(j), k secs] [Times: user:l sys=m, real=n secs]
a: GC 或者是 Full GC
b: 用来说明发生这次 GC 的原因
c: 表示发生GC的区域,这里表示是新生代发生了GC,上面那个例子是因为在新生代中内存不够给新对象分配了,然后触发了 GC
d: GC 之前该区域已使用的容量
e: GC 之后该区域已使用的容量
f: 该内存区域的总容量
g: 表示该区域这次 GC 使用的时间
h: 表示 GC 前整个堆的已使用容量
i: 表示 GC 后整个堆的已使用容量
j: 表示 Java 堆的总容量
k: 表示 Java堆 这次 GC 使用的时间
l: 代表用户态消耗的 CPU 时间
m: 代表内核态消耗的 CPU 时间
n: 整个 GC 事件从开始到结束的墙钟时间(Wall Clock Time)
日志分析工具
GC 日志可视化分析工具 GCeasy 和 GCviewer 。通过 GC 日志可视化分析工具,我们可以很方便的看到 JVM 各个分代的内存使用情况、垃圾回收次数、垃圾回收的原因、垃圾回收占用的时间、吞吐量等,这些指标在我们进行 JVM 调优的时候是很有用的。
-
GCeasy 是一款在线的 GC 日志分析器,可以通过 GC 日志分析进行内存泄露检测、 GC 暂停原因分析、 JVM 配置建议优化等功能,而且是可以免费使用
在线使用地址: https://gceasy.io/index.jsp
-
GCViewer 是一款实用的 GC 日志分析软件,免费开源使用,你需要安装 jdk 或者 Java 环境才可以使用。软件为 GC 日志分析人员提供了强大的功能支持,有利于大大提高分析效率
JVM 调优实战
对于 Tomcat 的优化,主要是从 2 个方面入手,一是, Tomcat 自身的配置,另一个是 Tomcat 所运行的 JVM 虚拟机的调优。
配置 Tomcat
配置可以访问 Server Status :
#修改配置文件,配置tomcat管理用户
vim tomcat-users.xml
#写入如下消息
<role rolename="manager"/>
<role rolename="manager-gui"/>
<role rolename="admin"/>
<role rolename="admin-gui"/>
<user username="tomcat" password="tomcat" roles="admin-gui,admin,manager-gui,manager"/>
Tomcat 8 不行,需要修改一个配置文件,否则访问会报 403
vim webapps/manager/META-INF/context.xml
<Context antiResourceLocking="false" privileged="true" >
<CookieProcessor className="org.apache.tomcat.util.http.Rfc6265CookieProcessor"
sameSiteCookies="strict" />
<!-- <Valve className="org.apache.catalina.valves.RemoteAddrValve"
allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" /> -->
<Manager sessionAttributeValueClassNameFilter="java\.lang\.
(?:Boolean|Integer|Long|Number|String)|org\.apache\.catalina\.filters\.CsrfPreventionFilter\$Lr
uCache(?:\$1)?|java\.util\.(?:Linked)?HashMap"/>
</Context>
使用 Apache Jmeter 进行测试
Apache Jmeter 是开源的压力测试工具,我们借助于此工具进行测试,将测试出 Tomcat 的吞吐量等信息。
调整 Tomcat 参数进行优化
禁用 AJP 服务
AJP ( Apache JServer Protocol )是定向包协议 。 WEB 服务器和 Servlet 容器通过 TCP 连接来交互;为了节省 SOCKET 创建的昂贵代价, WEB 服务器会尝试维护一个永久 TCP 连接到 servlet 容器,并且在多个请求和响应周期过程会重用连接
Tomcat 在 server.xml
中配置了两种连接器:
- 第一个连接器监听
8080
端口,负责建立 HTTP 连接。在通过浏览器访问 Tomcat 服务器的 Web 应用时,使用的就是这个连接器。 - 第二个连接器监听
8009
端口,负责和其他的 HTTP 服务器建立连接。在把 Tomcat 与其他 HTTP 服务器集成时,就需要用到这个连接器。 AJP 连接器可以通过 AJP 协议和一个 web 容器进行交互
修改 conf
下的 server.xml
文件,将 AJP 服务禁用掉即可。
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
设置执行器(线程池)
频繁地创建线程会造成性能浪费,所以使用线程池来优化:在 Tomcat 中每一个用户请求都是一个线程,所以可以使用线程池提高性能。
修改 server.xml
文件:
<!--将注释打开-->
<Executor name="tomcatThreadPool" namePrefix="catalina‐exec‐" maxThreads="500" minSpareThreads="50" prestartminSpareThreads="true" maxQueueSize="100"/>
<!--
参数说明:
maxThreads:最大并发数,默认设置 200,一般建议在 500 ~ 1000,根据硬件设施和业务来判断
minSpareThreads:Tomcat 初 始 化 时 创 建 的 线 程 数 , 默 认 设 置 25
prestartminSpareThreads: 在 Tomcat 初始化的时候就初始化 minSpareThreads 的参数值,如果不等于 true,
minSpareThreads 的值就没啥效果了
maxQueueSize,最大的等待队列数,超过则拒绝请求
-->
<!-- A "Connector" represents an endpoint by which requests are received
and responses are returned. Documentation at :
Java HTTP Connector: /docs/config/http.html
Java AJP Connector: /docs/config/ajp.html
APR (HTTP/AJP) Connector: /docs/apr.html
Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
-->
<Connector port="8080" executor="tomcatThreadPool" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
<!-- A "Connector" using the shared thread pool-->
设置最大等待队列
默认情况下,请求发送到 Tomcat ,如果 Tomcat 正忙,那么该请求会一直等待。这样虽然可以保证每个请求都能请求到,但是请求时间就会变长。有些时候,我们也不一定要求请求一定等待,可以设置最大等待队列大小,如果超过就不等待了。这样虽然有些请求是失败的,但是请求时间会虽短。
<!‐‐ 最大等待数为100 ‐‐>
<Executor name="tomcatThreadPool" namePrefix="catalina‐exec‐" maxThreads="500" minSpareThreads="100" prestartminSpareThreads="true" maxQueueSize="100"/>
并不是设置线程数越多吞吐量越好。因此需要根据我们具体的场景进行响应的调试,选取一个较优的参数。
设置 nio2 的运行模式
<Connector executor="tomcatThreadPool" port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" connectionTimeout="20000" redirectPort="8443" />
调整 JVM 参数进行优化
设置并行垃圾回收器
# catalina.sh
#年轻代、老年代均使用并行收集器,初始堆内存64M,最大堆内存512M
JAVA_OPTS="-XX:+UseParallelGC -XX:+UseParallelOldGC -Xms64m -Xmx512m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log"
查看 gc 日志文件 将 gc.log
文件上传到 gceasy.io
查看 gc 中是否存在问题。
调整年轻代大小
# catalina.sh
JAVA_OPTS="-XX:+UseParallelGC -XX:+UseParallelOldGC -Xms512m -Xmx512m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log"
设置 G1 垃圾收集器
# cataliina.sh
# 设置使用G1垃圾收集器最大停顿时间100毫秒,初始堆内存512m,最大堆内存512m
JAVA_OPTS="-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms512m -Xmx512m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log"