JVM技术

  • Post author:
  • Post category:其他

Java程序的开发运行过程为: 我们利用 JDK (调用 Java API)开发Java程序,编译成字节码或者打包程序 然后可以用 JRE 则启动一个JVM实例,加载、验证、执行 Java 字节码以及依赖库, 运行Java程序 而JVM 将程序和依赖库的Java字节码解析并变成本地代码执行,产生结果

性能调优我们可采用的手段和方式包括:

使用JDWP或开发工具做本地/远程调试

系统和JVM的状态监控,收集分析指标

性能分析: CPU使用情况/内存分配分析

内存分析: Dump分析/GC日志分析

调整JVM启动参数,GC策略等等

字节码

Java bytecode由单字节(byte) 的指令组成,理论上最多支持256个操作码(opcode )实际上Java只使用了200左右的操作码,还有一些操作码则保留给调试操作。

根据指令的性质,主要分为四个大类:

1.栈操作指令,包括与局部变量交互的指令:所有的计算都发生在栈上

2.程序流程控制指令

3.对象操作指令,包括方法调用指令

4.算术运算以及类型转换指令

生成字节码示例:vi Hello.java

public class Hello {
        public static void main(String[] args) {
                Hello obj = new Hello();
        }
}

使用javac进行编译,生成字节码文件:javac Hello.java

使用javap -c Hello查看字节码文件(反编译,使用ASM和Javassist之类的字节码操作工具地实现字节码编辑和修改)

将 class 文件和java源文件归档到一个名为hello.jar 的档案中,通过 e 选项指定jar的启动类 Hello

jar cvfe hello.jar Hello Hello.class Hello.java

 java ‐jar hello.jar    然后通过 ‐jar 选项来执行jar包:

Compiled from "Hello.java"
public class Hello {
  public Hello();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class Hello
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: return
}

运算的时候有一个本地变量表,如上面的#1,可以在本地变量表中找到具体意义。运算时将变量从本地变量表加载到栈上,进行运算。

 使用javap -c -verbose Hello,可以查看更详细的字节码,包括常量池等内容。

Classfile /root/Hello.class
  Last modified Feb 22, 2022; size 272 bytes
  MD5 checksum adc98c8855d7cc843b3556c4e922d2ac
  Compiled from "Hello.java"
public class Hello
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
   #2 = Class              #14            // Hello
   #3 = Methodref          #2.#13         // Hello."<init>":()V
   #4 = Class              #15            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               SourceFile
  #12 = Utf8               Hello.java
  #13 = NameAndType        #5:#6          // "<init>":()V
  #14 = Utf8               Hello
  #15 = Utf8               java/lang/Object
{
  public Hello();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class Hello
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
}
SourceFile: "Hello.java"

算数操作与类型转换

 方法调用的指令

invokestatic:顾名思义,这个指令用于调用某个类的静态方法,这是方法调用指令中最快的一个。

invokespecial:用来调用构造函数,但也可以用于调用同一个类中的private方法,以及可见的超类方法。

invokevirtual:如果是具体类型的目标对象,invokevirtual 用于调用公共,受保护和package级的私有方法。

invokeinterface:当通过接口引用来调用方法时,将会编译为invokeinterface指令。

invokedynamic,:JDK7 新增加的指令,是实现“动态类型语言”( Dynamically TypedLanguage)支持而进行的升级改进,同时也是JDK8以后支持lambda表达式的实现基础。

JVM类加载器

类的生命周期:

1.加载(Loading) :找Class文件

2.验证(Verification) :验证格式、依赖

3.准备(Preparation) :静态字段、方法表

4.解析(Resolution) :符号解析为引用

5.初始化(Initialization) :构造器、静态变量赋值、静态代码块

6.使用(Using )

7.卸载(Unloading)

类加载的时机

1.当虚拟机启动时,初始化用户指定的主类,就是启动执行的main方法所在的类;

2.当遇到用以新建目标类实例的new指令时,初始化new指令的目标类,就是new一个类的时候要初始化;

3.当遇到调用静态方法的指令时,初始化该静态方法所在的类;

4.当遇到访问静态字段的指令时,初始化该静态字段所在的类;

5.子类的初始化会触发父类的初始化;

6.如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;

7.使用反射API对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化;

8.当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。

不会初始化(可能会加载)

1.通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

2.定义对象数组,不会触发该类的初始化。

3.常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。

4.通过类名获取Class对象,不会触发类的初始化,Hello.class 不会让Hello类初始化。

5.通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。

Class.forName(”jvm.Hello” )默认会加载Hello类。

6.通过ClassLoader默认的loadClass方法,也不会触发初始化动作(加载了,但是不初始化)。

三类加载器

1.启动类加载器(bootstrap class loader): 它用来加载 Java 的核心类,是用原生 C++代码来实现的,并不继承自 java.lang.ClassLoader(负责加载JDK中 jre/lib/rt.jar里所有的class)。它可以看做是JVM自带的,我们再代码层面无法直 接获取到启动类加载器的引用,所以不允许直接操作它, 如果打印出来就是个 null 。举例来说,java.lang.String是由启动类加载器加载的,所以
String.class.getClassLoader()就会返回null。但是后面可以看到可以通过命令行
参数影响它加载什么。

2.扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext 或者由java.ext.dirs系统属性指定的目录中的JAR包的类,代码里直接获取它的父
类加载器为null(因为无法拿到启动类加载器)。

3.应用类加载器(app class loader):它负责在JVM启动时加载来自Java命令的­ classpath或者­cp选项、java.class.path系统属性指定的jar包和类路径。在应用程 序代码里可以通过ClassLoader的静态方法getSystemClassLoader()来获取应用 类加载器。如果没有特别指定,则在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载。

扩展类加载器和应用类加载器都是继承制URLClassLoader(Java9以前):

类加载机制有三个特点:

1. 双亲委托:当一个自定义类加载器需要加载一个类,比如java.lang.String,它很 懒,不会一上来就直接试图加载它,而是先委托自己的父加载器去加载,父加载 器如果发现自己还有父加载器,会一直往前找,这样只要上级加载器,比如启动 类加载器已经加载了某个类比如java.lang.String,所有的子加载器都不需要自己 加载了。如果几个类加载器都没有加载到指定名称的类,那么会抛出 ClassNotFountException异常。

2. 负责依赖:如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类 或接口,也会去尝试加载这些依赖项。

3. 缓存加载:为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加
载,那么它会缓存这个加载结果,不会重复加载。

加载器的加载路径:

 显示当前ClassLoader加载了哪些Jar

import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;

public class JvmClassLoaderPrintPath {
    public static void main(String[] args) {
        // 启动类加载器,虽然拿不到启动类加载器,但是可以获取URL
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        System.out.println("启动类加载器");
        for(URL url : urls) {
            System.out.println(" ==> " +url.toExternalForm());
        }
        // 扩展类加载器
        printClassLoader("扩展类加载器", JvmClassLoaderPrintPath.class.getClassLoader().getParent());
        // 应用类加载器
        printClassLoader("应用类加载器", JvmClassLoaderPrintPath.class.getClassLoader());
    }

    public static void printClassLoader(String name, ClassLoader CL){
        if(CL != null) {
            System.out.println(name + " ClassLoader ‐> " + CL.toString()); printURLForClassLoader(CL);
        }else{
            System.out.println(name + " ClassLoader ‐> null");
        }
    }
    public static void printURLForClassLoader(ClassLoader CL){
        Object ucp = insightField(CL,"ucp"); Object path = insightField(ucp,"path");
        ArrayList ps = (ArrayList) path;
        for (Object p : ps){
            System.out.println(" ==> " + p.toString());
        }
    }
    private static Object insightField(Object obj, String fName) {
        try {
            Field f = null;
            if(obj instanceof URLClassLoader){
                f = URLClassLoader.class.getDeclaredField(fName);
            }else{
                f = obj.getClass().getDeclaredField(fName);
            }
            f.setAccessible(true);
            return f.get(obj);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

 在类的启动命令行参数加上 ‐XX:+TraceClassLoading 或者 ‐verbose 即 可,注意需要加载java命令之后,要执行的类名之前,不然不起作用。

java -verbose Hello
java -XX:+TraceClassLoading Hello

怎么自己指定加载哪些类

参数 ‐Dsun.boot.class.path 表示我们要指定启动类加载器加载什么, 最基础的东西都在rt.jar这个包了里,所以一般配置它就够了。需要注意的是因为在 windows系统默认JDK安装路径有个空格,所以需要把整个路径用双引号括起来,如 果路径没有空格,或是Linux/Mac系统,就不需要双引号了。
参数 ‐Djava.ext.dirs 表示扩展类加载器要加载什么,一般情况下不需要的话可以直接配置为空即可。

java -Dsun.boot.class.path=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.322.b06-1.el7_9.x86_64/jre/lib/rt.jar -verbose Hello

自定义类加载器

JVM内存模型

 如果是原生数据类型的局部变量,那么它的内容就全部保留在线程栈上。

如果是对象引用,则栈中的局部变量槽位中保存着对象的引用地址,而实际的对象内容保存在堆中。

对象的成员变量与对象本身一起存储在堆上,不管成员变量的类型是原生数值,还是对象引用。

类的静态变量则和类定义一样都保存在堆中。

方法中使用的原生数据类型和对象引用地址在栈上存储;对象、对象成员与类定义、静态变量在堆上。

堆内存又称为“共享堆”,堆中的所有对象,可以被所有线程访问,只要他们能拿到对象的引用地址。

如果一个线程可以访问某个对象时,也就可以访问该对象的成员变量。

如果两个线程同时调用某个对象的同一方法,则它们都可以访问到这个对象的成员变量,但每个线程的局部变量副本是独立的。

 

 堆内存是所有线程共用的内存空间,JVM将Heap内存分为年轻代( Young generation)和老年代(Old generation,也叫Tenured)两部分。

年轻代还划分为3个内存池,新生代(Edenspace)和存活区( Survivor space ),在大部分GC算法中有2个存活区(S0, S1),在我们可以观察到的任何时刻,S0和S1总有一个是空的,但一般较小,也不浪费多少空间。

Non-Heap本质上还是Heap,只是一般不归GC管理,里面划分为3个内存池。

Metaspace,以前叫持久代( 永久代,Permanentgeneration), Java8换了个名字叫Metaspace.对象的结构,常量池。

CCS, Compressed Class Space,存放class信息的,和Metaspace有交叉。

CodeCache,存放JIT编译器编译后的本地机器代码。

JVM启动参数

以-开头为标准参数,所有的JVM都要实现这些参数,并且向后兼容。

-D设置系统属性。

以-X开头为非标准参数,基本都是传给JVM的,默认JVM实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容。可以使用java-X命令来查看当前JVM支持的非标准参数。

以-XX:开头为非稳定参数,专门用于控制JVM的行为,跟具体的JVM实现有关,随时可能会在下个版本取消。

-XX:+-Flags形式,+-是对布尔值进行开关。

-XX:key=value形式,指定某个选项的值。

1.系统属性参数

 2.运行模式参数

1. -server: 设置JVM使用server模式,特点是启动速度比较慢,但运行时性能和内存管理效率很高,适用于生产环境。在具有64位能力的jDK环境下将默认启用该模式,而忽略-client参数。

2.-client : jDK1.7之前在32位的x86机器上的默认值是-client选项。设置JVM使用client模式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或者PC应用开发和调试。此外,我们知道JVM加载字节码后,可以解释执行,也可以编译成本地代码再执行,所以可以配置JVM对字节码的处理模式:

3. – Xint:在解释模式(interpreted mode )下运行,-Xint 标记会强制JVM解释执行所有的字节码,这当然会降低运行速度,通常低10倍或更多。

4. -Xcomp: -Xcomp 参数与-Xint正好相反,JVM 在第一次使用时会把所有的字节码编译成本地代码,从而带来最大程度的优化。[注意预热 )

5. -Xmixed: -Xmixed 是混合模式,将解释模式和编译模式进行混合使用,有JVM自己决定,这是JVM的默认模式,也是推荐模式。我们使用java -version可以看到mixed mode等信息。

3.堆内存设置参数

-Xmx,指定最大堆内存。如-Xmx4g.这只是限制了Heap部分的最大值为4g。这个内存不包括栈内存,也不包括堆外使用的内存。

-Xms,指定堆内存空间的初始大小。如-Xms4g。而且指定的内存大小,并不是操作系统实际分配的初始值,而是GC先规划好,用到才分配。专用服务器上需要保持-Xms和-Xmx -致,否则应用刚启动可能就有好几个FullGC。当两者配置不一致时,堆内存扩容可能会导致性能抖动。

-Xmn,等价于-XX:NewSize,使用G1垃圾收集器不应该设置该选项,在其他的某些业务场景下可以设置。官方建议设置为-Xmx的1/2 ~ 1/4.

-XX: MaxPermSize=size, 这是JDK1.7之前使用的。Java8默认允许的Meta空间无限大,此参数无效。

-XX: MaxMetaspaceSize=size, Java8默认不限制Meta空间,一般不允许设置该选项。

-XX: MaxDirectMemorySize=size, 系统可以使用的最大堆外内存,这个参数跟-Dsun.nio.MaxDirectMemorySize效果相同。

-Xss,设置每个线程栈的字节数。例如-Xss1m指定线程栈为1MB,, 与-XX:ThreadStackSize=1m等价

4. GC设置参数

-XX: +UseG1GC:使用G1垃圾回收器

-XX: +UseConcMarkSweepGC: 使用CMS垃圾回收器

-XX: +UseSerialGC: 使用串行垃圾回收器

-XX: +UseParallelGC: 使用并行垃圾回收器

// Java 11+

-XX: +UnlockExperimentalVMOptions -XX:+UseZGC

// Java 12+

-XX: +UnlockExperimentalVMOptions -XX:+UseShenandoahGC

5.分析诊断参数

-XX: +-HeapDumpOnOutOfMemoryError 选项,当OutOfMemoryError产生,即内存溢出(堆内存或持久代)时,自动Dump堆内存。

示例用法: java -XX:+HeapDumpOnOutOfMemoryError -Xmx256m ConsumeHeap

-XX: HeapDumpPath 选项,与HeapDumpOnOutOfMemoryError搭配使用,指定内存溢出时Dump文件的目录。

如果没有指定则默认为启动Java程序的工作目录。

示例用法: java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/ ConsumeHeap自动Dump的hprof文件会存储到/usr/ocal/目录下。

-XX: OnError 选项,发生致命错误时(fatal error)执行的脚本。

例如,写一个脚本来记录出错时间,执行一些命令,或者curl一下某个在线报警的url.

示例用法: java -XX:OnError=’ gdb – %p” MyApp

可以发现有一个%p的格式化字符串,表示进程PID。

-XX: OnOutOfMemoryError 选项,抛出OutOfMemoryError错误时执行的脚本。

-XX: ErrorFile=filename 选项,致命错误的日志文件名,绝对路径或者相对路径。

-Xdebug -Xrunjdwp:transport=dt socket,server=y,suspend=n,address=1506, 远程调试

6. JavaAgent参数

Agent 是 JVM 中的一项黑科技, 可以通过无侵入方式来做很多事情,比如注入 AOP 代码,执行统 计等等,权限非常大。这里简单介绍一下配置选项,详细功能需要专门来讲。

设置 agent 的语法如下:

-agentlib:libname[=options] 启用 native 方式的 agent, 参考 LD_LIBRARY_PATH 路径。

-agentpath:pathname[=options] 启用 native 方式的 agent。

-javaagent:jarpath[=options] 启用外部的 agent 库, 比如 pinpoint.jar 等等。

-Xnoagent 则是禁用所有 agent。

以下示例开启 CPU 使用时间抽样分析:
JAVA_OPTS=”-agentlib:hprof=cpu=samples,file=cpu.samples.log”


版权声明:本文为qq_41054749原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。