JVM核心技术(第一篇)

279 阅读10分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

Java基础知识

java是一个面向对象的,静态类型,编译执行,有VM/GC和运行时的跨平台的高级语言。

一. 字节码技术

将写好的java文件编译成class

javac .\TestJvm.java

查看字节码

javap -c TestJVM

查看更详细的字节码

javap -c -verbose TestJVM

字节码的运行时结构

JVM是一个基于栈的计算机器。每个线程都有他所对应的线程栈,用于存储栈帧。每一次方法调用,都会创建一个栈帧。

栈帧由操作数栈,局部变量数组以及一个Class引用(也叫动态链接)组成。

  • 操作数栈:每个帧都包含了一个后入先出的栈,称为操作数栈。

  • 局部变量表:用于存放方法参数和内部定义的局部变量。局部变量表的容量以变量槽(Slot)为单位,一个Slot只能存放一个boolean、byte、char、shoert、int、float、reference或returnAddress类型的数据

  • Class引用:指向当前方法在运行时常量池中对应的class

二、JVM类加载器

类的生命周期

  1. 加载:找class文件
  2. 校验 :验证格式,依赖
  3. 准备 :静态字段,方法表
  4. 解析:符号解析为引用
  5. 初始化 :构造器,静态变量赋值,静态代码块
  6. 使用
  7. 卸载
类的加载时机
  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 方法,也不会触发初始化动作(加载了,但是 不初始化)

三类类加载器

从上到下依次是:

  • 启动类加载器(BootstrapClassLoader)
  • 扩展类加载器(ExtClassLoader)
  • 应用类加载器(AppClassLoader)

类加载器特点: ==双亲委派、负责依赖、缓存加载==

加载过程:如一个Hello.class文件,不考虑自定义加载器,首先会在AppClassLoader中检查是否已经加载过,如果加载过就不加载了。如果没有加载过,就会拿到父加载器,那么父加载器(ExtClassLoader)就会检查是否加载过,如果没有,就再往上,让BootStrapClassLoader检查是否加载过。

如果还是没有,因为他的上面已经没有父加载器了,那么他就开始自己加载,如果能加载,他就自己加载。不能加载,就下沉到子加载器去加载,一直到最底层,如果没有类加载器能加载就抛出异常ClassNotFoundException

添加引用类的几种方式

  1. 放到JDK的lib/ext下,或者-Djava.ext.dirs
  2. java -cp/classpath 或者将class文件放在当前路径
  3. 自定义ClassLoader加载
  4. 拿到当前执行类的ClassLoader,反射调用addUrl方法添加jar或路径
public class Test1 {

    public static void main(String[] args) throws MalformedURLException, NoSuchMethodException, ClassNotFoundException, InvocationTargetException, IllegalAccessException {
       String appurl="file:/d:/logs/";
        URLClassLoader classLoader = (URLClassLoader)Test1.class.getClassLoader();
        Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
        addURL.setAccessible(true);
        URL url=new URL(appurl);
        addURL.invoke(classLoader,url);
        Class.forName("Test2");
    }
}

我将Test2.class文件放在d盘的logs文件夹下。

三、JVM内存结构

Jvm整体结构

可以看到,我们的JVM进程里面除了堆还有栈、非堆、JVM自身。而我们的操作系统里还有其他进程。

所以我们设置堆内存的时候,不能设置为机器的内存大小,如4G的机器千万不能把-Xms -Xmx 设置为4G,一般设置为机器内存的60%-70%。

JVM栈结构

栈:线程栈,也叫Java方法栈,每启动一个线程就会创建一个栈,如果使用了JNI方法,就会分配一个单独的本地方法栈。线程执行过程中,一般会有多个方法组成调用关系,如方法A调用方法B,每执行到一个方法,就会创建一个栈帧。

==所有的原生对象类型(如int,long)和对象引用地址都在栈上存储。==

JVM堆结构

堆:==对象、对象成员以及类定义、静态变量、字符串常量池都在堆上。==

元空间

元空间:主要用于存储类的信息、运行时常量池、方法数据、方法代码等 什么是JMM?

Java内存模型。明确定义了不同的线程之间,通过哪些方式,在什么时候可以看到其他线程保存在共享变量中的值;以及如何对共享变量进行同步。JMM规范的是线程间的交互操作。

从抽象上来看,JMM定义了线程和主内存之间的抽象关系。

四、JVM启动参数

JVM启动参数有如下几类:

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

  • 以-D开头的,设置系统属性 如:-Dfile.encoding=UTF-8

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

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

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

  • -XX:key=value 形式, 指定某个选项的值 如:-XX:MaxPermSize=256m

4.1 系统属性参数

-Dfile.encoding=UTF-8 -Duser.timezone=GMT+08

或者通过 System.setProperty("a","A100");设定,Linux上还可以通过a=A100 java XXX 设定。

4.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 等信息。
4.3 堆内存

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

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

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

-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.4 GC相关

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

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

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

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

// Java 11+
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

// Java 12+
-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

4.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/local/ 目录下。

-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,远程调试

4.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