类加载、字节码技术

类文件结构

//魔数
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):优化的范围涵盖了多个基本块

posted @   半条咸鱼  阅读(69)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示