JVM概述
虚拟机介绍
虚拟机:指虚拟的计算机,是一款软件
,用来执行一系列计算机指令,虚拟机分为系统虚拟机和程序虚拟机。
系统虚拟机
:完全对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。比如VMware。程序虚拟机
:专门为执行某个计算机程序设计的,比如Java虚拟机。
Java虚拟机:二进制字节码的运行环境,负责转载字节码到其内部,解释编译为对应平台上的机器指令执行。
二进制字节码:可以是Java语言编写编译生成的字节码,也可以是其他语言编写编译额生成的字节码。
JVM的整体结构
JVM的整体结构:
- Java源文件被前端编译器编译生成字节码文件。
- 字节码文件被类加载器子系统加载到内存中,一个字节码文件对应一个Class类对象。
- 数据存放在运行时数据区。
- 执行引擎执行加载到内存中的字节码指令。
JVM的架构模型
指令集架构模型分为:
基于栈式架构模型
:设计和实现简单,适用于资源受限的系统,使用零地址指令分配方式,指令集小,指令数量多,并且不需要硬件支持,可移植性好。基于寄存器架构模型
:性能好并且执行效率高,指令集完全依赖硬件,可移植性差,指令集多指令数量少。
Java编译器输入的指令流基本上是基于栈的指令集架构
JVM的生命周期
JVM的生命周期
- 虚拟机的启动:通过引导类加载器创建一个初始类来完成,这个类由虚拟机的具体实现指定。
- 虚拟机的执行:虚拟机执行Java程序,执行一个Java程序的时候,实际上就是执行一个Java虚拟机进程。
- 虚拟机的退出:程序执行结束虚拟机就停止,程序执行过程中遇到异常或者系统错误虚拟机也会终止。
类加载子系统
类加载子系统结构图
类加载过程
加载阶段
加载阶段:查找并加载类的二进制数据,生成Class的实例
- 通过类的全名,获取类的二进制数据流。
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
注意:数组对象的加载不是由类加载器负责创建,而是由JVM在运行时根据需要直接创建,但数组的元素仍然需要依靠类加载器去创建。
加载二进制文件的方式
- 从本地系统中直接加载
- 通过网络获取
- 从zip压缩包中读取,比如jar、war
- 运行时计算生成,比如动态代理技术
- 从加密文件中获取,可以防止class文件被反编译
类加载的分类:
显示加载
:指代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或者this.getClass().getClassLoader().loadClass()加载class对象。隐式加载
:通过虚拟机自动加载到内存中,不是通过直接在代码中调用ClassLoader的方式加载class对象。比如加载某个类的class文件时,该类的class文件中引用了另一个类对象,此时额外引用的类将通过JVM自动加载到内存中。
链接阶段
链接阶段会执行三个操作:
验证
:当类加载到系统后,会对字节码文件的字节流中包含的信息进行验证,验证是否符合虚拟机的要求,保证加载类的正确性,不会危害虚拟机的安全。
验证阶段包括四种验证:文件格式验证、元数据验证、字节码验证和复合引用验证。
准备
:为类的静态变量分配内存,并将其初始化为默认值。
静态常量在编译使其就分配内存,准备阶段会为静态常量进行显示赋值。
解析
:将类、接口、字段和方法的符号引用转换为直接引用,解析过程实际上是在JVM完成初始化之后再执行。
- 符号引用:一些字面量的引用,和虚拟机的内存数据结构、内存布局无关,在class类文件中通过常量池进行了大量的符号引用。
- 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
符号引用的作用:可以延迟解析,即需要使用引用时才解析,可以节省内存和提高性能。
初始化阶段
初始化:JVM会将类静态成员的显示赋值语句和static语句块的语句进行合并生成一个clinit方法,初始化阶段执行clinit方法。
对于clinit方法的调用,虚拟机会在内部确保多线程环境的安全性,所以JVM会给clinit方法进行加锁处理,如果clinit方法耗时很长,会导致多个线程阻塞。
类加载器的分类
类加载器的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
JVM只支持两种类加载器:引导类加载器(Bootstrap ClassLoader)、自定义类加载器(User-Defined ClassLoader)。
所有派生于抽象类ClassLoader的类加载器都属于自定义类加载,比如扩展类加载器、系统类加载器。
引导类加载器
引导类加载器(BootstrapClassLoader)
- 引导类加载器使用c/c++语言实现的,嵌套在JVM内部。
- 引导类加载器加载Java的核心库(JAVA_HOME/jre.lib/目录下的某些jar包),用于提供JVM自身需要的类。
- 引导类加载器不继承java.lang.ClassLoader类,没有父加载器,处于安全考虑,引导类加载器只加载包名为java、javax、sun等开头的类。
自定义类加载器
所有派生于抽象类ClassLoader的类加载器都属于自定义类加载,比如扩展类加载器、系统类加载器。
扩展类加载器(Extension ClassLoader)
- 扩展类加载器使用java语言编写的,由sun.misc.Launcher$ExtClassLoader实现。派生于ClassLoader类。
- 扩展类加载的父类加载器为引导类加载器。
- 扩展类加载器从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。
系统类加载器(AppClassLoader)
- 系统类加载器使用java语言编写的,由sun.misc.Launcher$AppClassLoader实现。派生于ClassLoader类。
- 系统加载器的父类加载器为扩展类加载器。
- 系统类加载负责加载环境变量classpath或系统属性,java.class.path指定路径下的类库。系统类加载是程序中默认的类加载器,Java应用的类都是由系统类加载完成加载。
- 系统类加载可以通过ClassLoader.getSystemClassLoader() 获取。
用户自定义类加载器(UserDefined ClassLoader)
- 开发人员自己编写一个类,继承 ClassLoader 抽象类或者 URLClassLoader 抽象类。
- 自定义类加载器继承ClassLoader类,将自定义的类加载逻辑写在findClass() 方法中。
- 自定义类加载器继承URLClassLoader类,可以不用编写findClass() 方法,使自定义类加载器编写更加简洁。
有以下情况需要自定义类加载:
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄漏
ClassLoader类
ClassLoader类:是一个抽象类,所有的自定义类加载器都继承自ClassLoader,只有启动类加载器(Bootstrap ClassLoader)不继承ClassLoader类。
ClassLoader类常用方法:
方法名 | 描述 |
---|---|
getParent() | 返回该类加载器的超类加载器 |
loadClass(String name) | 加载名称为name的类,返回结果为java.lang.Class类的实例 |
findClass(String name) | 查找名称为name的类,返回结果为java.lang.Class类的实例 |
findLoadedClass(String name) | 查找名称为name的已经被加载过的类,返回结果为java.lang.Class的实例 |
defineClass(String name,byte[] b, int off, int len) | 把字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例 |
resolveClass(Class<?> c) | 连接指定的一个Java类 |
获取ClassLoader的方式
- 获取当前类的ClassLoader
clazz.getClassLoader();
- 获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader();
- 获取系统的ClassLoader
ClassLoader.getSystemClassLoader();
- 获取调用者的ClassLoader
DriverManager.getCallerClassLoader();
双亲委派机制
双亲委派机制:指要求除了引导类加载器之外的所有类加载器在尝试加载类时,都会优先委托给其父加载器来尝试加载,只有在父加载器无法加载时才由子加载器自行加载。
为什么要有双亲委派机制?
- 避免类的重复加载
- 保护程序安全,防止核心API被篡改
沙箱安全机制:自定义java.lang.String类,加载自定义String类时会向上委派到引导类加载器,引导类加载器会加载jdk自带的文件,会加载jdk自带的String类而不会加载自定义的String类。
运行时数据区
运行时数据区的结构图
运行时数据区:JVM在运行期间使用到的内存,其中一些会随着虚拟机的启动和结束而创建和销毁,另外一些则是与线程对应的,会随着线程的启动和结束而创建和销毁。
线程单独的区域
:程序计数器PC、虚拟机栈、本地方法栈。线程共享的区域
:堆、方法区。
每个JVM只有一个Runtime实例(运行时环境)。
当Java线程准备好执行以后,此时操作系统的本地线程同时创建。Java线程执行终止后,本地线程也回收。
虚拟机线程
:该线程的操作需要JVM达到安全点(代码特定位置)才会出现,这样堆才不会变化。周期任务线程
:该线程时事件周期事件的体现(比如中断),一般用于周期性操作的调度执行。GC线程
:该线程在JVM里不同种类的垃圾收集行为提供支持。编译线程
:该线程在运行时将字节码编译成本地代码。信号调度线程
:该线程接收信号并发送给JVM,在内部通过调用适当的方法进行处理。
程序计数器
程序计数器(Program Count Register)是一块很小的内存空间,但运行速度最快的存储空间,用来指向下一条指令的地址,也就是即将要执行的指令代码,由执行引擎读取程序计数指向的指令。
为什么要使用程序计数器记录当前线程的执行地址呢?
- 因为CPU需要不停的切换各个线程,使用程序计数器记录线程执行的地址,这样子CPU切换回来时,就能知道接着从哪里开始继续执行。
为什么程序计数器是每个线程私有的?
- 多线程是在一个特定的时间段内只会执行其中一个线程的方法,CPU会不断的做任务切换,为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以给每个线程分配一个程序计数器,这样各个线程之间可以进行独立计算,不会出现相互干扰的情况。
本地方法栈
本地方法:指由本地代码(通常是C、C++编写的)实现的Java方法,这些方法不是Java语言编写的,而是通过JNI与底层的本地库进行交互。本地方法通常用于执行一些与底层系统、硬件或特定平台相关的操作,这些操作无法用纯Java语言实现。
本地方法栈:用于管理本地方法的调用,本地方法栈是线程私有的。
虚拟机栈
由于跨平台性的设计,Java的指令都是根据栈设计的,不同平台的CPU架构不同,所以不能设计为基于寄存器架构。
虚拟机栈介绍
Java虚拟机栈(Java Virtual Machine Stack),每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次次的Java方法调用。
- Java虚拟机栈是线程私有的,生命周期和线程一致。
- Java虚拟机栈管Java程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回。
虚拟机栈保存方法的局部变量:如果局部变量是基本数据类型,则保存局部变量的值;如果局部变量是引用数据类型,则保存局部变量的引用。
Java虚拟机栈的特点:
- Java虚拟机栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
- JVM直接对Java虚拟机栈的操作只有两个:每个方法的执行伴随栈帧的进栈和出栈。
- 对于栈来说不存在垃圾回收问题,但存在异常问题。
Java虚拟机栈存在的异常有哪些?
栈溢出异常StackOverflowError
:如果线程请求分配的栈容量超出Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个栈溢出异常。超出内存容量异常OutOfMemoryError
:如果Java虚拟机栈在扩展的时候无法申请到足够的内存,或者在创建线程时没有足够的内存创建对应的虚拟机栈,Java虚拟机将会抛出一个超出内存容量异常。
设置虚拟机栈大小:-Xss256k
虚拟机栈的存储单位
虚拟机栈的存储单位是栈帧,一个方法对应一个栈帧,栈帧中维护着方法执行过程中各种数据信息。
栈的运行原理
- 只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧成为当前栈帧,执行引擎运行的所有字节码指令只针对当前栈帧进行操作,
- 如果方法中调用了其他方法,对应新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。
- 方法返回时,当前栈帧会传回此方法的执行结果给前一个栈帧,此时虚拟机会丢弃当前栈帧,使前一个栈帧成为栈顶栈帧(当前栈帧)。
Java方法有两种返回函数的方式:一种是正常函数返回;另一种是抛出异常。这两种返回都会导致栈帧被弹出。
栈帧的内部结构
栈帧的内部结构:
- 局部变量表:
- 操作数栈:
- 动态链接:
- 方法返回地址:
- 一些附加信息:
局部变量表
局部变量表:本质是一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型、对象引用、返回值类型。
局部变量表中的变量只在当前方法调用中有效,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
局部变量表的基本单位变量槽slot:
- 方法的参数存放在变量槽中,JVM会为局部变量表中的每个slot都分配一个访问索引,通过这个索引可以成功访问到局部变量表中指定的局部变量值。
- 局部变量表存放编译期间可知的各种基本数据类型、对象引用、返回值类型的局部变量,32位以内的类型只占用一个slot,64位的类型占用两个slot。
- 实例方法的参数和方法体内部定义的局部变量将会被按照定义顺序复制到局部变量表中的每个slot上,this变量放在第一个位置。静态方法的局部变量表中不存在this变量,所以静态方法不能访问this。
Slot的重复利用:栈帧中的局部变量表中的操作是可以重复利用的,如果一个局部变量过了作用域,那么在其作用域之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的。
操作数栈
操作数栈:在方法执行过程中,根据字节码指令往栈中写入数据或提取数据,即入栈和出栈。
操作数栈是JVM执行引擎的一个工作区,当一个方法开始执行时,一个新的栈帧会被创建出来,这时方法的操作数栈为空。方法执行运算过程中,某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用它们后再把结果压入栈。
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
动态链接
动态链接:指向运行时常量池中该栈帧所属方法的引用,为了支持当前方法的代码能够实现动态链接。
在Java源文件编译成字节码文件,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。动态链接的作用是将这些符号引用转换为调用方法的直接引用。
常量池的作用:提供一些符号和常量,便于指令的识别。
方法返回地址
方法返回地址:指方法执行完返回到执行下一条执行的指令的地址。
方法结束有两种方式:
- 正常执行完成:执行引擎执行方法返回的字节码指令,会有返回值传递给上层的方法调用者。
- 出现异常,非正常退出:方法执行过程中遇到了异常,并且这个异常没有进行处理,会导致方法异常退出。
栈顶缓存技术
栈顶缓存技术:用于提高线程执行速度,当一个方法调用另一个方法时,JVM将被调用方法的栈帧缓存在调用方法的栈顶上,而不是将其直接推入调用方法的栈帧之中。
通过栈顶缓存技术,可以减少方法调用的开销,提高方法之间的调用效率。因为被调用方法的栈帧缓存在调用方法的栈顶上,可以减少栈帧的创建和销毁次数。
方法调用本质
链接的分类
静态链接
:当字节码文件被转载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下调用方法的符号引用转换为直接引用的过程成为静态链接。动态链接
:被调用的方法在编译期无法被确定,只能在程序运行期将调用的方法的符号引用转换为直接引用,这种引用转换过程具备动态性,称为动态链接。
虚方法和非虚方法
虚方法
:方法在编译期无法确定具体的调用版本,这种方法称为非虚方法。非虚方法
:方法在编译期就确定了具体的调用版本,且这个版本在运行时不可变,这种方法称为非虚方法。
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法,其他方法都是虚方法。
方法重写的本质:
- 找到操作数栈顶的第一个元素所执行的对象的实际类型。
- 如果在类型中找到常量中描述符合简单名称都相符的方法,则进行访问权限校验。如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
- 如果在类型中找不到常量中描述复合简单类型都相符的方法,则按照继承关系从下往上依次对类型的各个父类进行第二步的搜索和验证过程。
- 如果始终找不到合适的方法,则抛出java.lang.AbstractMethodError异常。
堆
堆介绍
堆:JVM用来存储对象实例的内存区域,堆对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们共享同一堆空间。
堆的内部结构
堆分为两部分:新生代和老年代
Java堆进一步细分:年轻代和老年代,年轻代又分为伊甸园区和幸存者1区和幸存者2区。
存储在JVM中的Java对象划分为两类:
- 生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速,保存在年轻代。
- 生命周期非常长,在某些极端情况下与JVM的生命周期一致,这类对象保存在老年代。
堆空间为什么要分为新生代和老年代?
- 分代的原因是优化GC性能,如果没有分代,所有对象都放在一起,GC的时候就需要扫描所有对象,性能很低。如果分代将新创建的对象放在伊甸园区,大部分GC只需要扫描这部分即可,放在老年代的对象无需经常扫描。
对象分配过程
对象分配过程:
- 创建一个对象时,大部分对象都是创建在新生代的伊甸园区,当对象太大导致伊甸园区存放不下,对象就会直接存放在老年代;
- 当Eden区内存满了,JVM的垃圾回收器对年轻代进行垃圾回收(Minor GC);
- 垃圾回收后,将非垃圾对象移动到to区,from区域的对象也移动到to区,此时from区变成to区,to区变成from区;
- 当年轻代的对象经历了16次垃圾回收时还没有被回收,此时该对象会移动到老年代;
- 当老年代的内存不足时,会触发垃圾回收(Major GC),对老年代内存进行清理;
- 如果老年代进行了垃圾回收还是无法保存对象,就会产生OOM异常。
每次进行Minor GC后,from区和to区交换,同时伊甸园和to区都会变成空。
触发垃圾回收的情况:一种是伊甸园区满了触发Minor GC对年轻代进行垃圾回收;一种是老年代满了触发Major GC对老年代进行垃圾回收。(幸存者区满了不会触发垃圾回收)
垃圾回收分类
HotSpot虚拟机不同的垃圾回收器有不同的垃圾回收:
Minor GC
:只对新生代垃圾收集,当伊甸园区满了就会触发Minor GC,但幸存者区满了不会触发Minor GC。Major GC
:只对老年代垃圾收集,当老年代满了就会触发Major GC。Mixed GC
:收集整个新生代和以及部分老年代的垃圾收集。Full GC
:对整个堆空间和方法区的垃圾收集。
Full GC触发的情况:
- 调用System.gc()时,系统可能会触发Full GC。
- 老年代空间不足时。
- 方法区空间不足时。
本地缓存TLAB
TLAB:Thread LocalAllocation Buffer,JVM为每个线程分配了一个私有缓存区域,包含在Eden空间内,多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时可以同生内存分配的吞吐量。
所有的对象分配都是优先在TLAB上分配,一旦对象在TLAB空间分配失败,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
堆空间的参数设置
- -XX:+PrintFlagsInitial:查看所有的参数的默认初始值
- -XX:+PrintFlagsFinal:查看所有的参数的最终值
- -Xms:初始堆空间内存(默认为物理内存的1/64)
- -Xmx:最大堆空间内存(默认为物理内存的1/4)
- -Xmn:设置新生代最大内存大小
- -XX:NewRatio:配置新生代与老年代的占比,如果设置为2,则表示新生代占1,老年代占2,新生代占整个堆的1/3,默认情况下新生代与老年代的占比为1:2
- -XX:SurvivorRatio:设置Eden与另外两个Survivor空间所占比例,如果设置为8,则表示Eden:s1:s2=8:1:1,几乎所有的Java对象都是在Eden区被new出来的。
- -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄(默认值为15)
- -XX:+PrintGCDetails:输出回收堆空间的垃圾对象的GC处理日志
- -XX:+PrintGC:打印GC的简要信息
- -XX:HandlePromotionFailure:是否设置空间分配担保
对象不是全部分配在堆上
随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些变化,所有对象分配到堆上不是绝对的了。
逃逸分析
:编译器优化的一项技术,用于分析对象的作用域和生命周期,确定对象的引用是否会逃逸出当前线程的上下文。作用是帮助Java编译器分析一个新的对象的引用的使用范围从而决定是否将这个对象分配到堆上。
判断一个对象是否发生逃逸:
- 当对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
- 当对象在方法中被定义后,被外部方法所引用,则发生逃逸(例如调用参数传递到其他方法中)。
逃逸分析的代码优化:
栈上分配
:如果对象不会发生逃逸,对象可能会分配在栈上,而不是在堆上分配。同步省略
:如果发现对象只有一个线程访问,那么该对象可以不考虑同步。分离对象或标量替换
:如果发现对象不需要作为一个连续的内存结构也可以被访问到,那么对象的部分可以不存储在堆上,而是存储在CPU寄存器中。
方法区
方法区介绍
方法区:方法区是各个线程共享的内存区域,方法区会在JVM启动的时候创建,并且方法区实际的物理内存和Java堆一样可以是不连续的,关闭JVM就会释放方法区。
JDK7之前习惯将方法区称为永久代,JDK8之后使用元空间替代了永久代。
元空间和永久代的区别:
- 元空间使用本地内存来存储类的元数据,不再受到堆内存大小限制;永久代使用JVM堆内存中的一部分来存储类的元数据。
- 元空间采用动态分配内存的方式,在需要时动态申请内存空间,避免内存移除问题;永久代的大小是固定的,并且难以调整,当内存不足时会导致内存溢出。
- 元空间不再需要进行Full GC来释放方法区的空间,因为元空间的内存不属于Java堆;永久代内存不足会触发Full GC来释放空间。
虚拟机栈、堆和方法区的关系
方法区的存储信息
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、及时编译器后的代码缓存等。
运行时常量池
运行时常量池:是方法区的一部分,用于存放编译期生成的字面量和符号引用的内存区域。
运行时常量池存放如下内容:
字面量
:包括字符串字面量、数值字面量等。符号引用
:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。
为什么要有运行时常量池?
节省内存空间
:常量池中字面量和符号引用可以在多个地方引用,在常量池中只需要存储一份即可。提高性能
:常量池中存储的字符串等不可变对象,可以复用,避免重复创建和销毁对象的开销。支持动态解析
:在运行时,可以动态将符号引用解析为直接引用,在程序运行过程中能够正确访问类、方法、字段等。
方法区的垃圾回收
方法区内常量池中主要存放两大常量来回收:字面量和符号引用。
HotSpot虚拟机对常量池常量的回收策略:常量池中的常量没有被任何地方引用,就可以被回收。
一个类是否允许被回收,需要满足三个条件:
- 类所有的实例都已经被回收,也就是Java堆中不存在类和任何派生子类的实例;
- 加载类的类加载器被回收,该条件很难达到;
- 类对应的Class对象没有被任何地方引用,无法通过反射访问类的方法。
对象的实例化
对象的创建方式
对象创建的步骤
对象创建的步骤:
判断对象对应类是否加载、链接、初始化
:虚拟机首先检查类是否被加载、解析和初始化,如果没有的话,在双亲委派模式下,使用当前类加载器查找对应的字节码文件。如果没有找到字节码文件,则抛出ClassNotFoundException异常;如果找到了,进行类加载并生成对应的Class对象。为对象分配内存
:计算对象占用内存空间大小,在堆中划分一块内存给新对象。处理并发安全问题
:通过CAS失败重试机制和区域加锁,保证指针更新操作的原子性。属性初始化
:内存分配结束后,虚拟机将分配的内存空间初始化为零值。设置对象的对象头
:将对象所属类、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。执行init方法进行初始化
:将类属性显示初始化语句、代码块中的语句、构造器语句,全部封装到init方法,并执行init方法,对属性进行初始化赋值。
对象的内存布局
对象的内存布局:
对象头
:包含运行时数据和类型指针,如果是数组还会记录数组的长度。- 运行时元数据:包括哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID和偏向时间戳。
- 类型指针:指向类元数据,确定对象所属的类型。
实例数据
:对象真正存储的有效信息,包括程序中定义的各种字段。填充数据
:不是必须的,也没有实际含义,仅仅为了保证对象的大小是8字节的整数倍。
对象的访问定位
对象的访问定位有两种方式:
句柄访问
:引用中存储稳定句柄地址,对象移动时只会改变句柄中实例数据指针,引用不需要修改。直接指针
:引用中存储的是堆中实例数据的指针,在实例数据中存储指向方法区的对象类型数据。
HotSpot采用的是直接指针方式。
句柄访问图示
直接指针访问图示
直接内存
直接内存:直接内存是在Java堆外的,不是虚拟机运行时数据区的一部分,直接向系统申请的内存空间。通常,访问直接内存的速度会比Java堆快。
- 读写频繁的场合可能会考虑使用直接内存。
- Java的NIO库允许Java程序使用直接内存,用于数据缓冲区。
执行引擎
执行引擎概述
执行引擎:负责执行Java字节码,将字节码翻译成机器码并执行。
执行引擎的工作流程:
- 执行引擎依次执行字节码指令,没执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址;
- 方法在执行过程中,执行引擎可以通过存储在局部变量表中的对象引用准确定位到存储在堆中的对象实例,以及通过对象实例的对象头中的元数据指针定位到目标对象的类型信息。
计算机语言的发展历程
计算机语言:
- 机器语言:可以被计算机理解和接收,所以机器语言编写的程序执行速度最快,但机器语言和人类语言差别太大,不容易理解和记忆。
- 汇编语言:使用助记符代替机器指令的操作码,用地址符号和标记符号代替指令或操作数的地址。但汇编指令可读性还是较差。
- 高级语言:高级语言比机器语言、汇编语言更接近人类语言,当计算机执行高级语言编写的程序时,需要将程序先解释和编译成机器语言。
字节码:一种中间状态的二进制代码,需要通过直译器转译成机器码才能被计算机执行。字节码主要是为了实现跨平台特性,字节码通过不同的直译器翻译成对应平台的机器码,从而实现跨平台特性。
Java程序的编译和执行过程
Java程序编译和执执行全过程如下图:
Java源代码是由Java编译器(javac)来完成
Java字节码的执行由JVM执行引擎来完成
执行引擎内部结构
执行引擎内部结构:
解释器
:负责逐条解释执行字节码指令,解释器将字节码翻译成对应操作系统的本地指令并执行,实现了跨平台的能力。即时编译器
:负责将热点代码块编译成本地机器码,提高程序的执行速度。垃圾回收器
:负责对内存对象进行垃圾回收。
Java是半编译半解释型语言
Java代码的执行分类:
- 解释器:将源代码编译成字节码文件,然后在运行时通过解释器将字节码文件转为机器码执行。
- 即时编译器:将方法编译成机器码后再执行。
解释器的执行速度慢,但启动快;即时编译器执行速度快,但启动速度慢。
JVM在执行Java代码的时候,通常会将解释器和即时编译器二者结合起来进行,权衡编译本地代码的时间和启动速度。
StringTable
String的基本特性
String的基本特性
- String是final修饰的类,不可被继承的;
- String实现Serializable接口,支持序列化的;
- String实现Comparable接口,字符串之间可以比较大小;
- String的底层数据是final修饰的,String不可变的;
- String的字面量是保存在常量池中的,也就是在方法区中;
- String在JDK8之前通过char数组保存,JDK9之后通过byte数组保存。
为什么JDK9之后String底层通过byte数组保存数据?
- 因为大多数字符都是一个字节,所以使用byte数组可以大大节省内存,可以通过byte加上编码标记,字符是占用一个字节还是两个字节。
String的内存分配
String保存在元空间的字符串常量池中
字符串常量池逻辑上保存在方法区中,但物理上和堆共享同一块内存空间
字符串的拼接操作
字符串的拼接操作
- 常量与常量的拼接结果在常量池里,原理是编译器优化;
- 常量池中不会存在相同内容的变量;
- 只要其中有一个是变量,结果就在队中,原理是StringBuilder。
- 如果拼接结果调用intern()方法,则会将常量池中还没有的字符串放入常量池中,并返回对象地址。
方式1:常量与常量的拼接
String s1 = "a" + "b" + "c"; // 在编译期间会优化为String s1 = "abc";
String s2 = "abc";
// s1 == s2:true
方式2:常量与变量的拼接
String s1 = "abc";
String s2 = "def";
String s3 = "abc" + "def";
String s4 = s1 + "def";
String s5 = s1 + s2;
String s6 = s5.intern();
// s3 == s4:false
// s3 == s5:false
// s4 == s5:false
// s3 == s6:true
// s5 == s6:flase
/*
s5 = s1 + s2底层执行过程:
① StringBuilder s = new StringBuilder();
② s.append(s1);
③ s.append(s2);
④ s5 = s.toString(); // 约等于 s5 = new String(s);
*/
方式3:final修饰的变量的拼接
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // 等价于:String s4 = "a" + "b";
// s3 == s4:true
String的intern()方法
intern()方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中,并返回该字符串在常量池中的地址;若存在则直接返回该字符串在常量池中的地址。
String相关面试题
面试题1:new String(“ab”)会创建几个对象?
- 会创建2个对象,一个是堆中new的String对象,一个是常量池中的”ab”对象。
面试2:new String(“a”) + new String(“b”)会创建几个对象?
- 会创建6个对象
- 对象1:new StringBuilder()
- 对象2:new String(“a”)
- 对象3:常量池中的”a”
- 对象4:new String(“b”)
- 对象5:常量池中的“b”
- 对象6:new String(“ab”)
面试题3:
// intern():将字符串放到常量池,并返回该常量池地址
String s1 = new String("1");
s.intern();
String s2 = "1";
System.out.println(s1 == s2); // false
// 堆中创建一个对象,保存字符串"11"字符串常量池中不存在"11"
String s3 = new String("1") + new String("1");
// JDK6:在字符串常量池中创建"11"
// JDK7:在字符串常量池中创建一个对象保存堆中对象("11")的地址
s3.intern();
String s4 = "11"; // 将堆中对象("11")地址返回
System.out.println(s3 == s4); // JDK6:false JDK7:true
// 堆中创建一个对象,保存字符串"11"字符串常量池中不存在"11"
String s5 = new String("1") + new String("1");
// 在常量池中创建一个"11",这个"11"不指向堆中对象
String s6 = "11";
// 返回17行在常量池中创建"11"地址
s3.intern();
System.out.println(s5 == s6); // JDK6和JDK7都是false
垃圾回收
垃圾回收概述
垃圾回收:指JVM在运行Java程序时,自动管理内存的过程。开发人员无需手动管理内存,因为JVM会负责分配内存以及在内u才能不再使用时进行垃圾回收。
如何确定一个对象是垃圾对象?
- 引用计数算法:每个对象保存一个整数的计数器属性,用于记录被引用的情况。
- 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器为0,则表示对象不可以再被使用,可进行回收。
引用计数算法存在循环引用问题:
- 可达性分析:以根对象集合为起始点,按照从上往下的方式搜索被根对象集合所连接的目标对象是否可达,如果对象不可达,则意味着对象已经死亡,该对象为垃圾对象。
GC Roots对象:
- 虚拟机栈中引用的对象;
- 本地方法栈内JNI引用的对象;
- 方法区中静态属性引用的对象;
- 方法区中常量引用的对象;
- 所有被同步锁持有的对象;
- Java虚拟机内部的引用。
垃圾回收算法
标记清除算法
标记清除算法的执行过程:
标记
:从引用根节点开始遍历,标记所有被引用的对象。清除
:对堆内存从头到尾进行遍历,将垃圾对象进行回收。
标记清除算法的缺点:
效率低
:因为标记阶段需要从根节点开始遍历,标记所有被引用的对象,而且在清除阶段,需要对所有对象进行先行遍历。用户体验差
:标记清除算法在执行期间,需要停止整个程序。产生内存碎片
:标记清除算法整理出来的内存不是连续的,会产生内存碎片问题。
复制算法
复制算法的核心思想:将内存空间分为两块,每次只能使用其中的一块,在垃圾回收时正在使用的内存中存活的对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,然后交换两块内存的角色,最后完成垃圾回收。
复制算法的执行过程:
标记
:从引用根节点开始遍历,标记所有被引用的对象。复制
:将所有活动对象从一个半区复制到另一个半区。更新引用
:更新从From区到To区的所有引用地址,确保存活对象能够被正确引用。清理
:将From区中所有对象清理掉,此时To区就变成新的可用空间。
复制算法的优点:
- 不需要对整个区域进行遍历清除,实现简单,运行高效。
- 复制之后可以保证空闲空间连续,不会出现碎片问题。
复制算法的缺点:
- 复制算法需要两倍的内存空间,一半用于使用,一半空闲用于辅助垃圾回收。
- 复制算法的更新引用阶段中指向该对象的所有引用所有引用都需要修改,这个过程时间开销大。
- 复制算法适用于存活对象不多的场景下,当存活对象过多,复制的对象就会很多,效率就会很低。
复制算法适用于存活对象不多的场景下,所以适合在年轻代中使用。
标记整理算法
标记整理算法的执行过程:
标记
:从引用根节点开始遍历,标记所有被引用的对象。整理
:将所有存活对象压缩到内存的一端,按顺序排放。清理
:清理存活对象边界外所有空间。
标记整理算法的优点:
- 标记整理算法会对内存进行整理,不会产生内存碎片问题。
- 标记整理算法不需要内存减半的代价。
标记整理算法的缺点
- 效率最低,比标记清理和复制算法的效率都更低。因为整理阶段需要调整其他对象引用。
- 用户体验差,标记整理算法在执行过程中需要停止整个应用程序。
垃圾回收算法对比
复制算法需要将活动区域的存活对象赋值到另一半区域中,所以存活对象越少效率越高,年轻代的对象是朝生夕死的对象,存活对象少,所以赋值算法适用于年轻代区域。
标记清除算法 | 标记整理算法 | 复制算法 | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少 | 少 | 多,需要两倍空间 |
产生内存碎片 | 会 | 不会 | 不会 |
移动对象 | 否 | 是 | 是 |
使用场景 | 老年代 | 老年代 | 年轻代 |
内存溢出和内存泄漏
内存溢出
:指没有空闲内存,并且垃圾收集器收集一次之后还是内存空间还是不够。
导致内存溢出的情况:
- Java虚拟机的堆内存设置太小,而需要处理的数据量比较大。
- 代码中创建了大量大的对象,并且长时间不能被垃圾收集器收集。
内存泄漏:指对象不会被程序使用到,但是GC又不能回收该对象(还存在引用),这时内存就会发生泄漏。
内存泄漏的情况:
- 单例的声明周期和应用程序一样长,但是该单例对象程序中又不需要使用,则会导致内存泄漏。
- 需要手动关闭的对象未关闭,比如数据库连接、网络连接、IO流等,则会导致内存泄漏。
虽然内存泄漏不会立刻导致程序崩溃,但是内存泄漏会将程序中可用内存逐步蚕食,直至耗尽所有的可用内存,最终出现内存溢出问题。
暂停程序STW
Stop The World:简称STW,指GC过程中,会产生程序的停顿,停顿产生时整个应用程序都会被暂停,没有任何响应。
- 可达性分析算法中枚举根节点会导致所有Java执行线程停顿,因为分析工作必须在一个确保一致性的快照中进行,如果分析过程中对象引用关系还在不断变化,则分析结果的准确性就无法保证。
所有垃圾回收器都会产生STW。
安全点和安全区域
安全点
:表示线程的一个执行状态,线程只有进行到这个状态时,才能对其进行GC。安全点的选择很重要,如果太少可能会导致GC等待的时间太长,如果太频繁可能会导致运行时的性能问题。
如果GC发生时,如何检查所有线程都跑到最近的安全点停顿下来?
- 抢先式中断:首先中断所有线程,如果还有线程不在安全点,就恢复线程,让线程跑到安全点再中断。
- 主动式中断:设置一个中断状态,各个线程运行到安全点的时候主动轮询这个标志,如果中断标志为真,则将线程中断挂起。
安全区域
:指一段代码中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。
对象引用的分类
强引用(StrongReferenece)
:最传统的引用的定义,是指在程序代码之中普遍存在的引用复制,即类似Object obj = new Object()
这种引用关系。无论任何情况下,只要强引用关系存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用(SoftReference)
:在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
弱引用(Weak Refernce)
:被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
虚引用(PhantomReference)
:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时一个系统通知。
垃圾回收器的分类
垃圾回收器概述
垃圾回收器:用来管理和释放Java程序中不再需要的对象,负责回收无效对象占用的内存空间,以便系统可以重复利用这些空间。
垃圾回收器的分类
- 按线程数分:串行垃圾回收器和并行垃圾回收器
- 按工作模式分:并发式垃圾回收器和独占式垃圾回收器
- 按碎片处理方式:压缩式垃圾回收器和非压缩式垃圾回收器
- 按工作的内存区间分:年轻代垃圾回收器和老年代垃圾回收器
评估GC的性能指标:
吞吐量
:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)。暂停时间
:执行垃圾收集时,程序的工作线程被暂停的时间。垃圾收集的开销
:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。收集频率
:相对于应用程序的执行,收集操作发生的频率。内存占用
:Java堆区所占的内存大小。
高吞吐量和低暂停时间是一对相互竞争的目标,如果选择吞吐量优先,那么需要降低内存回收的执行频率,这样一次垃圾回收就会花费更长的时间,暂停时间就会更长。相反如果频繁地执行内存回收,吞吐量就会下降。
现在的标准:
在最大吞吐量优先的情况下,降低暂停时间
。
7个经典的垃圾回收器
7个经典的垃圾回收器
- 串行回收器:Seria、Serial Old
- 并行回收器:ParNew、Parallel Scavenge、Parallel Old
- 并发回收器:CMS、G1
7个垃圾回收器的组合关系
7个垃圾回收器的对比关系
垃圾收集器 | 分类 | 作用位置 | 使用算法 | 特点 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 作用于新生代 | 复制算法 | 响应速度优先 | 适用于单CPU环境下的client模式 |
ParNew | 并行 | 作用于新生代 | 复制算法 | 响应速度优先 | 多CPU环境Server模式下与CMS配合使用 |
Parallel | 并行 | 作用于新生代 | 复制算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
Serial Old | 串行 | 作用于老年代 | 标记压缩算法 | 响应速度优先 | 适用于单CPU环境下的client模式 |
Parallel Old | 并行 | 作用于老年代 | 标记压缩算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
CMS | 并发 | 作用于老年代 | 标记清除算法 | 响应速度优先 | 适用于互联网或B/S业务 |
G1 | 并发、并行 | 作用于新生代、老年代 | 标记压缩算法、复制算法 | 响应速度优先 | 面向服务端应用 |
CMS垃圾回收器
CMS垃圾回收器注重降低垃圾回收的停顿时间,适用于响应时间要求比较高的应用程序。
CMS垃圾回收器采用标记清除算法,回收过程分为四个步骤:
初始标记
:程序中所有线程都STW,标记GC Roots能直接关联到的对象。并发标记
:从GC Roots的直接关联对象开始白能力整个对象图,这个过程非常耗时,但不会出现STW。重新标记
:程序中所有线程都STW,对并发标记阶段中由于并发情况下对标记错误的对象进行校正。并发清除
:清理删除掉垃圾对象,这个阶段可以和用户线程并发执行,不会出现STW。
CMS的优点:并发收集、低延迟。
CMS的缺点:
- 内存碎片问题:清除阶段不会对空闲内存整理,会出现内存碎片问题。
- CPU资源敏感:在并发阶段,因为占用一部分线程导致应用程序变慢,总吞吐量降低。
- 无法处理浮动垃圾:并发姐u但程序的工作线程和垃圾回收线程同时运行,所以并发阶段如果产生新的垃圾对象,CMS无法对这些垃圾对象进行标记。
G1垃圾回收器
G1垃圾回收器:面向服务端的垃圾收集器,进一步降低暂停时间,同时兼顾良好的吞吐量,G1垃圾收集是全功能垃圾收集器。
G1垃圾收集器的特点:
并发与并行
:G1在回收期间,可以有多个GC线程同时工作,回收期间多个GC线程是并行的。分代收集
:G1不要求整个Eden区、年轻代或者老年代都是连续的,G1将堆空间分为若个区域。空间整合
:G1将内存划分为一个个的Region,内存回收是以Region为基本单位。可预测的停顿时间模型
:G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
字节码文件
字节码文件概述
字节码文件:任何一个Class文件都对应唯一一个类或接口的定义信息,但是Class文件不一定以磁盘文件存在,可能时以8位字节为基本单位的二进制流。
字节码文件的跨平台性:当Java源代码成功编译成字节码后,如果想在不同平台上运行,只需要使用对应平台的虚拟机即可实现同一个字节码文件在不同平台上运行。
字节码文件结构
字节码文件结构:
魔数
:每个Class文件开头的4个字节的无符号整数,魔数是Class文件的标志。作用是表示是否位一个能被虚拟机接受的有效合法的Class文件。版本号
:魔数后四个字节存储的是Class文件的版本号,Class文件的第5和第6字节所代表的是Class文件的副版本号,第7和第8个字节代表的是Class文件的主版本号。常量池集合
:在版本号之后,紧跟着的是常量池的数量以及若干个常量池表项,常量池中常量的数量是不固定的,所以在常量池的入口需要放置两个字节代表常量池容量计数值。- 常量池计数器:两个字节表示常量池容器的计数值,常量池容量数值从1开始。
- 常量池表:常量池中主要存放两个常量,字面量和符号引用。
访问标识
:常量池之后,紧跟着访问标识,访问标识用两个字节表示,用于识别一些类或者接口层次的访问信息,比如是否public修饰、是否abstract修饰等。索引集合
:在访问标识之后存放索引标识,指定类的类型、父类类别和实现的接口信息。字段表集合
:用于描述接口或类中声明的字段。方法集合
:指向常量池索引结合,完整描述每个方法的签名。属性表集合
:指Class文件所携带的辅助信息,比如Class文件的源文件名称。
性能监控与调优
性能调优的步骤
- 性能监控
- 性能分析
- 性能调优
性能评价的指标
- 停顿时间:请求返回时间和请求提交时间的差值。
- 吞吐量:运行用户代码的时间占总运行时间的比例。
- 并发数:同一时刻,对服务器有实际交互的请求数。
- 内存占用:Java堆区所占的内存大小。
性能监控-命令行
查看Java进程
jps:显示指定系统内所有的HotSpot虚拟机进程,可用于查询正在运行的虚拟机进程。
基本语法:jps
仅仅显示PKID:jps -q
输出应用程序主类的全类名:jps -l
输出虚拟机进程启动时的参数:jps -m
输出虚拟机进程启动时的JVM参数:jps -v
查看远程的Java进程:jps hostid
注意:如果Java进程关闭了UsePerfData参数(-XX:-UsePerfData),那么jps命令无法显示该Java进程。
JVM统计信息
jstat:监视虚拟机各种运行状态信息,显示进程中类装载、内存、垃圾收集、JIT编译等运行数据。
jstat -class:显示ClassLoader相关信息
jstat -gc:显示与GC相关的堆信息
jstat -gcutil:以百分比的形式显示GC相关的对信息
jstat -gcnew:显示新生代GC状况
jstat -gcold:显示老年代GC状况
jstat -compiler:显示JIT编译器编译过的方法、耗时等信息
jstat -printcompilation:输出已经被JIT编译的方法
查看和修改JVM参数
jinfo:查看虚拟机配置参数信息,也可以用于修改虚拟机的配置参数。
显示属性信息:jinfo -sysprops pkid
显示所有的VM参数信息:jinfo -flags pkid
显示指定的VM参数信息:jinfo -flag UseParallelGC pkid
修改boolean类型的VM参数:jinfo -flag +PrintGCDetails pkid
修改数值类型的VM参数:jinfo -flag MaxHeapFreeRatio=90 pkid
拓展:
java -XX:+PrintFlagsInitial
:查看所有JVM参数启动的初始值java -XX:+PrintFlagsFinal
:查看所有JVM参数的最终值java -XX:+PrintCommandLineFlags
:查看哪些已经被用户或者JVM设置过的详细的XX参数的名称和值
导出内存映像文件
jmap:可以获取dump文件,还可以获取目标Java进程的内存相关信息。
手动导出所有对象的dump文件:jmap -dump:format=b,file=/wenxuan/1.hprof pkid
手动导出存活对象的dump文件:jmap -dump:live,format=b,file=/wenxuan/1.hprof pkid
出现堆内存溢出时自动导出dump文件:添加两个VM参数(-XX:+HeapDumpOnOutOfMemoryError,-XX:HeadpDumpPath=/wenxuan/1.hprof)
dump文件是二进制文件,无法通过文本编辑器打开查看,需要通过jhat命令或者工具打开。
分析栈信息
jstack:生成虚拟机指定进程当前时刻的线程快照,线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合。
线程快照状态:
- Deadlock:死锁
- Waiting on condition:等待资源
- Waiting on monitor entry:等待获取监视器
- Blocked:阻塞
- Runnable:执行中
- Suspended:暂停
- TIMED_WAITING:对象等待中
- Parked:停止
基本使用:jstack pkid
强制输出线程堆栈:jstack -F pkid
显示锁的额外信息:jstack -l pkid
显示本地方法信息:jstack -m pkid
多功能命令行
jcmd:在JDK7后,新增一个多功能的工具,可以实现除了jstat之外的所有命令的功能。
jcmd -l pkid:列出所有的JVM进程
jcmd pkid help:针对指定的进程,列出支持的命令工具
jcmd pkid Thread.print:替换jstack,输出栈信息
jcmd pkid GC.heap_dump:替换jmap,导出内存映像文件
jcmd pkid GC.run:查看GC的执行情况
jcmd pkid VM.uptime:查看程序的总执行时间
jcmd pkid VM.system_properties:替换jinfo,查看属性信息
jcmd pkid VM.flags:获取JVM的配置参数信息
性能监控-GUI
jconsole
jconsole:从JDK5开始,JDK自带的Java监控和管理控制台。用于对JVM中内存、线程和类等的监控,是一个基于JMX的GUI性能监控工具。
jconsole启动:通过双击JDK目录的bin目录下面的jconsole.exe
Visual VM
Visual VM是一个功能强大的性能监控的可视化工具,JDK7之后Visual VM作为JDK的一部分,在JDK的bin目录下的jvisualvm.exe。
Visula VM的主要功能:
- 生成读取堆内存
- 查看JVM参数和系统属性
- 查看运行中的虚拟机进程
- 程序资源的实时监控
- JMX代理连接、远程环境监控
Arthas
Arthas是Alibaba开源的Java诊断工具,支持Linux/Mac/Windows,采用命令行交互模式。
Arthas官网:arthas.aliyun.com/
Arthas下载安装:
curl -O https://arthas.aliyun.com/arthas-boot.jar
Arthas启动
java -jar arthas-boot.jar
Arthas指令
- help:查看命令帮助信息
- cat:打印文件内容,和linux里的cat命令类似
- echo:打印参数,和linux里的echo命令类似
- grep:匹配查找,和linux里的gep命令类似
- tee:复制标隹输入到标准输出和指定的文件,和linux里的tee命令类似
- pwd:返回当前的工作目录,和linux命令类似
- cls:清空当前屏幕区域
- session:查看当前会话的信息
- reset:重置增强类,将被 Arthas增强过的类全部还原, Arthas服务端关闭时会重置所有增强过的类
- version:输出当前目标Java进程所加载的 Arthas版本号
- history:打印命令历史
- keymap:Arthas快捷键列表及自定义快捷键
- quit/exit:退出当前 Arthas客户端,其他 Arthas客户端不受影响
- stop/shutdown:关闭 Arthas服务端,所有 Arthas客户端全部退出
- dashboard:当前系统的实时数据面板
- thread:查看当前JVM的线程堆栈信息
- jvm:查看当前JVM的信息
- sysprop:查看和修改JVM的系统属性
- sysem:查看JVM的环境变量
- vmoption:查看和修改JVM里诊断相关的option
- perfcounter:查看当前JVM的 Perf Counter信息
- logger:查看和修改logger
- getstatic:查看类的静态属性
- ognl:执行ognl表达式
- mbean:查看 Mbean的信息
- heapdump:类似jmap命令的 heap dump功能
JVM运行时参数
JVM运行时参数分类
JVM运行时参数分为三类:
标准参数选项
:比较稳定,后续版本基本不会变,可以通过java -help
查看。-X参数
:非标准参数,功能比较稳定,但后续可能变更,可以通过java -X
查看。-XX参数
:非标准化参数,这类选项属于实验性不稳定。
-XX参数分为Boolean类型和非Boolean类型:
- Boolean类型:启用Boolean类型通过+号,禁用通过-号。
- 非Boolean类型:通过=号赋值。
添加JVM运行时参数
添加JVM运行时参数常见有三种方式:
- 运行jar包:java -Xms100m -jar demo.jar
- Tomcat运行war包:
# linux下catalina.sh添加
JAVA_OPTS="-XmsS512M"
# Windos下catalina.bat添加
set "JAVA_OPTS=-Xms512m"
- 程序运行中
# 设置Boolean类型参数
jinfo -flag [+][-]<name> pkid
# 设置非Boolean类型参数
jinfo -flag <name>=<value> pkid
常用的JVM运行时参数
- -Xms:设置初始 Java 堆大小,等价于-XX:InitialHeapSize参数,单位有g、m、k。
- -Xmx:设置最大 Java 堆大小,等价于-XX:MaxHeapSize参数。
- -Xss:设置 Java 线程堆栈大小,等价于-XX:ThreadStackSize参数。
- -Xmn:设置年轻代大小。
- -XX:SurvivorRatio:设置Eden区与Survivor区的比值,默认为8。
- -XX:NewRatio:设置老年代与年轻代的比例,默认为2。
- -XX:+UseAdaptiveSizePolicy:设置大小比例自适应,默认开启。
- -XX:PretenureSizeThreadshold:设置让大于此阈值的对象直接分配在老年代,只对Serial、ParNew收集器有效。
- -XX:MaxTenuringThreshold:设置新生代晋升老年代的年龄限制,默认为15。
- -XX:TargetSurvivorRatio:设置MinorGC结束后Survivor区占用空间的期望比例。
- -XX:MetaspaceSize / -XX:PermSize:设置元空间/永久代初始值。
- -XX:MaxMetaspaceSize / -XX:MaxPermSize:设置元空间/永久代最大值。
- -XX:+UseCompressedOops:使用压缩对象。
- -XX:+UseCompressedClassPointers:使用压缩类指针。
- -XX:CompressedClassSpaceSize:设置Klass Metaspace的大小,默认1G。
- -XX:MaxDirectMemorySize:指定DirectMemory容量,默认等于Java堆最大值。
- -XX:+HeapDumpOnOutMemoryError:内存出现OOM时生成Heap转储文件,两者互斥。
- -XX:+HeapDumpBeforeFullGC:出现FullGC时生成Heap转储文件,两者互斥。
- -XX:HeapDumpPath:指定heap转储文件的存储路径,默认当前目录。
- -XX:OnOutOfMemoryError:指定可行性程序或脚本的路径,当发生OOM时执行脚本。
- -XX:+PrintGC:打印简要日志信息。
- -XX:+PrintGCDetails:打印详细日志信息。
- -XX:+PrintGCTimeStamps:打印程序启动到GC发生的时间,搭配XX:+PrintGCDetails使用。
- -XX:+PrintGCDateStamps:打印GC发生时的时间戳,搭配-XX:+PrintGCDetails使用。
- -XX:+PrintHeapAtGC:打印GC前后的堆信息
- -Xloggc:输出GC到指定路径下的文件中。
- -XX:+TraceClassLoading:监控类的加载。
- -XX:+PrintGCApplicationStoppedTime:打印GC时线程的停顿时间。
- -XX:+PrintGCApplicationConcurrentTime:打印垃圾收集之前应用未中断的执行时间。
- -XX:+PrintReferenceGC:打印回收了多少种不同引用类型的引用。
- -XX:+PrintTenuringDistribution:打印JVM在每次MinorGC后当前使用的Survivor中对象的年龄分布。
- -XX:+UseGCLogFileRotation:启用GC日志文件的自动转储。
- -XX:NumberOfGCLogFiles:设置GC日志文件的循环数目。
- -XX:GCLogFileSize:设置GC日志文件的大小。
- -XX:+UseG1GC:手动指定使用G1收集器执行内存回收任务。
- -XX:G1HeapRegionSize:设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。
- -XX:MaxGCPauseMillis:设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms。
- -XX:ParallelGCThread:设置STW时GC线程数的值。最多设置为8
- -XX:ConcGCThreads:设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
- -XX:InitiatingHeapOccupancyPercent:设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
- -XX:G1NewSizePercent:新生代占用整个堆内存的最小百分比(默认5%)。
- -XX:G1MaxNewSizePercent:新生代占用整个堆内存的最大百分比(默认60%)。
- -XX:G1ReservePercent=10:保留内存区域,防止 to space(Survivor中的to区)溢出。
- -XX:+DisableExplicitGC:禁用hotspot执行System.gc(),默认禁用。 -XX:ReservedCodeCacheSize / -XX:InitialCodeCacheSize:指定代码缓存的大小。
- -XX:+UseCodeCacheFlushing:放弃一些被编译的代码,避免代码缓存被占满时JVM切换到interpreted-only的情况。
- -XX:+DoEscapeAnalysis:开启逃逸分析。
- -XX:+UseBiasedLocking:开启偏向锁。
- -XX:+UseLargePages:开启使用大页面。
- -XX:+PrintTLAB:打印TLAB的使用情况。
- -XX:TLABSize:设置TLAB大小。
Java代码获取JVM参数
Java提供了java.lang.management包用于监视和管理Java虚拟机和Java运行时中的其他组件,它允许本地或远程监控和管理运行的Java虚拟机。其中ManagementFactory类较为常用,另外Runtime类可获取内存、CPU核数等相关的数据。通过使用这些api,可以监控应用服务器的堆内存使用情况,设置一些阈值进行报警等处理。
public class MemoryMonitor {
public static void main(String[] args) {
MemoryMXBean memorymbean = ManagementFactory.getMemoryMXBean();
MemoryUsage usage = memorymbean.getHeapMemoryUsage();
System.out.println("INIT HEAP: " + usage.getInit() / 1024 / 1024 + "m");
System.out.println("MAX HEAP: " + usage.getMax() / 1024 / 1024 + "m");
System.out.println("USE HEAP: " + usage.getUsed() / 1024 / 1024 + "m");
System.out.println("\nFull Information:");
System.out.println("Heap Memory Usage: " + memorymbean.getHeapMemoryUsage());
System.out.println("Non-Heap Memory Usage: " + memorymbean.getNonHeapMemoryUsage());
System.out.println("=======================通过java来获取相关系统状态============================ ");
System.out.println("当前堆内存大小totalMemory " + (int) Runtime.getRuntime().totalMemory() / 1024 / 1024 + "m");// 当前堆内存大小
System.out.println("空闲堆内存大小freeMemory " + (int) Runtime.getRuntime().freeMemory() / 1024 / 1024 + "m");// 空闲堆内存大小
System.out.println("最大可用总堆内存maxMemory " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "m");// 最大可用总堆内存大小
}
}
分析GC日志
GC的分类:
- 新生代收集(Minor GC):只是新生代的垃圾收集
- 老年代收集(Major GC):只是老年代的收集
- 混合收集(Mixed GC):整个新生代和部分老年代的垃圾收集
- 整堆收集(Full GC):收集整个Java堆和方法区
GC分析工具:
GCEasy是一款在线的GC日志分析器,可以通过GC日志分析进行内存泄漏检测、GC暂停原因分析、JVM配置建议优化等功能。
GCEasy官网:gceasy.io/