类加载、字节码技术
类文件结构
//魔数
u4 magic;
//次要版本
u2 minor_version;
//主要版本
u2 major_version;
//常量池信息
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
//访问修饰
u2 access_flags;
//包名、类名信息
u2 this_class;
//父类
u2 super_class;
//接口信息
u2 interfaces_count;
u2 interfaces[interfaces_count];
//类中变量信息
u2 fields_count;
field_info fields[fields_count];
//类中方法信息
u2 methods_count;
method_info methods[methods_count];
//类的附加属性信息
u2 attributes_count;
attribute_info attributes[attributes_count];
字段信息
FieldType | Type | 解释 |
B | byte | 带符号 byte |
C | char | 基本多语言平面中的 Unicode 字符代码点, 使用 UTF-16 编码 |
D | double | 双精度浮点值 |
F | float | 单精度浮点值 |
I | int | integer |
J | long | long integer |
L ClassName ; |
reference | class 实例 |
S | short | 带符号 short |
Z | boolean | true 或 false |
[ | reference | 一维数组 |
方法执行流程
1、.java 原始代码 -> 编译为 .class 字节码文件
2、常量池载入运行时常量池
3、方法字节码载入方法区
4、main 线程开始运行,分配栈帧内存
5、return
(1)完成 main 方法调用,弹出 main 栈帧
(2)清除 main 操作数栈内容
(3)程序结束
条件判断指令
指令 | 助记符 | 含义 |
0x99 | ifeq | 判断是否 == 0 |
0x9a | ifne | 判断是否 != 0 |
0x9b | iflt | 判断是否 < 0 |
0x9c | ifge | 判断是否 >= 0 |
0x9d | ifgt | 判断是否 > 0 |
0x9e | ifle | 判断是否 <= 0 |
0x9f | if_icmpeq | 两个 int 是否 == |
0xa0 | if_icmpne | 两个 int 是否 != |
0xa1 | if_icmplt | 两个 int 是否 < |
0xa2 | if_icmpge | 两个 int 是否 >= |
0xa3 | if_icmpgt | 两个 int 是否 > |
0xa4 | if_icmple | 两个 int 是否 <= |
0xa5 | if_acmpeq | 两个引用是否 == |
0xa6 | if_acmpne | 两个引用是否 != |
0xc6 | ifnull | 判断是否 == null |
0xc7 | ifnonnull | 判断是否 != null |
1、在局部变量表中,32 位以内的类型只占用一个 Slot(包括 returnAddress 类型),64 位的类型(long 和 double)占用两个 Slot
(1)byte、short、char 在存储前被转换为 int
(2)boolean 在存储前被转换为 int,0 表示 false,非 0 表示 true
2、goto 用来进行跳转到指定行号的字节码
3、循环控制指令使用上述指令实现
构造方法
1、编译器会按从上至下的顺序,收集所有 static 静态代码块、静态成员赋值的代码,合并为一个特殊的方法:<cinit>()V
2、<cinit>()V 方法会在类加载的初始化阶段被调用
3、编译器会按从上至下的顺序,收集所有 {} 代码块、成员变量赋值的代码,形成新的构造方法:<init>()V,但原始构造方法内的代码总是在最后
方法调用指令
1、普通调用指令
(1)invokestatic:调用静态方法,解析阶段确定唯一方法版本
(2)invokespecial:调用 <init> 方法、私有方法、父类方法,解析阶段确定唯一方法版本
(3)invokevirtual:调用所有虚方法
(4)invokeinterface:调用接口方法
(5)以上四条指令固化在虚拟机内部,不可人为干预方法的调用执行
(6)invokestatic 指令和 invokespecial 指令调用的方法称为非虚方法,其余的(final 修饰的除外)称为虚方法
(7)fianl 方法、private 方法,构造方法都是由 invokespecial 指令来调用,属于静态绑定
(8)普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态
(9)成员方法与静态方法调用的另一个区别是,执行方法前是否需要“对象引用”
2、动态调用指令
(1)invokedynamic:动态解析出需要调用的方法,然后执行
(2)invokedynamic 指令支持由用户确定方法版本
(3)JVM 字节码指令集一直比较稳定,直到 Java 7 中,才增加一个 invokedynamic 指令,因为 Java 为了实现动态类型语言支持而做的一种改进
(4)但在 Java 7 中并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 底层字节码工具,来产生 invokedynamic 指令
(5)直到 Java 8 的 Lambda 表达式的出现,invokedynamic 指令在 Java 中才有直接的生成方式
(6)Java 7 中增加动态语言类型支持的本质,是对 JVM 规范的修改,而不是对 Java 语言规则的修改,增加虚拟机中的方法调用,最直接的受益者就是运行在 Java 平台的动态语言的编译器
3、new:创建对象,给对象分配堆内存,执行成功会将对象引用,压入操作数栈
4、dup:赋值操作数栈栈顶,需要两份引用的原因:配合 invokespecial 调用该对象的构造方法 <init>:()V(消耗掉栈顶一个引用),且配合局部变量压栈指令,赋值给局部变量
多态原理
1、因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令
2、在执行 invokevirtual 指令时,经历以下步骤
(1)先通过栈帧中对象的引用查找对象
(2)分析对象头,找到对象实际 Class
(3)Class 结构中有 vtable
(4)查询 vtable,找到方法的具体地址
(5)执行方法的字节码
异常
1、catch
(1)Exception table 结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
(2)多个 single-catch 块,即不同 catch 捕获不同 Exception:当异常出现时,只能进入 Exception table 中一个分支,多个变量共用局部变量表中同一 slot 位置
(3)multi-catch,即同一个 catch 捕获不同 Exception:[from, to) 检测范围相同,多个异常共用一个变量
2、finally
(1)工作流程:finally 中的代码被复制为 3 份,分别放入 try、catch、出现异常,但未被捕获
(2)从字节码指令来看,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次
(3)finally 出现 return:return 指令代替 athrow,即不再抛出异常,开发中不要在 finally 中使用 return
(4)try 出现 return:不会立刻返回值,而是先判断是否还有 finally,如果有就执行 finally,如果没有就返回值
synchronized
1、加锁过程
(1)dup:复制一份锁对象,放到操作数栈,用于加锁时消耗
(2)将操作数栈顶元素弹出,暂存到局部变量表的 slot,这时操作数栈中有一份对象的引用
(3)monitorenter:加锁
(4)执行加锁内容
2、释放锁过程
(1)将局部变量表中的锁对象,加载到栈顶
(2)monitorexit:释放锁
3、持锁期间出现异常
(1)需要使用 Exception table
(2)先释放锁(与 2 相同),再处理异常
编译期处理
1、语法糖
(1)在 java 编译器把 *.java 源码,编译为 *.class 字节码的过程中,自动生成、转换的一些代码
(2)目的:减轻开发负担
2、默认构造器
public class Candy1 {
}
public class Candy1 {
//无参构造器由编译器添加
public Candy1() {
//即调用父类 Object 的无参构造方法,即调用 java/lang/Object.<init>:()V
super();
}
}
3、自动拆装箱
(1)在 JDK 5 以后加入
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
public class Candy2 {
public static void main(String[] args) {
//装箱:基本类型赋值给包装类型
Integer x = Integer.valueOf(1);
//拆箱:包装类型赋值给基本类型
int y = x.intValue();
}
}
4、泛型集合取值
(1)泛型在 JDK 5 开始加入
(2)但 Java 在编译泛型代码后,会执行泛型擦除,即泛型信息在编译为字节码后就丢失,实际类型都当做 Object 类型来处理
public class Candy3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
//实际调用List.add(Object e);
list.add(10);
//实际调用Object obj = List.get(int index)
//并将 Object 转为 Integer:Integer x = (Integer)list.get(0);
Integer x = list.get(0);
}
}
(3)擦除的是字节码上的泛型信息,LocalVariableTypeTable 仍保留方法参数泛型的信息
(4)无法通过反射,在 LocalVariableTypeTable 获取泛型信息,只有在方法参数、返回值上,才能通过反射获取泛型信息
5、可变参数
(1)在 JDK 5 开始加入
(2)例:main 方法:String... args -> String[] args
(3)注意:如果调用 foo() 则等价代码为 foo(new String[]{}),创建一个空的数组,而不会传递 null
public class Candy4 {
public static void foo(String... args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
public class Candy4 {
public static void foo(String[] args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
}
6、简化数组赋初始值
int[] array = {1, 2, 3, 4, 5};
int[] arr = new int[]{1, 2, 3, 4, 5};
7、foreach 循环
(1)JDK 5 开始引入
(2)数组
public class Candy5 {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
for(int x : arr) {
System.out.println(x);
}
}
}
public class Candy5 {
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
for(int i = 0; i < arr.length; ++i) {
int x = arr[i];
System.out.println(x);
}
}
}
(3)集合
public class Candy5 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer x : list) {
System.out.println(x);
}
}
}
public class Candy5 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// 获得该集合的迭代器
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
Integer x = iterator.next();
System.out.println(x);
}
}
}
8、switch 字符串
(1)从 JDK 7 开始,switch 可以作用于字符串、枚举类
(2)执行两次 switch,第一次:根据字符串 hashCode 和 equals,将字符串的转换为相应 byte 类型,第二次:利用 byte 执行进行比较
(3)第一次必须比较 hashCode,且利用 equals 比较:hashCode 是为了提高效率,减少可能的比较,equals 为了防止 hashCode 冲突
public class Cnady6 {
public static void main(String[] args) {
String str = "hello";
switch (str) {
case "hello" :
System.out.println("h");
break;
case "world" :
System.out.println("w");
break;
default:
break;
}
}
}
public class Candy6 {
public static void main(String[] args) {
String str = "hello";
int x = -1;
//第一个switch,通过字符串 hashCode + value,判断是否匹配
switch (str.hashCode()) {
//hello 的 hashCode
case 99162322 :
//因为字符串 hashCode 有可能相等,所以再次比较
if(str.equals("hello")) {
x = 0;
}
break;
//world 的 hashCode
case 11331880 :
//因为字符串 hashCode 有可能相等,所以再次比较
if(str.equals("world")) {
x = 1;
}
break;
default:
break;
}
//第二个switch,进行输出判断
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
break;
default:
break;
}
}
}
9、switch 枚举
enum Sex {
MALE, FEMALE;
}
public class Candy7 {
public static void foo(Sex sex) {
switch (sex) {
case MALE:
System.out.println("男");
break;
case FEMALE:
System.out.println("女");
break;
}
}
}
public class Candy7 {
/**
* 定义一个合成类(仅 JVM 使用,不可见)
* 用来映射枚举 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
//数组大小即为枚举元素个数,存放 case 用于比较的数字
static int[] map = new int[2];
static {
//ordinal 即枚举元素对应所在的位置,MALE 为 0 ,FEMALE 为 1
map[SEX.MALE.ordinal()] = 1;
map[SEX.FEMALE.ordinal()] = 2;
}
}
public static void main(String[] args) {
SEX sex = SEX.MALE;
//将对应位置枚举元素的值赋给 x ,用于 case 操作
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("man");
break;
case 2:
System.out.println("woman");
break;
default:
break;
}
}
}
10、枚举类
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;
//调用构造函数,传入枚举元素的值及 ordinal
static {
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
/**
* 唯一的构造函数,程序员不能调用这个构造函数
* 它是为编译器发出的代码所使用的,以回应枚举类型声明
* @param name - 这个枚举常量的名称,也就是用来声明它的标识符
* @param ordinal - 这个枚举常数的序数(它在枚举声明中的位置,初始常数被分配在这里)
*/
//调用父类中的方法
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);
}
}
11、try-with-resources
(1)JDK 7 新增对需要关闭的资源处理的特殊语法
try(资源变量 = 创建资源对象) {
} catch() {
}
(2)其中资源对象需要实现 AutoCloseable 接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口
(3)使用 try-with-resources,可以不用写 finally 语句块,编译器会帮助生成关闭资源代码
public class Candy9 {
public static void main(String[] args) {
try(InputStream is = new FileInputStream("d:\\1.txt")){
System.out.println(is);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Candy9 {
public static void main(String[] args) {
try {
InputStream is = new FileInputStream("d:\\1.txt");
Throwable t = null;
try {
System.out.println(is);
} catch (Throwable e1) {
//t为代码出现的异常
t = e1;
throw e1;
} finally {
//判断资源不为null
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();
}
}
}
(4)添加被压制异常,即 addSuppressed(Throwable e) 的原因:防止异常信息的丢失,如:try-with-resources 生成 fianlly 中抛出异常, 既可以获得 catch 异常,也可以查到 finally 中被压制的异常
12、方法重写时的桥接方法
(1)方法重写时对返回值分两种情况:父子类的返回值完全一致、子类返回值可以是父类返回值的子类
class A {
public Number m() {
return 1;
}
}
class B extends A {
@Override
//子类 m 方法的返回值是 Integer,是父类 m 方法返回值 Number 的子类
public Integer m() {
return 2;
}
}
(2)桥接方法比较特殊,仅对 JVM 可见,且与原 public Integer m() 没有命名冲突
class B extends A {
public Integer m() {
return 2;
}
//此方法真正重写父类 public Number m()
public synthetic bridge Number m() {
//调用 public Integer m()
return m();
}
}
(3)反射验证
public static void main(String[] args) {
for(Method m : B.class.getDeclaredMethods()) {
System.out.println(m);
}
}
13、匿名内部类
(1)直接创建匿名内部类
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
public class Candy10 {
public static void main(String[] args) {
//使用额外创建的类,来创建匿名内部类对象
Runnable runnable = new Candy10$1();
}
}
//创建一个额外的类,实现 Runnable 接口
final class Candy10$1 implements Runnable {
@Override
public void run() {
System.out.println("running...");
}
}
(2)引用局部变量的匿名内部类
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok:" + x);
}
};
}
}
public class Candy11 {
//匿名内部类引用局部变量时,局部变量必须是 final
//因为在创建Candy11$1 对象时,将 x 的值赋值给 Candy11$1 对象的 val$x 属性,所以 x 不应该再发生变化,如果变化,则 val$x 属性不能一起变化
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}
//额外生成的类
final class Candy11$1 implements Runnable {
int val$x;
Candy11$1(int x) {
this.val$x = x;
}
public void run() {
System.out.println("ok:" + this.val$x);
}
}
类的加载过程
1、加载
2、链接:验证 -> 准备 -> 解析
3、初始化
4、使用
5、卸载
加载
1、将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,其重要 field 有:
(1)_java_mirror 即 Java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 Java 使用
(2)_super 即父类
(3)_fields 即成员变量
(4)_methods 即方法
(5)_constants 即常量池
(6)_class_loader 即类加载器
(7)_vtable 虚方法表
(8)_itable 接口方法表
2、如果这个类还有父类没有加载,先加载父类
3、加载、链接可能交替运行
4、注意
(1)instanceKlass 保存在方法区,JDK 8 以后,方法区位于元空间中,而元空间又位于本地内存中
(2)_java_mirror 保存在堆内存中
(3)InstanceKlass 和 *.class(Java 镜像类)互相保存对方的地址
(4)类的对象在对象头中保存 *.class 地址,让对象可以通过其找到方法区中的 instanceKlass,从而获取类的各种信息
链接
1、验证:验证类是否符合 JVM规范,安全性检查
2、准备:为 static 变量分配空间,设置默认值
(1)static 变量在 JDK 7 之前存储在 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
(2)static 变量分配空间和赋值是两个步骤:分配空间在准备阶段完成,赋值在初始化阶段完成
(3)如果 static 变量是 final 基本类型,以及字符串常量,则在编译阶段就确定其值,赋值在准备阶段完成
(4)如果 static 变量是 final 的,但属于引用类型,则赋值在初始化阶段完成
3、解析:将常量池中的符号引用解析为直接引用
初始化
1、初始化即调用 <clinit>()V ,虚拟机会保证这个类的构造方法的线程安全
2、初始化时机:类初始化是懒加载
(1)main 方法所在的类,总会被首先初始化
(2)首次访问这个类的静态变量 / 静态方法时
(3)子类初始化,如果父类还没初始化,会引发父类初始化
(4)子类访问父类的静态变量,只会触发父类的初始化
(5)Class.forName
(6)new 会导致初始化
3、不会导致类初始化的情况
(1)访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
(2)类对象.class 不会触发初始化
(3)创建该类的数组不会触发初始化
(4)类加载器的 loadClass 方法
(5)Class.forName 第二个参数为 false 时
public static Class<?> forName(String name,
boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
类加载器
1、只用于实现类的加载动作,但在 Java 程序的作用远超类加载阶段
2、类的唯一性
(1)对于任意一个类,都需要由加载它的类加载器、这个类本身,共同确认其在 JVM 中的唯一性
(2)每一个类加载器,都拥有一个独立的类命名空间:比较两个类是否相等
(3)只有在两个类是由同一个类加载器加载的前提下才有意义,否则,即使两个类源自同一个 .class 文件,被同一个虚拟机加载,只要加载他们的类加载器不同,则两个类就必定不相等
3、JDK 8 为例
名称 | 加载类的位置 | 说明 |
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
运行期优化
1、即时编译
2、分层编译:JVM 将执行状态分成 5 个层次
(1)第 0 层:程序纯解释执行,用解释器将字节码翻译为机器码,并且解释器不开启性能监控功能(Profiling)
(2)第 1 层:使用 C1 即时编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能(不带 profiling)
(3)第 2 层:使用 C1 即时编译器编译执行,仅开启方法及回边次数统计等有限的性能监控功能(带基本的 profiling)
(4)第 3 层:使用 C1 即时编译器编译执行,开启全部性能监控,除了第 2 层的统计信息外,还会收集如:分支跳转、虚方法调用版本等全部的统计信息(带完全的 profiling)
(5)第 4 层:使用 C2 即时编译器将字节码编译为本地代码,相比起 C1 即时编译器,C2 即时编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化
(6)以上层次并不是固定不变的,根据不同的运行参数和版本,虚拟机可以调整分层的数量,各层次编译之间的交互、转换关系如下
3、profiling:在运行过程中,收集一些程序执行状态的数据,例如:方法的调用次数、循环的回边次数等
4、解释器
(1)将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
(2)将字节码解释为针对所有平台都通用的机器码
5、即时编译器(JIT)
(1)将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
(2)根据平台类型,生成平台特定的机器码
6、选择执行
(1)对于大部分不常用代码,无需耗费时间将其编译成机器码,而是采取解释执行的方式运行
(2)对于仅占据小部分的热点代码,则将其编译成机器码,以达到理想的运行速度
(3)执行效率:Interpreter < C1 < C2,总的目标是发现热点代码,并优化这些热点代码
逃逸分析
1、Escape Analysis
(1)Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术
(2)不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术
2、逃逸分析的 JVM 参数
(1)开启逃逸分析:-XX:+DoEscapeAnalysis
(2)关闭逃逸分析:-XX:-DoEscapeAnalysis
(3)显示分析结果:-XX:+PrintEscapeAnalysis
(4)逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加参数
3、基本原理:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用
(1)方法逃逸:作为调用参数传递到其他方法中
(2)线程逃逸:有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量
(3)从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度
(4)如果能证明一个对象不会逃逸到方法或线程之外(换句话说是别的方法或线程无法通过任何途径访问到这个对象),或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化
对象逃逸状态
1、全局逃逸(GlobalEscape)
(1)一个对象的作用范围,逃出当前方法或当前线程,有以下几种场景:
(2)对象是一个静态变量
(3)对象是一个已经发生逃逸的对象
(4)对象作为当前方法的返回值
2、参数逃逸(ArgEscape)
(1)一个对象被作为方法参数传递,或被参数引用,但在调用过程中不会发生全局逃逸
(2)这个状态是通过被调方法的字节码确定的
3、没有逃逸:方法中的对象没有发生逃逸
逃逸分析优化:当一个对象没有逃逸时,可以得到以下几个虚拟机的优化
1、锁消除 / 同步消除
(1)当编译器确定当前对象只有当前线程使用,则会移除该对象的同步锁
(2)例如:StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作
(3)锁消除的 JVM 参数如下
(4)开启锁消除:-XX:+EliminateLocks
(5)关闭锁消除:-XX:-EliminateLocks
(6)锁消除在 JDK 8 中默认开启,并且锁消除都要建立在逃逸分析的基础上
2、标量替换
(1)基础类型、对象的引用为标量,不能被进一步分解;而能被进一步分解的量为聚合量,比如:对象
(2)标量替换:对象是聚合量,可以被进一步分解成标量,将其成员变量分解为分散的变量
(3)如果一个对象没有发生逃逸,则不用创建它,只会在栈或寄存器上,创建它用到的成员标量,节省内存空间,也提升应用程序性能
(4)标量替换的 JVM 参数如下
(5)开启标量替换:-XX:+EliminateAllocations
(6)关闭标量替换:-XX:-EliminateAllocations
(7)显示标量替换详情:-XX:+PrintEliminateAllocations
(8)标量替换在 JDK 8 中默认开启,并且都要建立在逃逸分析的基础上
3、栈上分配
(1)当对象没有发生逃逸时,该对象就可以通过标量替换,分解成成员标量分配在栈内存中
(2)和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能
方法内联
1、内联函数:在程序编译时,编译器将程序中出现的内联函数的调用表达式,用内联函数的函数体来直接进行替换
2、JVM 内联函数:在 C++ 中,是否为内联函数,由开发人员决定,而在 Java 中,由编译器决定,Java 不支持直接声明为内联函数的,如果需要内联,只能够向编译器提出请求,使用 final 指定函数是希望被 JVM 内联的
3、一般的函数都不会被当做内联函数,只有声明 final 后,编译器才会考虑是否将其函数变成内联函数
4、如果 JVM 监测到一些小方法被频繁的执行,它会把方法的调用替换成方法体本身
private int add4(int x1, int x2, int x3, int x4) {
return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
return x1 + x2;
}
private int add4(int x1, int x2, int x3, int x4) {
return x1 + x2 + x3 + x4;
}
5、首先短方法更利于 JVM 推断,流程更明显,作用域更短,副作用也更明显
6、方法是否内联,影响成员变量读取的优化
公共子表达式消除
1、语言无关的经典优化技术之一
2、含义:如果一个表达式 E 之前已经被计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生变化,那么 E 的这次出现就称为公共子表达式
(1)对于这种表达式,没有必要花时间再对它重新进行计算,只需要直接用前面计算过的表达式结果代替 E
(2)局部公共子表达式消除(Local Common Subexpression Elimination):优化仅限于程序基本块内
(3)全局公共子表达式消除(Global Common Subexpression Elimination):优化的范围涵盖了多个基本块
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战