JVM

1.类文件结构

The Java ® Virtual Machine Specification - Oracle虚拟机规范中定义的类文件结构如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dmpaeJnb-1692550185277)(…\Pictures\blog\calssfile_structure.png)]

  • 魔数(magic):第0-3字节,表示是否是class类型的文件,class类型文件魔数为ca fe ba be。

  • 次版本(minor_version):第4-5字节

  • 主版本(major_version):第6-7字节

  • 常量池长度(constant_pool_count):第8-9字节,常量池#0项不计入,也没有值

  • 常量池(constant_pool):根据tag(1个字节)的值找对应的类型,如下表

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u96zYD5C-1692550185278)(…\Pictures\blog\constant_pool.png)]

  • 访问标识符(acess_flags)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QmLzl555-1692550185279)(…\Pictures\blog\access_flag.png)]

  • 类的继承信息(this_class & super_class & interfaces_count & interfaces)

  • 类的成员变量(field_count & field_info)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RySHFsdF-1692550185280)(…\Pictures\blog\field_info.png)]

  • 方法信息(method_count & method_info)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zAgdyfYK-1692550185281)(…\Pictures\blog\method_info.png)]

  • 附加属性(attribute_count & attribute_info)

    ​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0itdsiZt-1692550185281)(…\Pictures\blog\attribute_info.png)]

2. 字节码指令

从字节码的角度来分析类文件

2.1 javap命令

对于上一小节中的二进制文件分析太过于麻烦,通过JDK提供的反编译命令javap命令来反编译class文件更便于理解阅读。javap命令的用法如下:

javap
-c对代码进行反汇编
p或-private显示所有类和成员
-v或-verbose输出附加信息(行号、本地变量表、反汇编等详细信息)
-public仅显示公共类和成员
-protected显示受保护的类和成员
-l输出行号和本地变量表
-s输出内部类型签名
-sysinfo显示正在处理的类的系统信息(路径、大小、日期、MD5散列)
-constants显示静态最终常量
-classpath path指定查找类文件的位置
-bootclasspath path覆盖引导类文件的位置

以Person类为例子,执行javap -v -p Person.class对应的反编译文件格式如下:

public class Person {
  private String name;
  private Integer age;
  public void sayHello(){
    System.*out*.println("hello");
  }
  public static void main(String[] args) {
    System.*out*.println("Person.main");
  }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mH7ucTz6-1692550185282)(…\Pictures\blog\person_class.png)]

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XtcCQpFy-1692550185282)(…\Pictures\blog\person_class2.png)]

2.2 构造方法

  • ()V类对象的初始化方法。编译器会按照从上往下的顺序,将所有的静态代码块和静态成员赋值的代码,合并为这个特殊的方法,该方法在类的初始化阶段被调用。

  • ()V类的实例对象的初始化方法。编译器会按照从上往下的顺序,收集所有非静态代码块和非静态成员变量赋值的代码,形成这个新的构造方法,但原始构造方法内的代码总在最后,该方法在创建对象时被调用

3) 方法调用

  • final方法、private方法和构造方法都是由invokespecial指令来调用,属于静态绑定,意思就是在编译阶段就能确定调用哪个类的那个方法了。此外,通过super调用父类方法也是invokespecial指令来调用

  • 普通成员方法都是由invokevirtual指令调用,属于动态绑定(多态),在运行时才能确定调用的具体实现。当执行invokevirtual指令时,首先通过栈帧中的对象引用找到对象,分析对象头找到对象实际的Class,通过查找Class中的vtable(虚方法表,在类加载的链接阶段就已经根据方法的重写规则生成好了)得到方法的具体地址来执行。

  • 静态方法是由指令invokestatic调用的,如果通过对象来调用静态方法会产生两条无用指令,所以静态方法应避免使用对象引用来调用,而应使用类对象来调用。

  • new关键字对应的字节码指令如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y0hCsdO9-1692550185282)(…\Pictures\blog\new_class.png)]

4) 异常处理

从字节码角度来看异常的处理:

  • 普通的try-catch

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mz2xAwLx-1692550185283)(…\Pictures\blog\normal_try_catch.png)]

  • 多个catch块和multi-catch的情况:异常槽位共用

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AvFTGNQe-1692550185283)(…\Pictures\blog\multi_try_catch.png)]

  • finally块的情况:可以看出finally中的代码被复制了三份,分别放入了try、catch和catch剩余的异常流程中,并且在catch any这种情况下的异常是通过指令athrow抛出

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6TeLAOVX-1692550185283)(…\Pictures\blog\finally.png)]

    如果finally中出现return语句,那么try中的return会被截胡,并且try中出现异常也会被吞掉,另外需要特别注意的是try中的return语句执行之前将返回的值存入到了局部变量表中

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kW4VHYy5-1692550185284)(…\Pictures\blog\finally2.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IAakvYgT-1692550185284)(…\Pictures\blog\finally3.png)]

3. 编译器处理(语法糖)

编译器在将源码文件转换成字节码的过程中,自动生成和转换了一些代码,也就是所谓的语法糖。

  • 默认构造器:无参构造器首会先调用父类的构造方法super()。
  • 自动拆装箱:比如Integer I = 1àInteger I = Integer.valueOf(1)。
  • 泛型:泛型代码在编译后会进行泛型擦除,实际类型都按照Object类型来处理,所以编译器还额外做了一个类型转换。泛型擦除的是字节码上的泛型信息,LocalVariableTypeTable仍然保留了方法参数泛型的信息。
  • 可变参数:编译器在编译期间会将可变参数转换为数组,比如(String… args) 转换成(String[] args)。
  • foreach:数组的foreach编译时会转换成普通的for循环,而实现了Iterable接口的集合的foreach则会被转换为对迭代器的调用。
  • 枚举类:枚举的实例对象都是静态的final成员,并且在静态块中完成初始化,实例对象都会被放进$VALUES的成员变量中。
  • try-with-resources:对那些实现了AutoCloseable接口的资源对象,使用该语法可以不用写finally块,编译器自动生成关闭资源的代码。
  • 方法重写时的桥接方法:如果子类的返回值是父类返回值的子类,那么编译器会生成一个桥接方法(桥接方法内调用子类覆盖的方法),该方法仅对虚拟机可见,并且与子类的方法没有命名冲突。
  • 匿名内部类:编译器会生成内部类的结构,内部类在引用局部变量时,局部变量必须是final的,因为生成的内部类中将局部变量的值赋给了内部类的属性,所以局部变量不应该发生变化,如果变化,内部类的属性没有机会跟着一起变化。

4. 类加载阶段

类的主动使用,将会导致类的初始化

  • new 直接使用
  • 访问类或接口的静态变量,或者对静态变量的赋值操作
  • 调用静态方法
  • 反射调用类
  • 初始化一个子类
  • 启动类

加载:查找并加载类的二进制数据

链接

  1. 验证:确保被加载类的正确性
  2. 准备:为类的静态变量分配内存,并将其初始化为默认值
  3. 解析:把类中的符号引用转换为直接引用

初始化:为类的静态变量赋予正确的初始值

4.1 加载

  • 将类的字节码加载到方法区中,也就是将静态存储(字节流)转化为方法区的运行时数据结构,也就是通过C++的instanceKlass描述JAVA类,并且在堆中创建类的Class对象,instanceKlass和Class对象互相持有对方的引用。instanceKlass的一些重要字段如下:

    • _java_mirror:java的类镜像,也就是Class对象,作用是将instanceKlass暴露给java使用
    • _super:父类
    • _fields:成员变量
    • _methods:方法
    • _constants:常量池
    • _class_loader:类加载器
    • _vtable:虚方法表
    • _itable:接口方法表
  • 如果父类没有加载,先加载父类

  • jdk1.8中instanceClass存储在方法区(元空间)中,类的静态变量存储在堆中的Class对象中,jdk1.7之前静态变量存储在instanceKlass中

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jpxy8iXk-1692550185285)(…\Pictures\blog\class_load_parse.png)]

4.2 链接

  • 验证

    确保class文件中的信息符合当前虚拟机的要求,包括文件格式、元数据、字节码和符号引用验证等。

  • 准备

    为static变量分配空间,设置默认值

    • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
    • 如果static变量是被final修饰的基本类型或者字符串常量(具有了ConstantValue属性),那么其值在编译阶段就确定了,赋值操作在准备阶段完成
    • static变量是引用类型(跟final修饰无关),赋值会在初始化阶段完成
  • 解析

    主要是将常量池中的符号引用解析为直接引用。也就是将常量池中的符号转换为真实的内存地址。

4.3 初始化

这里的初始化是类对象的初始化,也就是调用**()V**,虚拟机会保证该构造方法的线程安全。类的初始化是懒惰的

  • 导致类初始化的情况:

    • main方法所在的类,总会先被初始化
    • 首次访问类的静态变量或静态方法时
    • 子类初始化时,父类还未初始化时,会触发父类的初始化
    • 子类访问父类的静态变量,只会触发父类的初始化
    • Class.forName()方法
    • new关键字
  • 不会导致类初始化的情况:

    • 访问final修饰的static常量(基本类型或字符串)
    • Person.class不会触发初始化
    • 类加载器的loadClass方法
    • 创建类的数组时不会触发
    • Class.forName方法的第二个参数为false时,触发初始化

5. 类加载器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y1Dbilbo-1692550185285)(…\Pictures\blog\classloader.png)]

5.1 双亲委派

双亲委派是指调用LoadClass方法时,查找类的规则,源码部分如下,防止类重复加载,保证安全

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RfZFIuyv-1692550185286)(…\Pictures\blog\double_father_delegate.png)]

5.2 线程上下文类加载器

在Java中的SPI机制中通过ServiceLoader的load方法加载对应配置文件中的实现类,线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它的底层是通过Class.forName(接口实现类,false,线程上下文加载器)来完成类的加载。

5.3 自定义类加载器

  • 继承ClassLoader父类
  • 重写findClass方法,注意不是重写loadClass,否则不会走双亲委派的流程
  • 读取类的字节码(IO流操作)
  • 调用父类(ClassLoader)的defineClass方法加载类
  • 使用者调用自定义类加载器的loadClass方法来进行类的加载

6. JVM运行时数据区

《Java虚拟机规范》将虚拟机管理的内存划分为以下几个运行时数据区:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rrmWlqUZ-1692550185286)(…\Pictures\blog\jvm_runtime.png)]

6.1 程序计数器

作用就是记住当前线程执行的字节码指令的地址,如果是本地方法,计数器的值为空,该区域不会发生内存溢出

6.2Java虚拟机栈

Java虚拟机栈是每个线程运行时所需要的内存,是线程私有的区域,每个栈由多个栈帧(Frame)组成,每个栈帧对应着每次方法调用时所占用的内存每个线程只能有一个活动栈帧,对应着当前正在执行的方法。当栈帧过多超出栈所允许的最大深度时抛出StackOverflowError,当栈帧过大无法申请到足够的内存时抛出OutofMemoryError。

  • **栈帧(Frame)**中存储了局部变量表、操作数栈(用于计算)、动态链接、方法出口等信息。
  • 局部变量表:方法中定义的局部变量和方法的参数存在该表中,所需的内存在编译期间完成分配,方法运行时不会改变局部变量表的大小,局部变量表中的变量不能直接使用,必须通过相关指令将其加载到操作数栈中作为操作数使用。
  • 操作数栈:以压栈和出栈的方式存储操作数,操作数栈的最大深度在编译时已经确定。
  • 动态链接:每个栈帧中都包含一个指向运行时常量池中该栈帧所属方法的引用,该引用是为了支持方法调用过程中的动态链接。
  • 方法的返回地址:方法退出的两种方式:遇到方法返回的字节码指令和方法遇见异常且该异常没有在方法内得到处理。
  • Java虚拟机栈是方法执行的内存模型,方法调用完成后内存会自动释放,因此该区域不涉及垃圾回收。
  • **逃逸分析:**如果方法内的局部变量没有逃离方法的作用范围,它是线程安全的,如果局部变量引用了对象,并且逃离了方法的作用范围(方法参数和返回值),需要考虑线程安全。

3) 本地方法栈

如果线程执行的方法时native类型的,这些方法就会在本地方法栈中执行。如果是Java方法调用native的方法,会从Java方法动态连接到本地方法。同Java虚拟机栈一样可能会抛出StackOverflowError和OutofMemoryError。

4) Java堆

Java堆是所有线程共享的一块区域Java对象实例都在堆上分配,但是对着JIT编译器的发展和逃逸分析技术的发展成熟,栈上分配、标量替换这类优化技术使得所有对象都在堆上分配不那么绝对了。从垃圾回收的角度看,该区域又被分为Eden、From-Survivor、To-Survior、Old,从内存分配的角度看,堆中可能划分出多个线程私有的分配缓冲区(TLAB),但是不论如何划分,Java堆存储的仍然是对象实例。当堆内存不足时抛出OutofMemoryError

5) 方法区

方法区是各个线程共享的区域,存储编译后的类的版本、字段、类加载器、方法、接口、继承信息和常量池等信息。《Java虚拟机规范》中将方法区描述为堆的一个逻辑部分,但是它却又一个别名叫非堆(Non-Heap),jdk1.7中方法区的实现叫永久代,jdk1.8中方法区的实现叫做元空间,元空间是在本地内存中并且将常量池中的StringTable移入到了堆中(因为放在方法区中只有FullGC时才能回收)。方法区的内存溢出(比如动态生成的类的情况下)在1.8之前叫做OutofMemorError:PermGen space,在1.8之后叫做OutofMemoryError:Metaspace。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gfoaHhoL-1692550185287)(…\Pictures\blog\jvm_heap.png)]

  • 常量池

    .class文件中的constant_pool,虚拟机根据这张常量表找到要执行的类名、方法名、参数类型、字面量等。

  • 运行时常量池

    当类被加载时,常量池中的信息就会放入运行时常量池。并非只有Class文件中的常量池才能进入运行时常量池,运行期间也可能将新的常量加入到池中,比如String的**intern()**方法。

  • StringTable

    • 是一张哈希表,并且长度固定不能扩容。

    • 常量池中的符号在第一次用到时才变为对象(懒惰式)。

    • 利用串池的机制可以避免重复创建字符串对象

    • 字符串变量拼接的原理是StringBuilder(jdk1.8)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-19nUIG5e-1692550185287)(…\Pictures\blog\stringtable1.png)]

    • 字符串常量的拼接原理是编译器的优化

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p8LHopKY-1692550185288)(…\Pictures\blog\stringtable2.png)]

    • 调用字符串的intern()方法可以主动的将串池中还没有的字符串对象放入到串池中并返回串池中的对象。jdk1.8下将字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,并将串池中的对象返回(返回的还是原本的字符串);jdk1.6下将字符串尝试放入串池,如果有则不会放入,如果没有会将该字符串对象复制一份放入串池,并将串池中的对象返回(返回的是复制的字符串对象,与原本的字符串不是同一个)。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NH6hOLQQ-1692550185288)(…\Pictures\blog\stringtable3.png)]

    • -XX:StringTableSize=60013,通过该参数可以调整StringTable的桶个数,桶越少,哈希冲突几率越大,StringTable的效率越低。

6) 直接内存

  • 直接内存(Direct Memory)并不受JVM运行时数据区的一部分,不受JVM内存回收管理

  • 直接内存的分配不受Java堆大小的限制,但还是受到本机总内存大小的限制,直接内存分配回收成本较高,但是读写性能高(少了一次复制的操作)常见于NIO操作,用于数据缓冲区。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xaOxvqDU-1692550185288)(…\Pictures\blog\direct_memory.png)]

  • 直接内存不足时抛出OutofMemoryError: Direct memory space。

  • Java中对直接内存的分配是通过Unsafe类的**setMemory()方法,对直接内存的释放是通过Unsafe的freeMemory()**方法。

7. 垃圾回收

运行时数据区的Java虚拟机栈、本地方法栈和程序计数器是线程私有的,随线程而生,随线程而死,并且其内存的分配在类结构确定时就已知了,其内存分配和回收是确定的,无需考虑垃圾回收。但是堆和方法区就不一样了,一个接口的多个实现类需要的内存可能不一样,只有在程序运行期间才能确定,这部分内存的分配和回收都是动态的。因此GC主要回收的内存是堆和方法区

7.1 如何判断对象是垃圾

  • 引用计数法

    是给对象添加一个引用计数器,每当该对象被引用时,计数器加一,计数器为0时对象可以被回收,Java没有采用这种方式,因为这种方式很难解决对象之间互相引用的情况。

  • 可达性分析

    Java采用可达性分析来寻找所有存活的对象,以GC Root为起点向下搜索,如果GC Root到对象不可达,对象就能够被回收。由BootStrapClassLoader加载的核心类、活动线程中使用的对象、正在使用的锁对象、栈帧中局部变量表引用的对象、类的静态属性和常量引用的对象、本地方法栈中引用的对象等可以作为GC Root。

  • Java中的四种引用

    • 强引用

      变量的赋值操作就是强引用。通过GC Root的引用链引用不到的对象,才能被回收

      SourceTarget st = new SourceTarget()
      
    • 软引用(SoftReference)

      软引用引用的对象在首次垃圾回收时不会回收该对象如果内存不足,再次回收时才会释放该对象。软引用本身需要配合引用队列来释放。软引用的典型例子是Java中的反射

      SoftReference sr = new SoftReference(new SourceTarget())
      
    • 弱引用(WeakReference)

      弱引用引用的对象只要发生垃圾回收就会回收该对象。弱引用本身需要配合引用队列来释放。弱引用典型的例子是TreadLocalMap的内部类Entry对象。

      WeakReference wr = new WeakReference(new SourceTarget())
      
    • 虚引用(PhantomReference)

      虚引用引用的对象在垃圾回收时就会被回收虚引用必须配合引用队列来使用,当虚引用引用的对象被回收时,Reference Handler线程将虚引用对象入队,这样就知道哪些对象被回收了,从而对它们关联的资源做进一步的处理。虚引用的典型例子是Cleaner释放DirectByteBuffer关联的直接内存

      PhantomReference pr = new PhantomReference(new SourceTarget(), new ReferenceQueue())
      

7.2 垃圾回收算法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VJgCsn25-1692550185289)(…\Pictures\blog\gc1.png)]

  • 标记清除(Mark Sweep)

    • 首先标记出所有需要回收的对象,标记完成后统一回收被标记的对象。
    • 会产生内存碎片(在分配大对象时可能无法找到足够连续的内存而导致提前GC)
  • 复制(Copy)

    • 将内存分为两块,当一块内存用完了,就将存活对象移动到另外一块上,再把已经使用过的内存一次性清理掉。
    • 不会产生内存碎片,但是会浪费部分的内存。
    • 适用于新生代垃圾回收(新生代对象朝生夕死,对象存活率低,复制成本低)
  • 标记整理(Mark Compact)

    • 与标记清除一样,不过不是直接回收对象,而是让所有存活对象都向一端移动,然后直接清理边界以外的内存。
    • 效率较低(需要移动对象),不会产生内存碎片
    • 适用于老年代的回收(老年代都是不易回收的对象,对象存活率较高,采用复制算法复制成本高)
  • 分代收集

    分代收集其实就是上面三种算法的组合,根据堆中不同分区的特点采用最适当的收集算法,对于大批对象死去、少量对象存活的新生代采用复制算法,对于对象存活率高、没有额外空间分配的老年代采用标记清除或标记整理算法。

7.3 垃圾收集器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vuptXvmZ-1692550185289)(…\Pictures\blog\gc_garbage.png)]

7.3.1 Serial和 Serial Old(串行)
  • Serial是一种新生代单线程收集器,采用复制算法,垃圾收集时需要STW
  • Serial Old是Serial的老年代版本,采用标记整理算法,运行过程和Serial一样。
  • 使用参数-XX:+UserSerialGC或-XX:+UseSerialOldGC启用,两者开启一个另一个默认也会开启。
  • ParNew****收集器是Serial的多线程版本,采用复制算法。
7.4.1 Parallel Scavenge和Parllel Old(吞吐量优先)
  • Parallel Scavenge是新生代收集器,采用复制算法,是并行的多线程收集器**,相比ParNew更关注系统的吞吐量,需要STW。**
  • Parallel Old是Parallel Scavenge的老年代版本*,采用标记整理算法,也是更加关注吞吐量。**
  • 吞吐量**=用户代码执行时间/(用户代码执行时间+垃圾收集时间),吞吐量越大意味着垃圾收集的时间越短,参数-XX:GCTimeRatio=99表示垃圾收集时间的百分比,可以设置吞吐量大小,计算公式为1/(1+n),默认为99,垃圾收集的时间为1%,也就是吞吐量为99%。这个参数受最大垃圾收集停顿-XX:MaxGCPauseMills=200ms的影响,(堆内存越大,垃圾收集的次数越少,吞吐量就越高,但是垃圾收集时的停顿时间就会越大)。
7.4.2 CMS(响应时间优先)
  • Concurrent Mark Sweep是以(老年代)停顿时间为目标并发类收集器,采用标记清除算法。CMS虽然是老年代的回收器,但是却需要扫描新生代中的对象(原因是老年代中的对象可能被新生代所引用),CMS的执行流程如下:
    1. 初始标记标记GC Roots直接关联对象,不用Tracing,以及新生代中引用的老年代对象。速度很快。该阶段需要STW
    2. 并发标记进行GC Roots Tracing,此阶段和用户线程并发执行,并且此阶段对象的状态很大可能会发生变化(新生代晋升到老年代、大对象直接分配到老年代、对象的引用关系可能发生变化),虚拟机会将并发标记阶段老年代中发生状态改变的对象记录到卡表(解决跨代引用问题,是一个数组,数组的每一个元素表示一段内存地址,如果该区域有跨代引用,卡表的元素标识为1,表示脏页,否则为0)中。该阶段由于GC线程的加入,吞吐量会有所下降。
    3. 并发预清理标记在并发标记阶段老年代为脏页的卡表和幸存区中引用的老年代对象。
    4. 可中断并发预清理并发预清理后如果Eden区内存大于2M,则进入该阶段,目的是在重新标记之前尽可能的进行一次minor gc,以减少重新标记时STW的时间。相关参数说明见jvm参数章节。
    5. 重新标记重新扫描GC Root、新生代以及脏页中的对象。如果之前进行过minor gc则这一步扫描新生代的时间就会缩短。该阶段需要STW
    6. 并发回收清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为浮动垃圾
    7. 并发重置重置CMS相关数据,为下一次回收做准备。
  • 异常情况
    • 并发失败:如果执行垃圾收集时,新的大对象直接分配到老年代,但是老年代却没有足够的空间,会发生并发失败(Concurrent mode failure),会触发Full GC。**
    • ** 元空间内存不足:默认CMS不会回收元空间,当元空间内存不足时触发Full GC。
    • 晋升失败:新生代晋升到老年代时,老年代内存不足,会发生并发失败
  • 注意事项:CMS对CPU资源特别敏感,并发过程会降低吞吐量
7.4.5 G1

所谓Garbage First就是优先回收垃圾最多(回收价值高)的Region,仍然保留了分代收集的思想,采用标记整理算法,可以同时对新生代和老年代进行回收,G1中将堆内存划分为大小相等的Region,大小在1-32M之间,如果对象超过了Region的50%,不是让大对象进入老年代,而是提供了专门的Region来存放,在新生代或老年代回收时,会顺带将大对象Region一起回收。G1中的新生代和老年代只是逻辑上的概念。G1可以让我们设置一个预期的停顿时间,通过追踪每个Region的可回收对象的大小和预估时间,在垃圾回收时有选择的回收,从而达到设置的停顿时间,但并不是一定能达到该设置值,会尽量做到,并且还能兼顾吞吐量

7.4 垃圾回收调优

7.4.1 GC分类
  • 部分收集(Partial GC)
    • Minor GCYoung GC,只是新生代的垃圾收集。
    • Major GCOld GC,只是老年代的垃圾收集。只有CMS GC会有单独收集老年代的行为,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
    • Mixed GC:只有G1会有这种行为,收集整个新生代和部分的老年代。
  • 整堆收集(Full GC):整个堆和方法区的垃圾收集。下面几种情况会触发Full GC
    • 老年代或方法区空间不足。
    • 大对象直接进入老年代,而老年代的可用空间不足。
    • Minor GC后进入老年代的对象的平均大小大于老年代的可用内存时。
    • 显示调用System.gc()方法。
7.4.2 GC日志
  • 日志格式

    垃圾收集器日志中的表示
    SerialDefNew (Default New Generation)
    ParNewParNew
    Parallel ScavengePSYoungGen
    Parallel OldParOldGen
    CMSCMS
    G1garbage-first heap

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a1G1gufu-1692550185289)(…\Pictures\blog\gc_log1.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RenavLpA-1692550185290)(…\Pictures\blog\gc_log2.png)]

  • 日志时间

    • user:
    • sys:
    • real:
7.4.3 如何选择垃圾回收器
  • 优先调整堆大小让JVM自适应
  • 内存小于100M或单核单机程序,并且没有停顿时间要求,选择串行收集器
  • 多核CPU、高吞吐量、允许停顿时间超过1s,选择并行或者JVM自己选
  • 多核CPU、快速响应,选择并发收集器,推荐G1。

8. JVM参数

8.1 标准参数

标准参数以-开头,所有的JVM都会实现这些参数的功能而且向后兼容,运行java命令可以看到所有的标准选项。

用法:java [-options] class [args…]java [-options] -jar jarfile [args…]

选项说明
-client设置jvm使用client模式,启动快但运行时性能和内存管理效率不高,仅在32位系统时有效
-server设置jvm使用server模式,启动慢,但运行时性能和内存管理效率很高,64位系统下的默认模式
-cp classpath -classpath classpath告知jvm搜索目录名、jar文档名、zip文档名,多个路径用分号分隔。使用-classpath后在环境变量中定义的classpath将不会被搜索,如果-classpath和环境变量中都没有设置,则jvm使用当前路径(.)作为类搜索路径
-DpropName=value定义系统的全局属性值。如果value中有空格使用双引号括起来。等同于代码中的System.setProperty(“propertyName”,”value”)
-verbose:class -verbose:gc -verbose:jni输出jvm载入类的信息,当jvm找不到类或类冲突时可以进行诊断。 输出每次GC的相关情况。 输出native方法调用的相关情况,用于诊断jni调用错误
-agentlib:libname[=options]用于装载本地lib包,libname是本地代理库文件名,默认搜索环境变量PATH中的路径,options是传给本地库启动时的参数,多个参数用逗号分隔
-agentpath:pathname=[options]按全路径装载本地库,不在搜索环境变量PATH中的路径
-version输出java的版本信息
-help输出标准参数及其描述
-X输出非标准参数列表及其描述

8.2 非标准参数

非标准参数以-X开头,是在标准参数上扩展的参数,功能比较稳定,但后续版本可能会变化,运行java -X命令可以看到所有非标准参数。

选项说明
-Xmixed混合模式执行,默认值
-Xint仅解释模式执行,
-Xcomp仅采用即时编译器模式
-Xms设置初始堆大小
-Xmx设置最大堆大小
-Xss设置线程栈大小,默认1M
-Xloggc:file与-verbose:gc类似,将每次GC的情况记录到文件中,优先级高于-verbose:gc
-Xprof跟踪正在运行的程序,并将数据在标准输出中输出
-Xnoclassgc关闭针对class的gc功能,可能导致内存溢出
-Xincgc开启增量gc,默认是关闭的,可以减少gc停顿时间,但吞吐量会相应的下降

8.3 非Stable参数

  • 非Stable参数以-XX开头,是使用最多的参数类型,这类选项属于实验性的,不稳定,多用于jvm调优和Debug

  • 使用方法

    • -XX:+ 针对布尔值,表示启用选项
    • -XX:- 针对布尔值,表示禁用选项
    • -XX:= 给选项设置数字类型的值,可以跟单位,比如48M,1g
    • -XX:= 给选项设置字符串类型的值
  • 非Stable参数可以大致分为三类

    • 性能参数:用于JVM的性能调优和内存分配控制,比如堆栈大小

      参数含义
      -XX:ThreadStackSize=1M设置线程的栈大小,0表示使用系统默认值,等价于-Xss1M
      -XX:InitialHeapSize=200M设置初始堆大小,0表示使用系统默认值,等价于-Xms200M
      -XX:MaxHeapSize=1024M设置最大堆大小,等价于-Xmx100M
      -XX:NewSize=50M设置Young区初始大小
      -XX:MaxNewSize=300M设置Young区最大值
      -XX:SurvivorRatio=8设置Eden区与一个Survivor区的比值,默认为8
      -XX:NewRatio=4设置Old区和Young区的比值
      -XX:+UseAdaptiveSizePolicy设置自动选择各区大小比例,启用后Young区大小、Eden区和Survivor区比例以及晋升Old区的年龄等参数会被自动调整
      -XX:PretenureSizeThreshold=512设置大小超过此阈值的对象直接分配在Old区,单位是字节,只对Serial、ParNew收集器有效
      -XX:MaxTenuringThreshold=15设置对象年龄大于该阈值时进入老年代,默认值15
      -XX:+PrintTenuringDistribution设置每次Minor GC后打印Survivor区中对象的年龄分布
      -XX:TargetSurvivorRatio=50设置Minor GC后Survivor区中占用空间的期望比例
      -XX:MetaspaceSize=256M设置元空间初始大小
      -XX:MaxMetaspaceSize=1024M设置元空间最大值,默认使用系统本地内存,也就是没有限制
      -XX:+UseCompressedOops压缩对象指针(64位系统默认开启,是真实地址换算后的地址)
      -XX:+UseCompressedClassPointers压缩类指针(64位系统默认开启)
      -XX:CompressedClassSpaceSize=1G设置Klass Metaspace的大小,默认1G
      -XX:MaxDirectMemorySize=0设置直接内存大小,默认与堆最大值一样
    • 行为参数:用于改变JVM的基础行为,比如GC的算法

      参数含义
      -XX:+UseSerialGC设置Young区使用Seria同时Old区自动的使用SerialOld,是Client模式下默认的垃圾收集器
      -XX:+UseParNewGC设置Young区使用ParNew,是Serial的多线程版本
      -XX:ParallelGCThreads=6设置Young区并行收集器的线程数量,默认同CPU核数相同,生效与并行收集器
      -XX:+UseParallelGC设置Young区使用Parallel Scavenge(更注重吞吐量),与ParallelOld互相激活,JDK8默认的收集器。可以使用参数-XX:+UseAdaptiveSizePolicy来设置自动调整堆区大小等参数
      -XX:+UseParallelOldGC设置Old区使用ParallelOld收集器,与Parallel Scavenge相互激活
      -XX:MaxGCPauseMillis=200设置最大停顿时间,单位为毫秒
      -XX:GCTimeRatio=99设置吞吐量大小(应用运行时间/(垃圾收集时间+应用运行时间)),计算方式为1/(N+1),取值为(0,100),默认99也就是垃圾回收时间不超过1%
      -XX:+UseConcMarkSweepGC设置使用ParNew+CMS+SerialOld,开启后Young区自动使用ParNew
      -XX:+CMSConcurrentMTEnabled -XX:ConcGCThreads=3并发标记阶段采用多线程,默认开启 并发标记阶段线程数=(ParallelGCThreads+3)/4
      -XX:+CMSPrecleaningEnabled是否需要进行并发预处理,默认开启
      -XX: CMSScheduleRemarkEdenSizeThreshold=2M并发预处理之后如果Eden区的内存大于2M,就会进入可中断的并发预处理阶段。
      -XX: CMSMaxAbortablePrecleanLoops=0 -XX: CMSMaxAbortablePrecleanTime=5000 -XX: CMSScheduleRemarkEdenPenetration=50可中断并发预处理的执行次数, 可中断并发预处理的执行时间(ms), 可中断并发预处理时Eden区的使用率。 这三个参数是可中断并发预处理阶段的停止条件
      -XX:+CMSScavengeBeforeRemark在重新标记阶段之前进行一次Minor GC,目的是为了减少根对象的扫描数,提高重新标记的效率,默认关闭
      -XX:+CMSParallelRemarkEnabled重新标记阶段使用并行标记降低停顿时间,默认开启
      -XX: ParallelGCThreads=10并行收集阶段线程数
      -XX:+CMSClassUnloadingEnabled是否对方法区进行回收
      -XX: +CMSIncrementalMode是否开启增量模式,默认关闭,不提倡使用
      -XX:+UseCMSInitiatingOccupancyOnly -XX: CMSInitiatingOccupancyFraction=92UseCMSInitiatingOccupancyOnly参数默认关闭,开启后,CMSInitiatingOccupancyFraction参数才会生效,设置堆内存使用率达到该阈值后开启回收(由于垃圾收集阶段并发处理,CMS不能等老年代被填满了之后才开启回收,需要给用户线程预留一定的内存),如果内存增长缓慢,则阈值可以设置稍大一些来减少老年代回收的次数,如果内存增长很快,则阈值应该减小,以避免频繁触发老年代串行收集器,因此该选项可以有效降低Full Gc的频率
      -XX:+UseCMSCompactAtFullCollection设置在Full GC后对内存空间进行压缩整理,来避免内存碎片对内存分配的影响(为了解决大对象无法分配而不得不提前进行Full GC),该过程无法并发执行,会导致停顿时间变长。默认开启
      -XX:CMSFullGCsBeforeCompaction=n设置多少次不压缩的Full GC后对内存空间进行压缩整理,默认为0,表示每次Full GC时都进行一次碎片整理。
      -XX:+UseG1GC设置使用G1
      -XX:G1HeapRegionSize=2M设置每个Region的大小,值是2的幂,范围是1M到32M之间,目标是根据最小的堆大小划分出约2048个区域,默认值为堆的1/2000
      -XX:MaxGCPauseMillis=200设置最大停顿时间(不能保证达到此值),默认200ms
      -XX:ConcGCThreads=2设置并发标记的线程数,一般为ParallelGCThreads的1/4左右
      -XX:ParallelGCThread=6设置STW时GC线程数最多为8
      -XX:InitiatingHeapOccupancyPercent=n设置触发并发GC周期的堆占用率阈值,默认45,超过此值就会触发GC
      -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60设置新生代占堆内存的最小百分比和最大百分比,默认分别为5%和60%
    • 调试参数:用于监控、打印和输出JVM参数,显示JVM更详细的信息

      参数含义
      -XX:+PrintFlagsInitial打印出所有XX选项的默认值
      -XX:+PrintFlagsFinal打印出所有XX选项在运行时生效的值
      -XX:+PrintVMOptions打印JVM的参数
      -XX:+PrintCommandLineFlags在程序运行前打印出用户手动设置或JVM自动设置的XX选项
      -XX:+PrintGCApplicationStoppedTime打印GC时线程的停顿时间
      -XX:+PrintGCApplicationConcurrenTime打印垃圾收集之前应用未中断的执行时间
      -XX:+PrintGC打印简化的GC日志,等同于-verbose:gc
      -XX:+PrintGCDetails打印GC详细日志
      -XX:+PrintGCTimeStamps输出GC的时间戳
      -XX:+PrintGCDateStamps以日期的形式输出GC的时间戳
      -XX:+PrintHeapAtGC每一次GC前后打印堆信息
      -XX:GclogFileSize=1M设置GC日志文件大小,-Xloggc:设置日志文件
      -XX:NumberOfGClogFiles=1GC日志文件的循环数目
      -XX:+UseGClogFileRotation启用GC日志文件的自动转储
      -XX:+PrintReferenceGC记录回收了多少中不同引用类型的引用
      -XX:+TraceClassLoading追踪类的加载信息
      -XX:+TraceClassUnloading追踪类的卸载信息
      -XX:+TraceClassResolution追踪常量池

9 调试命令和工具

9.1 Linux分析命令

  • CPU

    top命令是常用的CPU性能分析工具,能够实时显示系统中各个进程的资源占用状况,默认按照CPU使用率从高到低展示输出

    #使用方式和参数
    top [-d 秒数] 或者 top [-bnp]
    #-d 可以接秒数,是整个进程界面更新的秒数,默认5秒
    #-b 以批量的方式执行top,通常会搭配数据流重定向来将批量的结果输出为文件
    #-n 与-b搭配,表示需要执行几次top的输出结果
    #-p 指定某个PID来执行检测
    #在top的执行过程中,可以使用按键P来以CPU使用率排序,按键P表示以内存使用率来排序,按键N以PID排序,按键T以进程使用的CPU时间累计排序,按键k给某个进程一个信号,按键r给某个进程重新指定一个nice值,按键q退出
    
    top -d 3
    top -b -n 2 >> /tmp/top.txt
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sk0LQ4vC-1692550185290)(…\Pictures\blog\top_command.png)]

    • 上半部界面为整个系统的资源使用状态
      • 第一行(top):目前的时间,开机到目前所经过的时间,已经登录到系统的用户人数,系统在1分钟、5分钟和15分钟的平均任务负载。
      • 第二行(Tasks):进程总量,不同状态下进程数量的统计,zombie表示僵尸进程,需要特别注意。
      • 第三行(%Cpu(s)):显示CPU的整体负载,需要注意的是wa,该项目代表I/O wait,另外,如果是多核设备,按下数字键1来查看不同CPU的负载率。
      • 第四第五行:物理内存和虚拟内存的使用情况,swap的使用量尽量少,如果swap被用的很多,表示系统物理内存不足了。
      • 第六行:空白行,是当在top进程中输入命令时,显示状态的地方
    • 下半部界面则是每个进程使用的资源情况
      • PID:进程id
      • USER:进程所有者的用户名
      • PR:进程优先级
      • NI:nice值,负值表示高优先级,正值表示低优先级
      • VIRT:进程使用的虚拟内存总量,单位Kb
      • SHR:共享内存大小
      • %CPU:cpu使用率
      • %MEM:内存使用率
      • TIME+:进程使用的CPU时间累加,单位1/100秒
      • COMMAND:命令名称
  • 内存

    free命令可以显示当前内存的使用,-h参数表示人类可读性

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oG7AQa5i-1692550185290)(…\Pictures\blog\command_free.png)]

    状态监控参数说明
    total内存总数
    used已使用内存数
    free空闲内存数
    shared被共享使用的物理内存大小
    buff/cache被buffer和cache使用的物理内存大小
    available还可以被应用程序使用的物理内存大小
  • 磁盘

    df 命令可以查看设备存储状况

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oo1HsfFZ-1692550185290)(…\Pictures\blog\command_df.png)]

    #-a 全部文件系统,包括虚拟文件系统
    #-B 指定显示的单位
    #-h 人类易读性
    #-H 和-h相似,不过1K=1000byte
    #-k 以字节显示
    #-i 列出inode信息
    #-l 显示本地文件系统
    #-p 使用POSIX规范输出
    #-t 打印指定的文件系统类型
    #-T 显示文件系统类型
    #-x 不打印指定的文件系统类型
    
  • 网络

    dstat可以检测CPU、磁盘、网络流量、IO、内存等,支持即时刷新,界面也挺友好。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d5kiYN7f-1692550185291)(…\Pictures\blog\command_dstat.png)]

    #-c CPU统计,usr用户占比,sys系统占比,idl空闲占比,wai等待次数,hiq硬中断次数,siq软中断次数
    #-C PID 查看某个或多个CPU信息,-C total是全部合计,比如-C 0,1
    #-l CPU平均负载统计,1m,5m,15m平均负载
    #-g 换页统计,out内存写入磁盘,in磁盘写入内存
    #-d 磁盘统计,read读取速度,writ写入速度
    #-m 内存统计
    #-n 网卡流量统计
    #-N ens33 指定网卡流量统计
    #-r --io IO统计信息(已完成的)
    #-p 进程数
    #-tcp tcp统计
    #-udp udp统计
    #-socket socket统计
    

9.2 JVM问题定位命令

  • jps:查询当前机器所有Java进程信息

    #语法
    jps [-q] [-mlvV] [hostid]
    #-q 仅显示进程id
    #-mlvV 可以指定这些参数的任意组合
    	#-m 显示传入main()的参数
    	#-l 输出完整的包名,主类名,jar文件的完整路径
    	#-v 显示jvm参数
    	#-V 显示通过flag文件传递的参数
    #hostid 不指定默认本机,可以指定远程主机
    
  • jinfo;实时查看和调整虚拟机运行参数

    #语法 
    jinfo [options] pid
    #-flag name 查看指定name的参数
    #-flag [+|-]name 启用或者禁用指定name的参数
    #-flag name=value 修改指定name的参数的值
    #-flags 显示全部的配置参数
    #-sysprops 以键值对的形式显示当前虚拟机的全部系统属性
    

    需要注意的时,只有那些标记为manageable的参数才能被动态修改。jps可以查看启动时显式指定的配置参数,而jinfo可以查看到没有显式指定的参数,另外还可以看到系统的属性信息。

  • jstat:监控虚拟机各种运行状态信息,比如类加载、内存、垃圾收集、即时编译等运行状态信息

    #语法
    jstat outputOptions [-t] [-h <lines>] [<pid>] [<interval> [<count>]]
    #-t 把时间戳列显示为第一列,该时间戳是虚拟机运行到现在的描述
    #-h number 每显示number行显示一次表头
    #-pid 进程号
    #inteval 显示信息的间隔时间
    #count 显示数据的次数
    ####输出选项outputOptions 
    #-class 显示类加载、卸载数量、总空间和装载耗时的统计信息
    #-compiler 显示即时编译的方法、耗时等
    #-gc 显示堆中各个区域内存使用和垃圾回收的统计信息
    #-gcutil 显示垃圾收集信息的摘要
    #-gccapacity 显示堆中各个区域容量及其对应空间的统计信息
    #-gccause 同gcutil并且显示最近和当前垃圾回收的原因
    #-gcnew 显示新生代垃圾回收统计信息
    #-gcold 显示老年代和元空间的垃圾回收统计信息
    #-gcnewcapacity 显示新生代大小及其对应空间的统计信息
    #-gcoldcapacity 显示老年代的大小统计信息
    #-gcmetacapacity 显示元空间的大小统计信息
    #-printcomplilation 显示即时编译方法的统计信息
    
  • jstack:查看线程栈信息

    #语法
    jstack [options] pid
    #-F 如果java进程由于挂起而没有任何响应,强制显示线程快照信息
    #-l 显示锁的信息
    #-m 显示混合的栈帧信息,(虚拟机栈和本地方法栈)
    
  • jmap:查看堆信息

    #语法
    jmap [option] pid
    #-heap 显示垃圾回收算法信息,堆的配置信息,堆的内存空间使用信息(分代情况、每个代的总容量、已使用内存、可使用内存)
    #-histo[:live] 显示堆中对象的统计信息:对象数量、占用内存大小、类的全限定名,如果指定了live参数,只计算活动的对象
    #-clstats 显示堆中元空间的类加载器信息,类加载器地址、已加载类数量、该类加载器加载的所有类的元数据所占的字节数、父类加载器地址、是否存活、类加载器类名
    #-finalizerinfo 显示在引用队列中等待Finalizer线程执行finalize方法的对象
    #-dump:[live,]format=b,file=filename  生成堆转储快照dump文件,如果指定了live则只转储活动对象,format=b表示以hprof二进制格式转储,file用于指定快照文件的文件名
    

9.3 常用工具

  • jconsole

  • jvisualvm

  • mat

  • arthas

coldcapacity 显示老年代的大小统计信息
#-gcmetacapacity 显示元空间的大小统计信息
#-printcomplilation 显示即时编译方法的统计信息


- jstack:查看线程栈信息

```shell
#语法
jstack [options] pid
#-F 如果java进程由于挂起而没有任何响应,强制显示线程快照信息
#-l 显示锁的信息
#-m 显示混合的栈帧信息,(虚拟机栈和本地方法栈)
  • jmap:查看堆信息

    #语法
    jmap [option] pid
    #-heap 显示垃圾回收算法信息,堆的配置信息,堆的内存空间使用信息(分代情况、每个代的总容量、已使用内存、可使用内存)
    #-histo[:live] 显示堆中对象的统计信息:对象数量、占用内存大小、类的全限定名,如果指定了live参数,只计算活动的对象
    #-clstats 显示堆中元空间的类加载器信息,类加载器地址、已加载类数量、该类加载器加载的所有类的元数据所占的字节数、父类加载器地址、是否存活、类加载器类名
    #-finalizerinfo 显示在引用队列中等待Finalizer线程执行finalize方法的对象
    #-dump:[live,]format=b,file=filename  生成堆转储快照dump文件,如果指定了live则只转储活动对象,format=b表示以hprof二进制格式转储,file用于指定快照文件的文件名
    

9.3 常用工具

  • jconsole

  • jvisualvm

  • mat

  • arthas

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐