JVM核心技术(第一篇)
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类加载器
类的生命周期
- 加载:找class文件
- 校验 :验证格式,依赖
- 准备 :静态字段,方法表
- 解析:符号解析为引用
- 初始化 :构造器,静态变量赋值,静态代码块
- 使用
- 卸载
类的加载时机
- 当虚拟机启动时,初始化用户指定的主类,就是启动执行的 main 方法所在的类;
- 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类,就是 new
一个类的时候要初始化; - 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类
- 子类的初始化会触发父类的初始化
- 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,
会触发该接口的初始化 - 使用反射 API 对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用
要么是已经有实例了,要么是静态方法,都需要初始化 - 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的
类
不会初始化(可能会加载)
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化
- 定义对象数组,不会触发该类的初始化
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类
- 通过类名获取 Class 对象,不会触发类的初始化,Hello.class 不会让 Hello 类初始化。
- 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触
发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。Class.forName
(“jvm.Hello”)默认会加载 Hello 类。 - 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作(加载了,但是
不初始化)
三类类加载器
从上到下依次是:
- 启动类加载器(BootstrapClassLoader)
- 扩展类加载器(ExtClassLoader)
- 应用类加载器(AppClassLoader)
类加载器特点:
双亲委派、负责依赖、缓存加载
加载过程:如一个Hello.class文件,不考虑自定义加载器,首先会在AppClassLoader中检查是否已经加载过,如果加载过就不加载了。如果没有加载过,就会拿到父加载器,那么父加载器(ExtClassLoader)就会检查是否加载过,如果没有,就再往上,让BootStrapClassLoader检查是否加载过。
如果还是没有,因为他的上面已经没有父加载器了,那么他就开始自己加载,如果能加载,他就自己加载。不能加载,就下沉到子加载器去加载,一直到最底层,如果没有类加载器能加载就抛出异常ClassNotFoundException
添加引用类的几种方式
- 放到JDK的lib/ext下,或者-Djava.ext.dirs
- java -cp/classpath 或者将class文件放在当前路径
- 自定义ClassLoader加载
- 拿到当前执行类的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 运行模式
- -server:设置 JVM 使用 server 模式,特点是启动速度比较慢,但运行时性能和内存管理效率
很高,适用于生产环境。在具有 64 位能力的 JDK 环境下将默认启用该模式,而忽略 -client 参
数。 - -client :JDK1.7 之前在32位的 x86 机器上的默认值是 -client 选项。设置 JVM 使用 client 模
式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或
者 PC 应用开发和调试。此外,我们知道 JVM 加载字节码后,可以解释执行,也可以编译成本
地代码再执行,所以可以配置 JVM 对字节码的处理模式: - -Xint:在解释模式(interpreted mode)下运行,-Xint 标记会强制 JVM 解释执行所有的字节
码,这当然会降低运行速度,通常低10倍或更多。 - -Xcomp:-Xcomp 参数与-Xint 正好相反,JVM 在第一次使用时会把所有的字节码编译成本地
代码,从而带来最大程度的优化。【注意预热】 - -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