JVM:全称 Java Virtual Machine,一个虚拟计算机,Java 程序的运行环境(Java二进制字节码的运行环境)
特点:
JVM 结构:
Java 代码执行流程:Java 程序(.java) --(编译)--> 字节码文件(.class)--(解释执行/JIT)--> 操作系统(Win,Linux)
JVM、JRE、JDK 对比:
Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,Java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构
JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡
内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区。
JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行
Program Counter Register 程序计数器(寄存器)
作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空)
原理:
特点:
Java 虚拟机栈:Java Virtual Machine Stacks,每个线程运行时所需要的内存 异常:
java.lang.StackOverflowError
设置栈内存大小:-Xss size
-Xss 1024k
(在 VM options 中设置)
虚拟机栈特点:
异常:
线程运行诊断:
本地方法栈:Native Method Stacks, 为虚拟机执行本地方法(native方法)时提供服务的
native
关键字修饰,例如:Object类中的wait()、clone()、hashCode等public final native void wait();
public native int hashCode();
protected native Object clone();
当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限
堆:Heap ,是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题 异常:java.lang.OutOfMemoryError:java heap space
存放哪些资源:
内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常
设置堆内存指令:-Xmx Size
/**
* 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
* -Xmx8m
*/
public class Demo1_5 {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a); // hello, hellohello, hellohellohellohello ...
a = a + a; // hellohellohellohello
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}
jhsdb jmap --heap --pid 进程id
在 Java7 中堆内会存在年轻代、老年代和方法区(永久代),Java8 永久代被元空间代替了
分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能
public static void main(String[] args) {
// 返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
// 返回Java虚拟机使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M
System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M
}
字符串常量池(String Pool / StringTable / 串池)存储的是 String 对象的直接引用或者对象,即保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定,字符串常量池类似于 Java 系统级别提供的缓存,存放对象和引用 StringTable,类似 HashTable 结构,通过
-XX:StringTableSize
设置大小,JDK 1.8 中默认 60013
// 字符串常量池(StringTable): [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
}
字节码:Java 反编译指令javap -v 文件名.class
//常量池
// 常量池中的信息,都会被加载到运行时常量池中,
// 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象,是懒惰的
Constant pool:
#1 = Methodref #12.#36 // java/lang/Object."<init>":()V
#2 = String #37 // a
#3 = String #38 // b
#4 = String #39 // ab
//运行代码
0: ldc #2 // String a
2: astore_1 //存入局部变量表slot 1号位
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
// ldc #2 会把 a 符号变为 "a" 字符串对象,StringTable: ["a"]
// ldc #3 会把 b 符号变为 "b" 字符串对象,StringTable: ["a", "b"]
// ldc #4 会把 ab 符号变为 "ab" 字符串对象,StringTable: ["a", "b" ,"ab"]
//局部变量表(栈)
LocalVariableTable:
Start Length Slot Name Signature
0 51 0 args [Ljava/lang/String;
3 48 1 s1 Ljava/lang/String;
6 45 2 s2 Ljava/lang/String;
9 42 3 s3 Ljava/lang/String;
// 字符串常量池(StringTable): [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
// new StringBuilder().append("a").append("b").toString() --> new String("ab") 堆中
String s4 = s1 + s2; //字符串变量 // 返回的是堆内地址
// javac 在编译期间的优化,结果已经在编译期确定为ab
String s5 = "a" + "b"; //字符串常量
System.out.println(s3 == s4); // F
System.out.println(s3 == s5); // T
}
JDK 1.8:将这个字符串对象尝试放入串池,如果 String Pool 中:
JDK 1.6:将这个字符串对象尝试放入串池,如果 String Pool 中:
// StringTable: ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab"; // StringTable: ["ab"]
// 堆 new String("a") new String("b") new StringBuilder() new String("ab")
// StringTable: ["ab", "a", "b"]
String s = new String("a") + new String("b"); // s只存在堆中,不存在StringTable
// 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
String s2 = s.intern();
System.out.println(s == x); // F
System.out.println(s2 == x); // T
}
结论:
String s1 = "ab"; // ab仅放入串池 StringTable: ["ab"]
String s2 = new String("a") + new String("b"); // ab仅放入堆 StringTable: ["a","b"]
String s = new String("ab"); // ab串池和堆都存在 StringTable: ["ab"]
问题一:
public static void main(String[] args) {
String s = new String("a") + new String("b");//new String("ab")
//在上一行代码执行完以后,字符串常量池中并没有"ab"
String s2 = s.intern();
//jdk6:串池中创建一个字符串"ab",把此对象复制一份
//jdk8:串池中没有创建字符串"ab",而是创建一个引用指向 new String("ab"),将此引用返回
System.out.println(s2 == "ab");//jdk6:true jdk8:true
System.out.println(s == "ab");//jdk6:false jdk8:true
}
问题二:
public static void main(String[] args) {
String str1 = new StringBuilder("58").append("tongcheng").toString();
System.out.println(str1 == str1.intern());//true,字符串池中不存在,把堆中的引用复制一份放入串池
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2 == str2.intern());//false,字符串池中存在,直接返回已经存在的引用
}
原因:
System 类当调用 Version 的静态方法,导致 Version 初始化:
private static void initializeSystemClass() {
sun.misc.Version.init();
}
Version 类初始化时需要对静态常量字段初始化,被 launcher_name 静态常量字段所引用的 "java"
字符串字面量就被放入的字符串常量池:
package sun.misc;
public class Version {
private static final String launcher_name = "java";
private static final String java_version = "1.8.0_221";
private static final String java_runtime_name = "Java(TM) SE Runtime Environment";
private static final String java_profile_name = "";
private static final String java_runtime_version = "1.8.0_221-b11";
//...
}
Java 7 之前,String Pool 被放在运行时常量池中,属于永久代;Java 7 以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误
演示 StringTable 位置:
-Xmx10m
设置堆内存 10m
在 JDK8 下设置: -Xmx10m -XX:-UseGCOverheadLimit
在 JDK6 下设置: -XX:MaxPermSize=10m
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
int i = 0;
try {
for (int j = 0; j < 260000; j++) {
list.add(String.valueOf(j).intern());
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
两种方式:
-XX:StringTableSize=桶个数
,数量越少,性能越差
/**
* 演示 intern 减少内存占用
* -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
* -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
*/
public class Demo1_25 {
public static void main(String[] args) throws IOException {
List<String> address = new ArrayList<>();
System.in.read();
for (int i = 0; i < 10; i++) {
//很多数据
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if(line == null) {
break;
}
address.add(line.intern());
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}
}
System.in.read();
}
}
方法区 Method Area:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) Java 1.8 以前:方法区由永久代实现 Java 1.8 之后:方法区由元空间实现 异常:java.lang.OutOfMemoryError:Metaspace
方法区构成:
类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表
常量池表(Constant Pool Table)是 Class 文件的一部分,存储了类在编译期间生成的字面量、符号引用,JVM 为每个已加载的类维护一个常量池
字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等
java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示
final int a = 1; //这个1便是字面量
String b = "jwt"; //jwt便是字面量
符号引用:类、字段、方法、接口等的符号引用
特点:
JVM内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM
本地内存:又叫做堆外内存,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 Java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM
Java8 开始 PermGen 被元空间代替,永久代的类信息、方法、常量池等都移动到元空间区
元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制
方法区内存溢出:
JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space
-XX:MaxPermSize=8m #参数设置
JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=8m #参数设置
元空间内存溢出演示:
/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
*/
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 10000; i++, j++) {
// ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
// 版本号, public, 类名, 包名, 父类, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[]
byte[] code = cw.toByteArray();
// 执行了类的加载
test.defineClass("Class" + i, code, 0, code.length); // Class 对象
}
} finally {
System.out.println(j);
}
}
}
直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
Direct Memory 优点:
直接内存缺点:
应用场景:
数据流的角度:
直接内存 | |
---|---|
非直 接内存 |
ByteBuffer 有两种类型:
描述 | 优点 | |
---|---|---|
HeapByteBuffer | 在jvm堆上面的一个buffer,底层的本质是一个数组 | 由于内容维护在jvm里,所以把内容写进buffer里速度会快些;并且,可以更容易回收 |
DirectByteBuffer | 底层的数据其实是维护在操作系统的内存中,而不是jvm里,DirectByteBuffer里维护了一个引用address指向了数据,从而操作数据 | 跟外设(IO设备)打交道时会快很多,因为外设读取jvm堆里的数据时,不是直接读取的,而是把jvm里的数据读到一个内存块里,再在这个块里读取的,如果使用DirectByteBuffer,则可以省去这一步,实现zero copy |
直接内存 DirectByteBuffer 源码分析:
DirectByteBuffer(int cap) {
//....
long base = 0;
try {
// 分配直接内存
base = unsafe.allocateMemory(size);
}
// 内存赋值
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 创建回收函数
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
}
private static class Deallocator implements Runnable {
public void run() {
unsafe.freeMemory(address);// 释放内存
//...
}
}
分配和回收原理:
Unsafe
对象的 allocateMemory
方法完成直接内存的分配,setMemory 方法完成赋值freeMemory
来释放直接内存接下来,我们通过一个案例来了解下代码和对象是如何分配存储的,Java 代码又是如何在 JVM 中运行的。
public class JVMCase {
// 常量
public final static String MAN_SEX_TYPE = "man";
// 静态变量
public static String WOMAN_SEX_TYPE = "woman";
// 静态方法
public static void print(Student stu) {
System.out.println("name: " + stu.getName() + "; sex:" + stu.getSexType() + "; age:" + stu.getAge());
}
// 非静态方法
public void sayHello(Student stu) {
System.out.println(stu.getName() + "say: hello");
}
public static void main(String[] args) {
Student stu = new Student();
stu.setName("nick");
stu.setSexType(MAN_SEX_TYPE);
stu.setAge(20);
JVMCase jvmcase = new JVMCase();
// 调用静态方法
print(stu);
// 调用非静态方法
jvmcase.sayHello(stu);
}
}
@Data
class Student{
String name;
String sexType;
int age;
}
运行上面代码时,JVM的整个处理过程如下:
<clinit>()
方法,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为<clinit>()
方法。
常见 Out Of Memory(OOM) 错误:
-Xss size
-Xmx Size
-XX:MaxPermSize=8m
-XX:MaxMetaspaceSize=8m
Java 编译指令:javac -g 文件名.java
-g 可以生成所有相关信息
Java 反编译指令:javap -v 文件名.class
-v 输出附加信息
后台运行:nohup java 全路径名
常量池:主要存放编译期生成的各种**字面量(Literal)和符号引用(Symbolic References)**。
运行时常量池:运行时常量池里面存储的主要是编译期间生成的字面量、符号引用等等。
字符串常量池:可以理解成运行时常量池分出来的一部分。类加载到内存的时候,字符串会存到字符串常量池里面。
3者区别?
在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符;在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池;对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池
变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的声明位置
静态内部类和其他内部类:方法区/堆
类变量:堆
实例变量:堆
局部变量:虚拟机栈
一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding)
对象头:
普通对象:分为两部分
Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等
hash(25) + age(4) + lock(3) = 32bit #32位系统
unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统
Klass Word:类型指针,指向该对象的 Class 类对象的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
-XX:+UseCompressedOops
),使用32bits指针
|-----------------------------------------------------|
| Object Header (64 bits) |
|---------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|---------------------------|-------------------------|
数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节)
|-------------------------------------------------------------------------------|
| Object Header (96 bits) |
|-----------------------|-----------------------------|-------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|-----------------------|-----------------------------|-------------------------|
实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来
对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全
32 位系统:
一个 int 在 java 中占据 4byte,所以 Integer 的大小为:
# 需要补位4byte
4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte
int[] arr = new int[10]
# 由于需要8位对齐,所以最终大小为56byte
4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte
JVM 是通过栈帧中的对象引用(reference)访问到堆中的对象实例:
创建对象的方式
创建对象的过程
当虚拟机遇到一条 new
指令时,首先检查是否能在运行时常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那先执行类加载。
首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。
选择哪种分配方式由堆是否规整所决定,而堆是否规整又由所采用的GC收集器是否带有压缩整理功能决定。
内存分配并发问题
在创建对象的时候有一个很重要的问题,就是线程安全,虚拟机采用两种方式来保证线程安全:
-XX:+UseTLAB
参数来设定分配到的内存空间都初始化为零值,通过这个操作保证了对象的字段可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
将对象的所属类(即类的元数据信息)、对象的哈希码、对象的GC分代年龄、锁信息等数据存储在对象的对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,执行 new 指令之后会接着执行 <init>
方法(初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量),把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象
Thread Local Allocation Buffer,TLAB 是虚拟机在堆内存的 Eden 划分出来的一块专用空间,是线程专属的。
在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做快速分配策略
我们说TLAB是线程独享的,但是只是在“分配”这个动作上是线程独享的,至于在读取、垃圾回收等动作上都是线程共享的。
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占
JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过使用加锁机制确保数据操作的原子性,从而直接在堆中分配内存
栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存
参数设置:
-XX:UseTLAB
:设置是否开启 TLAB 空间
-XX:TLABWasteTargetPercent
:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1%
-XX:TLABRefillWasteFraction
:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配
Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代
Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。 Eden 和 Survivor 大小比例默认为 8:1:1
Old 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Old 区
分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能
GC:
工作机制:
晋升到老年代:
-XX:PretenureSizeThreshold
:大于此值的对象直接在老年代分配
-XX:MaxTenuringThreshold
:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15
MaxTenuringThreshold
中要求的年龄。
-XX:TargetSurvivorRatio=percent
:设定survivor区的目标使用率,默认值是 50%
取这个年龄和 MaxTenuringThreshold
中更小的一个值,作为新的晋升年龄的阈值
空间分配担保:
内存垃圾回收机制主要集中的区域就是线程共享区域:堆和方法区
Minor GC 触发条件:当 Eden 空间满时,就将触发一次 Minor GC
Full GC 同时回收新生代、老年代和方法区,有以下触发条件:
-XX:MaxTenuringThreshold
调大对象进入老年代的年龄,让对象在新生代多存活一段时间手动 GC 测试,VM参数:-XX:+PrintGcDetails
public void localvarGC1() {
byte[] buffer = new byte[10 * 1024 * 1024];//10MB
System.gc(); //输出: 不会被回收, FullGC时被放入老年代
}
public void localvarGC2() {
byte[] buffer = new byte[10 * 1024 * 1024];
buffer = null;
System.gc(); //输出: 正常被回收
}
public void localvarGC3() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
System.gc(); //输出: 不会被回收, FullGC时被放入老年代
}
public void localvarGC4() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
int value = 10;
System.gc(); //输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空
}
安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下
在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法:
问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决
安全区域 (Safe Region):指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始 GC 都是安全的
运行流程:
垃圾:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾
作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象
区域:垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收
在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:引用计数算法和可达性分析算法
引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用)
优点:
缺点:
可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集
GC Roots :GC Roots 是一组活跃的引用,不是对象,放在 GC Roots Set 集合
工作原理
可达性分析算法以 GC Roots 为起始点,从上至下的方式搜索被 GC Roots 所连接的目标对象
分析工作必须在一个保障一致性的快照中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World
的一个原因
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型
1.强引用:被强引用关联的对象不会被回收,只有当所有 GC Roots 都不通过【强引用】引用该对象,才能被垃圾回收
Object obj = new Object();//使用 new 一个新对象的方式来创建强引用
2.软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
3.弱引用(WeakReference):被弱引用关联的对象一定会被回收,只能存活到下一次垃圾回收发生之前
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
4.虚引用(PhantomReference):也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
5.终结器引用(finalization)
三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色:
当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为:
并发标记时,对象间的引用可能发生变化,多标和漏标的情况就有可能发生
多标情况:当 E 变为灰色时,断开 D 对 E 的引用,导致对象 E/F/G 仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为浮动垃圾
漏标情况:当 E 变为灰色时,断开 E 对 G 的引用,再让 D 引用 G。此时切回 GC 线程继续跑,因为 E 已经没有对 G 的引用了,所以不会将 G 放到灰色集合;尽管 D 重新引用了G,但 D 已经是黑色了,不会再重新做遍历处理。 最终导致的结果是:G 会一直停留在白色集合中,最后被当作垃圾进行清除。这直接影响到了应用程序的正确性,是不可接受的。
即漏标只有同时满足以下两个条件时才会发生:
代码角度解释漏标:
var G = objE.fieldG; // 1.读
objE.fieldG = null; // 2.写
objD.fieldG = G; // 3.写
为了解决问题,我们只要在上面这三步中的任意一步中做一些“手脚”,将对象G记录起来,然后作为灰色对象再进行遍历即可。比如放到一个特定的集合,等初始的GC Roots遍历完(并发标记),再遍历该集合(重新标记)。
重新标记通常是需要STW的,因为应用程序一直在跑的话,该集合可能会一直增加新的对象,导致永远都跑不完。当然,并发标记期间也可以将该集合中的大部分先跑了,从而缩短重新标记STW的时间,这个是优化问题了。
解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理:
以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下:
方法区主要回收的是无用的类
判定一个类是否是无用的类,需要同时满足下面 3 个条件:
ClassLoader
已经被回收java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是可以,而并不是和对象一样不使用了就会必然被回收
在常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该常量,说明常量 “abc” 是废弃常量,如果这时发生内存回收的话而且有必要的话(内存不够用),”abc” 就会被系统清理出常量池
类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收
如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收
算法优点:
算法缺点:
应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合
现在的商业虚拟机都采用这种收集算法回收新生代,因为新生代 GC 频繁并且对象的存活率不高,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间
标记清除算法,是将垃圾回收分为两个阶段,分别是标记和清除
算法缺点:
算法优点:
标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法
优点:不会产生内存碎片
缺点:需要移动大量对象,处理效率比较低
算法 | 速度 | 空间开销 | 移动对象 |
---|---|---|---|
复制算法 | 最快 | 通常需要活对象的 2 倍大小(不堆积碎片) | 是 |
标记清除 | 中等 | 少(但会堆积碎片) | 否 |
标记整理 | 最慢 | 少(不堆积碎片) | 是 |
红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器
Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同:
查看默认的垃圾收回收器:
-XX:+PrintcommandLineFlags
:查看命令行相关参数(包含使用的垃圾收集器)
Serial:串行垃圾收集器,作用于新生代,使用单线程进行垃圾回收,采用复制算法,新生代基本都是复制算法
STW(Stop-The-World):垃圾回收时,只有一个线程在工作,并且 Java 应用中的所有线程都要暂停,等待垃圾回收的完成
Serial old:执行老年代垃圾回收的串行收集器,内存回收算法使用的是标记-整理算法,同样也采用了串行回收和 STW 机制
开启参数:-XX:+UseSerialGC
等价于新生代用 Serial GC 且老年代用 Serial old GC
优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率
缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如 JavaWeb 应用
Par 是 Parallel 并行的缩写,New 是只能处理的是新生代
并行垃圾收集器在串行垃圾收集器的基础之上做了改进,采用复制算法,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间
对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有ParNew GC 能与 CMS 收集器配合工作
相关参数:
-XX:+UseParNewGC
:表示新生代使用并行收集器,不影响老年代
-XX:ParallelGCThreads
:默认开启和 CPU 数量相同的线程数
ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器
Parallel Scavenge 收集器:是应用于新生代的并行垃圾回收器,采用复制算法、并行回收和 Stop the World 机制
Parallel Old :是应用于老年代的并行垃圾回收器,采用标记-整理算法
对比其他回收器:
应用场景:
停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降
在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,Java8 默认是此垃圾收集器组合
参数配置:
-XX:+UseParallelGC
:手动指定年轻代使用 Paralle 并行收集器执行内存回收任务-XX:+UseParalleloldcc
:手动指定老年代使用并行回收收集器执行内存回收任务-XX:+UseAdaptivesizepplicy
:设置 Parallel Scavenge 收集器具有自适应调节策略,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量-XX:ParallelGcThreads
:设置年轻代并行收集器的线程数,一般与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能-XX:MaxGCPauseMillis
:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒-XX:GCTimeRatio
:垃圾收集时间占总时间的比例 =1/(N+1),用于衡量吞吐量的大小-xx:MaxGCPauseMillis
参数有一定矛盾性,暂停时间越长,Radio 参数就容易超过设定的比例Concurrent Mark Sweep(CMS),是一款并发的、使用标记-清除算法、响应时间优先、针对老年代的垃圾回收器,其最大特点是让垃圾收集线程与用户线程同时工作
CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(低延迟)越适合与用户交互的程序,良好的响应速度能提升用户体验
分为以下四个流程:
Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因:Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿
优点:并发收集、低延迟
缺点:
参数设置:
-XX:+UseConcMarkSweepGC
:手动指定使用 CMS 收集器执行内存回收任务
开启该参数后会自动将 -XX:+UseParNewGC
打开,即:ParNew + CMS + Serial old的组合
-XX:CMSInitiatingoccupanyFraction
:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收
-XX:+UseCMSCompactAtFullCollection
:用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长
-XX:CMSFullGCsBeforecompaction
:设置在执行多少次 Full GC 后对内存空间进行压缩整理
-XX:ParallelCMSThreads
:设置 CMS 的线程数量
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,应用于新生代和老年代、采用标记-整理算法、响应时间优先、软实时、低延迟、可设定目标(最大 STW 停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1 JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器 应用场景:
-XX:G1HeapRegionSize
设定,取值范围为1MB~32MB之间且为 2 的 N 次幂
-XX:MaxGCPauseMillis
参数指定的停顿时间只意味着垃圾收集发生之前的期望值为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进 GC Roots 扫描范围。
记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来记录被哪些其他 Region 里的对象引用(谁引用了我就记录谁)
通过写屏障来更新记忆集:
程序对 Reference 类型数据写操作时,产生一个写屏障 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中,进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏
垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式:
卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等
卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,在垃圾收集发生时,只要筛选出卡表中 dirty 的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。JVM 对于卡页的维护也是通过写屏障的方式
我们已经解决了如何使用记忆集来缩减GC Roots扫描范围的问题,但还没有解决卡表元素如何维护的问题,例如它们何时变脏、谁来把它们变脏等。
卡表元素何时变脏
有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻。
如何变脏,即如何在对象赋值的那一刻去更新维护卡表呢?
在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。
写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)。HotSpot虚拟机的许多收集器中都有使用到写屏障,但直至G1收集器出现之前,其他收集器都只用到了写后屏障。
应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外的开销,不过这个开销与Minor GC时扫描整个老年代的代价相比还是低得多的。
Collection Set 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中
CSet根据两种不同的回收类型分为两种不同CSet。
-XX:G1MixedGCLiveThresholdPercent
(默认85%),只有活跃度高于这个阈值的才会准入CSet
-XX:G1OldCSetRegionThresholdPercent
(默认10%)设置,CSet跟整个堆的比例的数量上限。
G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 Full GC,在不同的条件下被触发
顺时针:Minor GC → Minor GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收
-XX:InitiatingHeapOccupancyPercent
设置阈值(默认45%)-XX:MaxGCPauseMillis
:设置期望达到的最大 GC 停顿时间指标,JVM会尽力实现,但不保证达到,默认值是 200ms
-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
:设置并发标记线程数,设置为并行垃圾回收线程数 ParallelGcThreads 的1/4左右-XX:InitiatingHeapOccupancyPercent
:设置并发标记阈值,默认值是 45-XX:+ClassUnloadingWithConcurrentMark
:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类-XX:G1NewSizePercent
:新生代占用整个堆内存的最小百分比(默认5%) -XX:G1MaxNewSizePercent
:新生代占用整个堆内存的最大百分比(默认60%) -XX:G1ReservePercent=10
:保留内存区域,防止Survivor中的 to 区溢出G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优:
不断调优暂停时间指标:
-XX:MaxGCPauseMillis=x
可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置不要设置新生代和老年代的大小:
-Xmn
或 -XX:NewRatio
等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标ZGC 收集器是JDK 11中推出的一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法
ZGC 目标:
ZGC 的工作过程可以分为 4 个阶段:
ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟
优点:高吞吐量、低延迟
缺点:浮动垃圾,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾
对比:
G1 需要通过写屏障来维护记忆集,才能处理跨代指针,得以实现Region的增量回收。记忆集要占用大量的内存空间,写屏障也对正常程序运行造成额外负担,这些都是权衡选择的代价。
ZGC就完全没有使用记忆集,它甚至连分代都没有,连像CMS中那样只记录新生代和老年代间引用的卡表也不需要,因而完全没有用到写屏障,所以给用户线程带来的运行负担也要小得多。可是,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾
新一代垃圾回收器ZGC的探索与实践 - 美团技术团队 (meituan.com)
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类可以有多个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
类型 | 名称 | 说明 | 长度 | 数量 |
---|---|---|---|---|
u4 | magic | 魔数,识别类文件格式 | 4个字节 | 1 |
u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 |
u2 | major_version | 主版本号(大版本) | 2个字节 | 1 |
u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 |
cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 |
u2 | access_flags | 访问标识 | 2个字节 | 1 |
u2 | this_class | 类索引 | 2个字节 | 1 |
u2 | super_class | 父类索引 | 2个字节 | 1 |
u2 | interfaces_count | 接口计数 | 2个字节 | 1 |
u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count |
u2 | fields_count | 字段计数器 | 2个字节 | 1 |
field_info | fields | 字段表 | n个字节 | fields_count |
u2 | methods_count | 方法计数器 | 2个字节 | 1 |
method_info | methods | 方法表 | n个字节 | methods_count |
u2 | attributes_count | 属性计数器 | 2个字节 | 1 |
attribute_info | attributes | 属性表 | n个字节 | attributes_count |
Class 文件格式只有两种数据类型:无符号数和表
_info
结尾,用于描述有层次关系的数据,整个 Class 文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面加上个数说明Class 文件获取方式:
javac -g xxx.java
指令javap -v xxx.class >xxx.txt
接下来以下面代码进行讲解类文件结构
package JJTest;
public class Demo {
private int num = 1;
public int add(){
num = num + 2;
return num;
}
}
上面代码的字节码对应的16进制
每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number),是 Class 文件的标识符,代表这是一个能被虚拟机接受的有效合法的 Class 文件,
0xCAFEBABE
,不符合则会抛出错误
4 个 字节,5 6两个字节代表的是编译的副版本号 minor_version,7 8 两个字节是编译的主版本号 major_version
java.lang.UnsupportedClassVersionError
00 34
: 转换成 10 进制 16*3+4 = 52,代表 JDK1.8主版本(十进制) | 副版本(十进制) | 编译器版本 |
---|---|---|
45 | 3 | 1.1 |
46 | 0 | 1.2 |
47 | 0 | 1.3 |
48 | 0 | 1.4 |
49 | 0 | 1.5 |
50 | 0 | 1.6 |
51 | 0 | 1.7 |
52 | 0 | 1.8 |
53 | 0 | 1.9 |
54 | 0 | 1.10 |
55 | 0 | 1.11 |
00 16
:转换成10进制 22,因此有 21 个常量池
constant_pool 是一种表结构,以 1 ~ constant_pool_count - 1 为索引,表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池
.
替换成 /
,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 ;
表示全限定名结束
Ljava/lang/Object;
,不同方法间用;
隔开 [ 数组类型,代表一维数组。比如:double[][][] is [[[D
常量池中常量类型
常量池的每一项常量都是一个表,表开始的第一位是一个 u1 类型的标志位(tag),代表当前这个常量属于哪种常量类型。
18 种常量没有出现 byte、short、char,boolean 的原因:编译之后都可以理解为 Integer
0a 00 04 00 12
: 访问标识(access_flag),又叫访问标志、访问标记,该标识用两个字节表示,用于识别一些类或者接口层次的访问信息,包括这个 Class 是类还是接口,是否定义为 public类型,是否定义为 abstract类型等
ACC_PUBLIC | ACC_FINAL
ACC_SUPER
可以让类更准确地定位到父类的方法,确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义,现代编译器都会设置并且使用这个标记00 21
: 0x0001 + 0x0020 代表 ACC_PUBLIC 和 ACC_SUPER标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 标志为 public 类型 |
ACC_FINAL | 0x0010 | 标志被声明为 final,只有类可以设置 |
ACC_SUPER | 0x0020 | 标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真,使用增强的方法调用父类方法 |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(由编译器产生的类,没有源码对应) |
ACC_ANNOTATION | 0x2000 | 标志这是一个注解 |
ACC_ENUM | 0x4000 | 标志这是一个枚举 |
本类索引、父类索引、接口索引集合
00 03
: cp_info #3 【即3号常量池】–> cp_info #20 –> JJTest/Demo00 04
:cp_info #4 –> cp_info #21 –> java/lang/Object00 00
:表示当前类没有实现接口长度 | 含义 |
---|---|
u2 | this_class |
u2 | super_class |
u2 | interfaces_count |
u2 | interfaces[interfaces_count] |
字段 fields 用于描述接口或类中声明的变量,包括类变量以及实例变量,但不包括方法内部、代码块内部声明的局部变量(local variables)以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述
fields_count(字段计数器),表示当前 class 文件 fields 表的成员个数,用两个字节来表示
00 01
:表示当前 class 文件只有一个字段,即numfields[](字段表):
标志名称 | 标志值 | 含义 | 数量 |
---|---|---|---|
u2 | access_flags | 访问标志 | 1 |
u2 | name_index | 字段名索引 | 1 |
u2 | descriptor_index | 描述符索引 | 1 |
u2 | attributes_count | 属性计数器 | 1 |
attribute_info | attributes | 属性集合 | attributes_count |
字段访问标志:
00 02
:ACC_PRIVATE,字段为private标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否为public |
ACC_PRIVATE | 0x0002 | 字段是否为private |
ACC_PROTECTED | 0x0004 | 字段是否为protected |
ACC_STATIC | 0x0008 | 字段是否为static |
ACC_FINAL | 0x0010 | 字段是否为final |
ACC_VOLATILE | 0x0040 | 字段是否为volatile |
ACC_TRANSTENT | 0x0080 | 字段是否为transient |
ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 |
ACC_ENUM | 0x4000 | 字段是否为enum |
字段名索引:根据该值查询常量池中的指定索引项即可
00 05
:cp_info #5 –>num描述符索引:用来描述字段的数据类型、方法的参数列表和返回值
00 06
:cp_info #6 –> I –> 整型字符 | 类型 | 含义 |
---|---|---|
B | byte | 有符号字节型树 |
C | char | Unicode字符,UTF-16编码 |
D | double | 双精度浮点数 |
F | float | 单精度浮点数 |
I | int | 整型数 |
J | long | 长整数 |
S | short | 有符号短整数 |
Z | boolean | 布尔值true/false |
V | void | 代表void类型 |
L Classname | reference | 一个名为Classname的实例 |
[ | reference | 一个一维数组 |
属性表集合:属性个数存放在 attribute_count 中,属性具体内容存放在 attribute 数组中,一个字段还可能拥有一些属性,用于存储更多的额外信息,比如常量的初始化值、一些注释信息等,对于常量属性而言,attribute_length 值恒为2
00 00
:当前字段没有属性ConstantValue_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}
方法表是 methods 指向常量池索引集合,其中每一个 method_info 项都对应着一个类或者接口中的方法信息,完整描述了每个方法的签名
要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存
methods_count(方法计数器):表示 class 文件 methods 表的成员个数,使用两个字节来表示
00 02
:表示两个方法,<init> 和 addmethods[](方法表):每个表项都是一个 method_info 结构,表示当前类或接口中某个方法的完整描述
方法表结构如下:
类型 | 名称 | 含义 | 数量 |
---|---|---|---|
u2 | access_flags | 访问标志 | 1 |
u2 | name_index | 字段名索引 | 1 |
u2 | descriptor_index | 描述符索引 | 1 |
u2 | attrubutes_count | 属性计数器 | 1 |
attribute_info | attributes | 属性集合 | attributes_count |
方法表访问标志:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | public,方法可以从包外访问 |
ACC_PRIVATE | 0x0002 | private,方法只能本类访问 |
ACC_PROTECTED | 0x0004 | protected,方法在自身和子类可以访问 |
ACC_STATIC | 0x0008 | static,静态方法 |
00 01
:ACC_PUBLIC,public,方法可以从包外访问
00 07
:<init> ,实例初始化方法
00 08
:()V,无参,返回值类型void
00 01
:当前方法有一个属性
属性表集合,指的是 Class 文件所携带的辅助信息,比如该 Class 文件的源文件的名称,以及任何带有 RetentionPolicy.CLASS
或者 RetentionPolicy.RUNTIME
的注解,这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试。字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息
attributes_ count(属性计数器):表示当前文件属性表的成员个数
00 01
:表示有1个属性表attributes[](属性表):属性表的每个项的值必须是 attribute_info 结构
案例中00 10
:属性名索引 cp_info #16 –>SourceFile
案例中00 00 00 02
:属性长度为 2
案例中00 11
:源码文件素引cp_info #17 –>Demo.java
属性的通用格式:
ConstantValue_attribute{
u2 attribute_name_index; //属性名索引
u4 attribute_length; //属性长度
u2 attribute_info; //属性表
}
属性类型:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java 代码编译成的字节码指令 |
ConstantValue | 字段表 | final 关键字定义的常量池 |
Deprecated | 类、方法、字段表 | 被声明为 deprecated 的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code 属性 | 方法的局部变量描述 |
StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Syothetic | 类,方法表,字段表 | 标志方法或字段为编泽器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 |
RuntimelnvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法 |
RuntirmelnvisibleParameterAnniotation | 方法表 | 作用与 RuntimelnvisibleAnnotations 属性类似,作用对象哪个为方法参数 |
AnnotationDefauit | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存 invokeddynanic 指令引用的引导方式限定符 |
ConstantValue属性表示一个常量字段的值。位于field_info结构的属性表中。
Deprecated 属性是在JDK1.1为了支持注释中的关键词@deprecated而引入的。
Code属性就是存放方法体里面的代码。但是,并非所有方法表都有Code属性。像接口或者抽象方法,他们没有具体的方法体,因此也就不会有Code属性了。Code属性表的结构,如下图:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | attribute_name_index | 1 | 属性名索引 |
u4 | attribute_length | 1 | 属性长度 |
u2 | max_stack | 1 | 操作数栈深度的最大值 |
u2 | max_locals | 1 | 局部变量表所需的存续空间 |
u4 | code_length | 1 | 字节码指令的长度 |
u1 | code | code_lenth | 存储字节码指令 |
u2 | exception_table_length | 1 | 异常表长度 |
exception_info | exception_table | exception_length | 异常表 |
u2 | attributes_count | 1 | 属性集合计数器 |
attribute_info | attributes | attributes_count | 属性集合 |
可以看到:Code属性表的前两项跟属性表是一致的,即Code属性表遵循属性表的结构,后面那些则是他自定义的结构。
为了方便说明特别定义一个表示类或接口的Class格式为C。如果C的常量池中包含某个CONSTANT_Class_info成员,且这个成员所表示的类或接口不属于任何一个包,那么C的ClassFile结构的属性表中就必须含有对应的InnerClasses属性。InnerClasses属性是在JDK1.1中为了支持内部类和内部接口而引入的,位于ClassFile结构的属性表。
LineNumberTable属性是可选变长属性,位于Code结构的属性表。
LineNumberTable属性是用来描述Java源码行号与字节码行号之间的对应关系。这个属性可以用来在调试的时候定位代码执行的行数。
在Code属性的属性表中,LineNumberTable属性可以按照任意顺序出现,此外,多个LineNumberTable属性可以共同表示一个行号在源文件中表示的内容,即LineNumberTable属性不需要与源文件的行一一对应。
LocalVariableTable是可选变长属性,位于Code属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息。在Code属性的属性表中,LocalVariableTable属性可以按照任意顺序出现。Code属性中的每个局部变量最多只能有一个LocalVariableTable属性。
// LocalVariableTable属性表结构:
LocalVariableTable_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{
u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}
Signature属性是可选的定长属性,位于ClassFile,field_info或method_info结构的属性表中。在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。
⑧ SourceFile属性
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | attribute_name_index | 1 | 属性名索引 |
u4 | attribute_length | 1 | 属性长度 |
u2 | sourcefile index | 1 | 源码文件素引 |
可以看到,其长度总是固定的8个字节。
Java虚拟机中预定义的属性有20多个,这里就不一一介绍了,通过上面几个属性的介绍,只要领会其精髓,其他属性的解读也是易如反掌。
主动引用:对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化
new
、getstatic
、putstatic
、invokestatic
这 4 条直接码指令时new
指令时会初始化类。即当程序创建一个类的实例对象。getstatic
指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。putstatic
指令时会初始化类。即程序给类的静态变量赋值。invokestatic
指令时会初始化类。即程序调用类的静态方法。java.lang.reflect
包的方法对类进行反射调用时如 Class.forname("...")
, newInstance()
等等。如果类没初始化,需要触发其初始化。main
方法的那个类),虚拟机会先初始化这个类。MethodHandle
和 VarHandle
可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle
来初始化要调用的类。被动引用:所有引用类的方式都不会触发初始化,称为被动引用
在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。
按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:
加载过程完成以下三件事:
Class
对象,作为方法区这些数据的访问入口二进制字节流可以从以下方式中获取:
方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构:
_java_mirror
即 Java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 Java 使用_super
即父类、_fields
即成员变量、_methods
即方法、_constants
即常量池、_class_loader
即类加载器、_vtable
虚方法表、_itable
接口方法表加载过程:
Class 对象
和 _java_mirror
相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互类实例&类模型位置 | 加载过程 |
---|---|
数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程:
确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全
主要包括四种验证:
为类变量/静态变量分配内存并设置默认初始值的阶段,使用的是方法区的内存。
说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次
类变量初始化:
实例:
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123:
public static int value = 123;
常量 value 被初始化为 123 而不是 0:
public static final int value = 123;
Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是 0,故 boolean 的默认值就是 false
类型 | 默认初始值 |
---|---|
byte | (byte)0 |
short | (short)0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0 |
char | \u0000 |
boolean | false |
reference | null |
将常量池中类、接口、字段、方法的符号引用替换为直接引用(内存地址)的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等
初始化阶段是执行初始化方法
<clinit>()
方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 <clinit>()
,另一个是实例的初始化方法 <init>()
类构造器 <clinit>()
与实例构造器 <init>()
不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机调用一次,后续实例化不再加载,引用第一次加载的类,而实例构造器则会被虚拟机调用多次,只要程序员创建对象
<clinit>()
:类构造器,由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的
作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块
<clinit>()
方法将不会被生成<clinit>()
方法只执行一次,在执行 <clinit>()
方法时,必须先执行父类的<clinit>()
方法线程安全问题:
<clinit>()
方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>()
方法,其它线程都阻塞等待,直到活动线程执行 <clinit>()
方法完毕<clinit>()
方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问
public class Test {
static {
//i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
接口中不可以使用静态语句块,但有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>()
方法
<clinit>()
方法不需要先执行父接口的 <clinit>()
方法<clinit>()
方法<init>()
指实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行
实例化即调用 <init>()V
,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行
new 关键字会创建对象并复制 dup 一个对象引用,一个调用 <init>
方法,另一个用来赋值给接收者
卸载类即该类的 Class 对象被 GC。
卸载类需要满足3个要求:
父类的类构造器<clinit>
➔ 子类的类构造器<clinit>
➔ 父类的实例构造器<init>
➔ 父类的构造函数 ➔ 子类的的实例构造器<init>
➔ 子类的构造函数
//父类
public class Father {
static {
System.out.println("父静态代码块");
}
{
System.out.println("父非静态代码块");
}
public Father(){
System.out.println("父构造器");
}
}
//子类
public class Son extends Father {
static {
System.out.println("子静态代码块");
}
{
System.out.println("子非静态代码块");
}
public Son(){
System.out.println("子构造器");
}
//创建对象
public static void main(String[] args) {
new Son();
}
}
/**
运行结果:
父静态代码块
子静态代码块
父非静态代码块
父构造器
子非静态代码块
子构造器
**/
//隐式加载
User user = new User();
//显式加载,并初始化
Class clazz = Class.forName("com.test.java.User");
//显式加载,但不初始化
ClassLoader.getSystemClassLoader().loadClass("com.test.java.Parent");
类加载器基本特征:
ClassLoader 类,是一个抽象类,除启动类加载器外其它类加载器都继承自 ClassLoader
获取 ClassLoader 的途径:
clazz.getClassLoader()
Thread.currentThread.getContextClassLoader()
ClassLoader.getSystemClassLoader()
DriverManager.getCallerClassLoader()
ClassLoader 类常用方法:
getParent()
:返回该类加载器的超类加载器 loadclass(String name)
:加载名为name的类,返回结果为Class类的实例,该方法就是双亲委派模式findclass(String name)
:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用findLoadedClass(String name)
:查找名称为 name 的已经被加载过的类,final 修饰无法重写defineClass(String name, byte[] b, int off, int len)
:将字节流解析成 JVM 能够识别的类对象resolveclass(Class<?> c)
:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析InputStream getResourceAsStream(String name)
:指定资源名称获取输入流在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制
类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象
从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器:
从 Java 开发人员的角度看:
JAVA_HOME/jre/lib
或 sun.boot.class.path
目录中的,或者被 -Xbootclasspath
参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中ExtClassLoader (sun.misc.Launcher$ExtClassLoader)
实现,上级为 Bootstrap,显示为 nullJAVA_HOME/jre/lib/ext
或者被 java.ext.dir
系统变量所指定路径中的所有类库加载到内存中AppClassLoader(sun.misc.Launcher$AppClassLoader)
实现,上级为 Extensionjava.class.path
指定路径下的类库getSystemClassLoader()
方法的返回值,因此称为系统类加载器public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层 扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6
//获取其上层 获取不到启动类加载器
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
System.out.println(bootStrapClassLoader);//null
//对于用户自定义类来说:使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//String 类使用引导类加载器进行加载的 --> java核心类库都是使用启动类加载器加载的
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
}
双亲委派机制的优点:
可以避免类被重复加载,当父类已经加载后则无需重复加载,保证类的全局唯一性
保护程序安全,防止类库的核心 API 被随意篡改
例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数
public class String {
public static void main(String[] args) {
System.out.println("demo info");
}
}
此时执行 main 函数会出现异常,在类 java.lang.String 中找不到 main 方法。因为双亲委派的机制,java.lang.String 在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法
双亲委派机制的缺点:
检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类(可见性)
通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。
参考:https://m.imooc.com/wiki/jvm-loadparent
双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean)
方法中体现。逻辑如下:
findLoadedClass(name)
,如果有,直接返回。
parent.loadClass(name,false)
接口进行加载。
findBootstrapClassorNull(name)
接口,让启动类加载器进行加载。
findClass(name)
接口进行加载。该接口最终会调用java.lang.ClassLoader
接口的defineClass
系列的native
接口加载目标Java类。
双亲委派的模型就隐藏在这第2和第3步中。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1.调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类
Class c = findLoadedClass(name);
// 当前类加载器如果没有加载过
if (c == null) {
long t0 = System.nanoTime();
try {
// 2.判断当前类加载器是否有父类加载器
if (parent != null) {
// 如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false)
// 父类加载器的 loadClass 方法,又会检查自己是否已经加载过
c = parent.loadClass(name, false);
} else {
// 3.当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader
// 则调用 BootStrap ClassLoader 的方法加载类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) { }
if (c == null) {
long t1 = System.nanoTime();
// 4.如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载
// 可以自定义 findClass() 方法
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
// 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析
resolveClass(c);
}
return c;
}
}
双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式
破坏双亲委派模型的方式:
JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型
热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,热替换的关键需求在于服务不能中断,修改必须立即表现正在运行的系统之中
对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可
作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏
//自定义类加载器,读取指定的类路径classPath下的class文件
public class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
public MyClassLoader(ClassLoader parent, String classPath) {
super(parent);
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
BufferedInputStream bis = null;
ByteArrayOutputStream baos = null;
try {
// 获取字节码文件的完整路径
String fileName = classPath + className + ".class";
// 获取一个输入流
bis = new BufferedInputStream(new FileInputStream(fileName));
// 获取一个输出流
baos = new ByteArrayOutputStream();
// 具体读入数据并写出的过程
int len;
byte[] data = new byte[1024];
while ((len = bis.read(data)) != -1) {
baos.write(data, 0, len);
}
// 获取内存中的完整的字节数组的数据
byte[] byteCodes = baos.toByteArray();
// 调用 defineClass(),将字节数组的数据转换为 Class 的实例。
Class clazz = defineClass(null, byteCodes, 0, byteCodes.length);
return clazz;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (baos != null)
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bis != null)
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
public static void main(String[] args) {
MyClassLoader loader = new MyClassLoader("/Workspace/Project/JVM_study/src/java1/");
try {
Class clazz = loader.loadClass("Demo1");
System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());//MyClassLoader
System.out.println("加载当前类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动:
<JAVA_HOME>\lib\ext
目录,此前使用这个目录或者 java.ext.dirs
系统变量来扩展 JDK 功能的机制就不需要再存在
jdk.internal.loader.BuiltinClassLoader
Java 代码执行流程:
Java 程序(.java) --(编译)--> 字节码文件(.class)--(解释执行/JIT)--> 操作系统(Win,Linux)
在 JVM 的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据
大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend)为相应的 int 类型数据
在做值相关操作时:
加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递
局部变量压栈指令:将给定的局部变量表中的数据压入操作数栈
xload
、xload_n
,x 表示数据类型,为 i、l、f、d、a; n 为 0 到 3xload_n
表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈xload n
通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const_<n>
、push
、ldc
指令
push
:包括 bipush 和 sipush,区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数ldc
:如果以上指令不能满足需求,可以使用 ldc 指令,接收一个 8 位的参数,该参数指向常量池中的 int、 float 或者 String 的索引,将指定的内容压入堆栈。ldc_w
接收两个 8 位参数,能支持的索引范围更大,如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w
指令aconst_null
将 null 对象引用压入栈,iconst_m1
将 int 类型常量 -1 压入栈,iconst_0
将 int 类型常量 0 压入栈出栈装入局部变量表指令:将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值
xstore
、xstore_n
,x 表示取值类型为 i、l、f、d、a, n 为 0 到 3xastore
表示存入数组,x 取值为 i、l、f、d、a、b、c、s扩充局部变量表的访问索引的指令:wide
算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈
没有直接支持 byte、 short、 char 和 boolean 类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组
运算模式:
NaN 值:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义,将使用 NaN 值来表示
double j = i / 0.0;
System.out.println(j);//无穷大,NaN: not a number
**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是先执行 iinc
4 iload_1 //存入操作数栈
5 iinc 1 by 1 //自增i++
8 istore_3 //把操作数栈没有自增的数据的存入局部变量表
9 iinc 2 by 1 //++i
12 iload_2 //加载到操作数栈
13 istore 4 //存入局部变量表,这个存入没有 _ 符号,_只能到3
public class Demo {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a); //11
System.out.println(b); //34
}
}
判断结果:
public class Demo {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
System.out.println(x); // 结果是 0
}
}
类型转换指令可以将两种不同的数值类型进行相互转换,除了 boolean 之外的七种类型
宽化类型转换:
窄化类型转换:
arraylength:取数组长度的指令。该指令弹出栈顶的数组元素,获取数组的长度,将长度压入栈。
普通调用指令:
动态调用指令:
指令对比:
指令说明:
方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的。
方法返回指令 | void | int | long | float | double | reference |
---|---|---|---|---|---|---|
xreturn | return | ireturn | lreturn | freutrn | dreturn | areturn |
ireturn(当返回值是boolean、byte、char、short和int 类型时使用)
通过ireturn指令,将当前函数操作数栈的顶层元素弹出,并将这个元素压入调用者函数的操作数栈中(因为调用者非常关心函数的返回值),所有在当前函数操作数栈中的其他元素都会被丢弃。
如果当前返回的是synchronized方法,那么还会执行一个隐含的monitorexit指令,退出临界区。
最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者。
JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的指令
比较指令:比较栈顶两个元素的大小,并将比较结果入栈
条件跳转指令:
指令 | 说明 |
---|---|
ifeq | equals,当栈顶int类型数值等于0时跳转 |
ifne | not equals,当栈顶in类型数值不等于0时跳转 |
iflt | lower than,当栈顶in类型数值小于0时跳转 |
ifle | lower or equals,当栈顶in类型数值小于等于0时跳转 |
ifgt | greater than,当栈顶int类型数组大于0时跳转 |
ifge | greater or equals,当栈顶in类型数值大于等于0时跳转 |
ifnull | 为 null 时跳转 |
ifnonnull | 不为 null 时跳转 |
比较条件跳转指令:
指令 | 说明 |
---|---|
if_icmpeq | 比较栈顶两 int 类型数值大小(下同),当前者等于后者时跳转 |
if_icmpne | 当前者不等于后者时跳转 |
if_icmplt | 当前者小于后者时跳转 |
if_icmple | 当前者小于等于后者时跳转 |
if_icmpgt | 当前者大于后者时跳转 |
if_icmpge | 当前者大于等于后者时跳转 |
if_acmpeq | 当结果相等时跳转 |
if_acmpne | 当结果不相等时跳转 |
多条件分支跳转指令:
无条件跳转指令:
抛出异常指令:athrow 指令,在Java程序中显示抛出异常的操作(throw语句)都是由athrow指令来实现。
处理异常:
JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是采用异常表来完成的
代码:
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
字节码:
0: iconst_0
1: istore_1 // 0 -> i ->赋值
2: bipush 10 // try 10 放入操作数栈顶
4: istore_1 // 10 -> i 将操作数栈顶数据弹出,存入局部变量表的 slot1
5: bipush 30 // 【finally】
7: istore_1 // 30 -> i
8: goto 27 // return
11: astore_2 // catch Exceptin -> e
12: bipush 20 //
14: istore_1 // 20 -> i
15: bipush 30 // 【finally】
17: istore_1 // 30 -> i
18: goto 27 // return
21: astore_3 // catch any -> slot 3
22: bipush 30 // 【finally】
24: istore_1 // 30 -> i
25: aload_3 // 将局部变量表的slot 3数据弹出,放入操作数栈栈顶
26: athrow // throw 抛出异常
27: return
Exception table: // 任何阶段出现任务异常都会执行 finally
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any // 剩余的异常类型,比如 Error
11 15 21 any // 剩余的异常类型,比如 Error
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I
如果一个方法定义了一个try-catch 或者try-finally的异常处理,就会创建一个 Exception table 的结构,异常表保存了每个异常处理信息
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any // 剩余的异常类型,比如 Error
11 15 21 any // 剩余的异常类型,比如 Error
Java虚拟机支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的
方法级的同步:是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法
方法内指定指令序列的同步:有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义
1)原始java 代码
/*
演示 字节码指令 和 操作数栈、常量池的关系
*/
public class Demo {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1; //32767 + 1
int c = a + b;
System.out.println(c);
}
}
2)编译后的核心字节码文件
MD5 checksum cc8fc12b6e178b8f28e787497e993363
Compiled from "Demo.java"
public class com.jvm.test.Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#25 // java/lang/Object."<init>":()V
#2 = Class #26 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
#6 = Class #31 // com/jvm/test/Demo
#7 = Class #32 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/jvm/test/Demo;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 Demo.java
#25 = NameAndType #8:#9 // "<init>":()V
#26 = Utf8 java/lang/Short
#27 = Class #33 // java/lang/System
#28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#29 = Class #36 // java/io/PrintStream
#30 = NameAndType #37:#38 // println:(I)V
#31 = Utf8 com/jvm/test/Demo
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (I)V
{
public com.jvm.test.Demo();
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 15: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/jvm/test/Demo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 17: 0
line 18: 3
line 19: 6
line 20: 10
line 21: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
}
SourceFile: "Demo.java"
3)常量池载入运行时常量池
4)方法字节码载入方法区
5)main线程开始运行,分配栈帧内存(stack=2, locals=4)
6)执行引擎开始执行字节码
bipush 10
istore_1
idc #3
istore_2
iload_1
再接下来,需要执行int c = a + b;执行引擎不能直接在局部变量表进行a+b操作,需要先将a、b进行读取,然后放入操作数栈中才能进行计算分析
iload 2
iadd
istore_3
getstatic #4
iload_3
invokevirtual #5
return
在日常的项目开发中,经常遇到a++、++a、a–之类,下面我们开始从字节码的视角来分析a++。
java代码如下:
/*
从字节码角度分析 a++ 相关题目
*/
public class Demo {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
}
使用javap -v xxx.class 来查看类文件全部指令信息,如下:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
21: iload_1
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 17: 0
line 18: 3
line 19: 18
line 20: 25
line 21: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 a I
18 15 2 b I
}
SourceFile: "Demo.java"
分析:
下面我们通过字节码来剖析如下两行代码在内存当中整个执行过程
int a = 10;
int b = a++ + ++a + a--;
下图是先将10通过bipush 放入操作数栈中
接着将10从操作数栈上弹出存入局部变量表1号槽位,相当于代码 int a = 10 执行完成
接着执行:int b = a++ + ++a + a–; 因为有从左往右的执行顺序,所以先执行a++,先将a的值加载到操作数栈中;通过iload_1加载1号槽位的数据到操作数栈中
接着执行a++自增1操作,这个操作是在局部变量表中完成的。相当于完成了a++执行
再接着执行++a自增1操作,这个操作也是在局部变量表中完成的
接着从局部变量表1号槽位加载数据到操作数栈中,即12入栈,完成发a++ 、++a 各自的执行了
然后,iadd是将操作数栈中弹出(出栈)两个数12、10进行求和操作,得到22,最后将累加的结果22存入栈中。即完成了a++ + ++a的执行
接着,需要执行a–,先将局部变量表槽位1的数据12加载到操作数栈中
然后,将局部变量表槽位1的数据自减1
接着,执行iadd操作,将操作数栈12、22弹出栈后,进行求和操作得到34,再将34结果压入栈
最后,执行istore_2操作,将操作数栈弹出数据34,并压入局部变量表2号槽位中
*.java
文件转变成 .class
文件的过程;如 JDK 的 Javac,Eclipse JDT 中的增量式编译器。C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度。
C1 编译器的优化方法:
C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是使用分层编译的原因
C2 的优化主要是在全局层面,逃逸分析是优化的基础,如果不存在逃逸行为,则可进行如下优化:
Java 是半编译半解释型语言,将解释执行与编译执行二者结合起来进行:
编译器(Compiler)和解释器(Interpreter)的工作都是将程序员的源代码翻译成可执行的机器代码,要么一次性翻译(编译器),要么逐行解释并运行(解释器)。
HostSpot 的默认执行方式:
HotSpot 可以通过 VM 参数设置程序执行方式:
在分层编译的工作模式出现前,采用客户端编译器还是服务端编译器完全取决于虚拟机是运行在客户端模式还是服务端模式下,可以在启动时通过
-client
或-server
参数进行指定,也可以让虚拟机根据自身版本和宿主机性能来自主选择。
要编译出优化程度越高的代码通常都需要越长的编译时间,为了在程序启动速度与运行效率之间达到最佳平衡,HotSpot 虚拟机在编译子系统中加入了分层编译(Tiered Compilation),JVM 将执行状态分成了 5 个层次:
C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度
C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高
实施分层编译后,解释器、C1编译器和C2编译器就会同时工作,可以用C1编译器获取更高的编译速度、用C2编译器来获取更好的编译质量。
即时编译器编译的目标是 “热点代码”,它主要分为以下两类:
判断某段代码是否是热点代码的行为称为 “热点探测” (Hot Spot Code Detection),主流的热点探测方法有以下两种:
HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter)
-XX:CompileThreshold
设置
工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器提交一个该方法的代码编译请求
语法糖:指 Java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担
public class Candy {
}
public class Candy {
// 这个无参构造是编译器帮助我们加上的
public Candy() {
super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
}
}
这段代码在 JDK 5 之前是无法编译通过的
Integer x = 1;
int y = x;
必须写成如下代码:
Integer x = Integer.valueOf(1); //装箱
int y = x.intValue(); //拆箱
JDK5 以后编译阶段自动转换成上述片段
泛型也是在 JDK 5 开始加入的特性,但 Java 在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:
List<Integer> list = new ArrayList<>();
list.add(10); // 实际调用的是 List.add(Object e)
Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
编译器真正生成的字节码中,还要额外做一个类型转换的操作:
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:
// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
public class Candy {
public static void foo(String... args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
可变参数 String... args
其实是 String[] args
, Java 编译器会在编译期间将上述代码变换为:
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
注:如果调用了 foo()
则等价代码为 foo(new String[]{})
,创建了一个空的数组,而不会传递 null 进去
数组的循环:
int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖
for (int e : array) {
System.out.println(e);
}
编译后为循环取数:
for(int i = 0; i < array.length; ++i) {
int e = array[i];
System.out.println(e);
}
集合的循环:
List<Integer> list = Arrays.asList(1,2,3,4,5);
for (Integer i : list) {
System.out.println(i);
}
编译后转换为对迭代器的调用:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator iter = list.iterator();
while(iter.hasNext()) {
Integer e = (Integer)iter.next();
System.out.println(e);
}
注:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器
switch 可以作用于字符串和枚举类:
switch (str) {
case "hello": {
System.out.println("h");
break;
}
case "world": {
System.out.println("w");
break;
}
}
注意:switch 配合 String 和枚举使用时,变量不能为 null
会被编译器转换为:
byte x = -1;
switch(str.hashCode()) {
case 99162322: // hello 的 hashCode
if (str.equals("hello")) {
x = 0;
}
break;
case 113318802: // world 的 hashCode
if (str.equals("world")) {
x = 1;
}
}
switch(x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
break;
}
总结:
switch 枚举的例子,原始代码:
enum Sex {
MALE, FEMALE
}
public class Candy {
public static void foo(Sex sex) {
switch (sex) {
case MALE:
System.out.println("男");
break;
case FEMALE:
System.out.println("女");
break;
}
}
}
编译转换后的代码:
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
// 数组大小即为枚举元素个数,里面存储 case 用来对比的数字
static int[] map = new int[2];
static {
map[Sex.MALE.ordinal()] = 1;
map[Sex.FEMALE.ordinal()] = 2;
}
}
public static void foo(Sex sex) {
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
}
JDK 7 新增了枚举类:
enum Sex {
MALE, FEMALE
}
编译转换后:
public final class Sex extends Enum<Sex> {
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values() {
return $VALUES.clone();
}
public static Sex valueOf(String name) {
return Enum.valueOf(Sex.class, name);
}
}
JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources
,格式:
try(资源变量 = 创建资源对象){
} catch( ) {
}
其中资源对象需要实现 AutoCloseable 接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码:
try(InputStream is = new FileInputStream("a.txt")) {
System.out.println(is);
} catch (IOException e) {
e.printStackTrace();
}
转换成:
try {
InputStream is = new FileInputStream("a.txt");
Throwable t = null;
try {
System.out.println(is);
} catch (Throwable e1) {
// t 是我们代码出现的异常
t = e1;
throw e1;
} finally {
// 判断了资源不为空
if (is != null) {
// 如果我们代码有异常
if (t != null) {
try {
is.close();
} catch (Throwable e2) {
// 如果 close 出现异常,作为被压制异常添加
t.addSuppressed(e2);
}
} else {
// 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
is.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
addSuppressed(Throwable e)
:添加被压制异常,是为了防止异常信息的丢失(fianlly 中如果抛出了异常)
方法重写时对返回值分两种情况:
class A {
public Number m() {
return 1;
}
}
class B extends A {
@Override
// 子类m方法的返回值是Integer是父类m方法返回值Number的子类
public Integer m() {
return 2;
}
}
对于子类,Java 编译器会做如下处理:
class B extends A {
public Integer m() {
return 2;
}
// 此方法才是真正重写了父类 public Number m() 方法
public synthetic bridge Number m() {
// 调用 public Integer m()
return m();
}
}
其中桥接方法比较特殊,仅对 Java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突
源代码:
public class Candy {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
转化后代码:
// 额外生成的类
final class Candy$1 implements Runnable {
Candy$1() {
}
public void run() {
System.out.println("ok");
}
}
public class Candy {
public static void main(String[] args) {
Runnable runnable = new Candy$1();
}
}
引用局部变量的匿名内部类,源代码:
public class Candy {
public static void test(final int x) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok:" + x);
}
};
}
}
转换后代码:
final class Candy$1 implements Runnable {
int val$x;
Candy$1(int x) {
this.val$x = x;
}
public void run() {
System.out.println("ok:" + this.val$x);
}
}
public class Candy {
public static void test(final int x) {
Runnable runnable = new Candy$1(x);
}
}
局部变量在底层创建为内部类的成员变量,必须是 final 的原因:
public class Reflect1 {
public static void foo() {
System.out.println("foo...");
}
public static void main(String[] args) throws Exception {
Method foo = Reflect1.class.getMethod("foo");
for (int i = 0; i <= 16; i++) {
System.out.printf("%d\t", i);
foo.invoke(null);
}
System.in.read();
}
}
foo.invoke 0 ~ 15 次调用的是 MethodAccessor 的实现类 NativeMethodAccessorImpl.invoke0()
,本地方法执行速度慢;当调用到第 16 次时,会采用运行时生成的类 sun.reflect.GeneratedMethodAccessor1
代替
public Object invoke(Object obj, Object[] args)throws Exception {
// inflationThreshold 膨胀阈值,默认 15
if (++numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
// 【调用本地方法实现】
return invoke0(method, obj, args);
}
private static native Object invoke0(Method m, Object obj, Object[] args);
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
// 如果有参数,那么抛非法参数异常
block4 : {
if (arrobject == null || arrobject.length == 0) break block4;
throw new IllegalArgumentException();
}
try {
// 【可以看到,已经是直接调用方法】
Reflect1.foo();
// 因为没有返回值
return null;
}
//....
}
通过查看 ReflectionFactory 源码可知:
即时编译器除了将字节码编译为本地机器码外,还会对代码进行一定程度的优化,它包含多达几十种优化技术,这里选取其中代表性的进行介绍:
逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸
public static StringBuilder concat(String... strings) {
StringBuilder sb = new StringBuilder();
for (String string : strings) {
sb.append(string);
}
return sb; // 发生了方法逃逸
}
public static String concat(String... strings) {
StringBuilder sb = new StringBuilder();
for (String string : strings) {
sb.append(string);
}
return sb.toString(); // 没有发生方法逃逸
}
如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配
-XX:+EliminateAllocations
:开启标量替换-XX:+PrintEliminateAllocations
:查看标量替换情况-XX:+EliminateLocks
可以开启同步消除 ( - 号关闭)方法内联:将调用的函数代码编译到调用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程
方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。
private static int square(final int i) {
return i * i;
}
System.out.println(square(9));
square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置:
System.out.println(9 * 9);
还能够进行常量折叠(constant folding)的优化:
System.out.println(81);
如果一个表达式 E 之前已经被计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生过变化,那么 E 这次的出现就称为公共子表达式。对于这种表达式,无需再重新进行计算,只需要直接使用前面的计算结果即可。
对于虚拟机执行子系统来说,每次数组元素的读写都带有一次隐含的上下文检查以避免访问越界。如果数组的访问发生在循环之中,并且使用循环变量来访问数据,即循环变量的取值永远在 [0,list.length) 之间,那么此时就可以消除整个循环的数据边界检查,从而避免多次无用的判断。
从Java5开始,在JDK中自带的Java监控和管理控制台。用于对JVM中内存、线程和类等的监控。
多功能的监测工具,可以连续监测,集成了多个JDK命令行工具,用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)等,甚至代替JConsole。
主要功能:
1-方法调用:对方法调用的分析可以帮助您了解应用程序正在做什么,并找到提高其性能的方法
2-内存分配:通过分析堆上对象、引用链和垃圾收集能帮您修复内存泄露问题,优化内存使用
3-线程和锁:JProfiler提供多种针对线程和锁的分析视图助您发现多线程问题
4-高级子系统:许多性能问题都发生在更高的语义级别上。例如,对于JDBC调用,您可能希望找出执行最慢的SQL语句。JProfiler支持对这些子系统进行集成分析
Arthas是Alibaba开源的Java诊断工具
JDK自带的工具,用于查看JVM运行时的状态
java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_351.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
JVM参数⼤致可以分为三类:
标注指令: -开头,所有的JVM实现都必须实现这些参数的功能,而且向后兼容。可以⽤ java -help 打印出来。
⾮标准指令: -X开头,默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容。可以⽤ java -X 打印出来。
非稳定参数: -XX 开头,此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;
java -XX:+PrintCommandLineFlags // 查看当前JVM的不稳定指令。
java -XX:+PrintFlagsInitial // 查看所有不稳定指令的默认值。
java -XX:+PrintFlagsFinal // 查看所有不稳定指令最终⽣效的实际值。
-Xmx Size
-Xss size
-XX:StringTableSize=个数
StringTable桶个数
-XX:MaxTenuringThreshold
:定义年龄的阈值,默认是 15
-XX:PretenureSizeThreshold
:大于此值的对象直接在老年代分配
-XX:MaxMetaspaceSize=size
元空间大小设置
-XX:MaxTenuringThreshold
调大对象进入老年代的年龄,让对象在新生代多存活一段时间
-XX:UseTLAB
:设置是否开启 TLAB 空间
-XX:TLABWasteTargetPercent
:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1%
-XX:TLABRefillWasteFraction
:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配
-XX:+EliminateAllocations
:开启标量替换
-XX:+PrintEliminateAllocations
:查看标量替换情况
含义参数
堆初始大小-Xms
堆最大大小-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC