编译器优化处理也就是所谓的 语法糖 ,其实就是指 java 编译器把 .java 源码编译为 .class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利 !
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
public class Candy1 {
}
经过编译期优化后
public class Candy1 {
// 这个无参构造器是java编译器帮我们加上的
public Candy1() {
super(); //当一个构造方法的第一行,既没有this也没有super,默认会有一个super( ),
//表示当前子类构造方法,调用父类的无参构造方法,
}
}
所以,必须保证父类的无参构造方法是存在的!虽说new一个对象一直在调用父类的构造方法!其本质还是new的一个对象,只是将父类
的特征继承过来! super( )可以看作,初始化当前对象的父类型特征;
测试:
//super测试
public class Test{
public static void main(string[] args){
new B(); //输出:构造A 构造B
}
}
class A{
public A(){
System.out.println("构造A")}
}
class B exdents A {
public B(){
super(); //这个super一般省略,等价于new A ( ),先初始化父亲,才能new儿子!
System.out.println("构造B")}
}
基本类型和其包装类型的相互转换过程,称为拆装箱
注:在 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(); // 包装类型赋值给基本类型,称谓拆箱
}
}
Java中为了提高程序的执行效率,将[-128,127]之间的所有包装对象提前创建好,放到了一个方法区内存中的一个整型常量池当中,使得
这个区间的对象不需要再new了!
泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理
public class Candy3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(); //泛型
list.add(10);
Integer x = list.get(0);
}
}
对应字节码
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
// 这里进行了泛型擦除,实际调用的是add(Objcet o)
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
// 这里也进行了泛型擦除,实际调用的是get(Object o)
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
// 这里进行了类型转换,将 Object 转换成了 Integer
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return
所以调用 get 函数取值时,有一个类型转换的操作。
Integer x = (Integer) list.get(0);
如果要将返回结果赋值给一个 int 类型的变量,则还有自动拆箱的操作
int x = (Integer) list.get(0).intValue();
使用反射可以得到,参数的类型以及泛型类型。泛型反射代码如下:
public static void main(String[] args) throws NoSuchMethodException {
// 1. 拿到方法
Method method = Code_20_ReflectTest.class.getMethod("test", List.class, Map.class);
// 2. 得到泛型参数的类型信息
Type[] types = method.getGenericParameterTypes();
for(Type type : types) {
// 3. 判断参数类型是否,带泛型的类型。
if(type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
// 4. 得到原始类型
System.out.println("原始类型 - " + parameterizedType.getRawType());
// 5. 拿到泛型类型
Type[] arguments = parameterizedType.getActualTypeArguments();
for(int i = 0; i < arguments.length; i++) {
System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
}
}
}
}
public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
return null;
}
输出:
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object
可变参数也是 JDK 5 开始加入的新特性: 例如:
public class Candy4 {
public static void foo(String... args) {
// 将 args 赋值给 arr ,可以看出 String... 实际就是 String[]
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo("hello", "world"); //传入2个字符串对象
}
}
可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。 同 样 java 编译器会在编译期间将上述代码变换为:
public class Candy4 {
public Candy4 {
}
public static void foo(String[] args) {
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo(new String[]);
}
}
注意,如果调用的是 foo() ,即未传递参数时,默认传入空的字符串数组,等价代码为 foo(new String[]{}) ,注意:创建了一个空数组,而不是直接传递的 null .
仍是 JDK 5 开始引入的语法糖,
数组的foreach循环:
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 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);
}
}
}
如果是集合使用 foreach
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);
}
}
集合要使用 foreach ,需要该集合类实现了 Iterable 接口,因为集合的遍历需要用到迭代器 Iterator.
public class Candy5 {
public 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);
}
}
}
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
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 Candy6() {
}
public static void main(String[] args) {
String str = "hello";
int x = -1;
// 通过字符串的 hashCode + value 来判断是否匹配
switch (str.hashCode()) {
// hello 的 hashCode
case 99162322 :
// 再次比较,因为字符串的 hashCode 有可能相等
if(str.equals("hello")) {
x = 0;
}
break;
// world 的 hashCode
case 11331880 :
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;
}
}
}
过程说明:
在编译期间,单个的 switch 被分为了两个
enum SEX {
MALE, FEMALE;
}
public class Candy7 {
public static void main(String[] args) {
SEX sex = SEX.MALE;
switch (sex) {
case MALE:
System.out.println("man");
break;
case FEMALE:
System.out.println("woman");
break;
default:
break;
}
}
}
编译器中执行的代码如下
enum SEX {
MALE, FEMALE;
}
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;
}
}
}
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 {
// 调用构造函数,传入枚举元素的值及 ordinal
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-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 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 {
// 判断了资源不为空
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) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常):
public class Test6 {
public static void main(String[] args) {
try (MyResource resource = new MyResource()) {
int i = 1/0;
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyResource implements AutoCloseable {
public void close() throws Exception {
throw new Exception("close 异常");
}
}
输出:
java.lang.ArithmeticException: / by zero
at test.Test6.main(Test6.java:7)
Suppressed: java.lang.Exception: close 异常
at test.MyResource.close(Test6.java:18)
at test.Test6.main(Test6.java:6)
我们都知道,方法重写时对返回值分两种情况:
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();
}
}
public class Candy10 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("running...");
}
};
}
}
转换后的代码
public class Candy10 {
public static void main(String[] args) {
// 用额外创建的类来创建匿名内部类对象
Runnable runnable = new Candy10$1();
}
}
// 创建了一个额外的类,实现了 Runnable 接口
final class Candy10$1 implements Runnable {
public Demo8$1() {
}
@Override
public void run() {
System.out.println("running...");
}
}
引用局部变量的匿名内部类,源代码:
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok:" + 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);
}
}
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}
注意:这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建 Candy11$1 对象时,将 x 的值赋值给了 Candy11$1 对象的 值后,如果不是 final 声明的 x 值发生了改变,匿名内部类则值不一致。
扩展:
总结:匿名内部类,如果访问外部的局部变量,是通过构造方法将这个局部变量拿到的,并将其设置为自身的属性,然后this.属性获取!
来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:
- JDBC
- Servlet 初始化器
- Spring 容器
- Dubbo(对 SPI 进行了扩展)
接着看 ServiceLoader.load 方法:
```java
public static ServiceLoader load(Class service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类 LazyIterator 中:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
throw new Error(); // This cannot happen
}
思考:发生如下情况的原因是什么?
package com.sqx.jvm;
public class JavaRunTimeTest01 {
public static void main(String[] args) {
for (int i = 1; i <= 200 ; i++) {
long start = System.nanoTime() ; //开始时间
for (int j = 0; j < 1000 ; j++) {
new Object() ;
}
long end = System.nanoTime() ;
System.out.printf("%d\t%d\n",i,(end - start));
}
}
}
1 38300
2 40700
3 24800 #解释器开始执行
4 32300
5 36200
6 39600
7 90500
....
130 11200
131 12200
132 12000
133 12400 #C1即时编译器执行,优化3倍数
134 12000
135 62600
136 27500
.....
195 500
196 600
197 600
198 500 #C2即时编译器执行,优化8倍数
199 500
200 600
答案:由于我们的JVM的运行期的分层编译,进行优化!
第一阶段:我们的解释器拿到这部分代码的字节码,将其解释为cpu能识别的机器码执行
第二阶段:发现我们的这部分代码一直复用,将解释器改为编译器,直接拿缓存的机器码直接创建,不再解释!
第三阶段:发现并没有对象只在这一个栈中,没有发生逃逸,进行优化,就干脆直接不创建了,节省堆内存!
JVM 将执行状态分成了 5 个层次:
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等
即时编译器(JIT)与解释器的区别 :
解释器
即时编译器:
将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
根据平台类型,生成平台特定的机器码
对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由 来),并优化这些热点代码。
逃逸分析
逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术
逃逸分析的 JVM 参数如下:
逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数
对象逃逸状态
1、全局逃逸(GlobalEscape)
2、参数逃逸(ArgEscape)
3、没有逃逸
逃逸分析优化
针对上面第三点,当一个对象没有逃逸时,可以得到以下几个虚拟机的优化
锁消除
消除
我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁
例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作锁消除的 JVM 参数如下:
锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上
标量替换
首先要明白标量和聚合量,基础类型和对象的引用可以理解为标量,它们不能被进一步分解。而能被进一步分解的量就是聚合量,比如:对象
对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替换。
这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能
标量替换的 JVM 参数如下:
标量替换同样在 JDK8 中都是默认开启的,并且都要建立在逃逸分析的基础上
栈上分配
当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能
思考:发生如下结果的原因是什么?
public class JavaRuntimeTest02 {
public static void main(String[] args) {
int x = 0 ;
for (int i = 0; i < 500 ; i++) {
long start = System.nanoTime() ;
for (int j = 0; j < 1000 ; j++) {
x = square(9) ;
}
long end = System.nanoTime() ;
System.out.printf("%d\t%d\t%d\n",i,x,(end-start));
}
}
0 81 41000
1 81 21300
2 81 13800
3 81 16900
4 81 17100
5 81 19600
........
81 81 3300
82 81 4900
83 81 3900
84 81 3400
85 81 5800
86 81 3900
......
493 81 0
494 81 0
495 81 0
496 81 0
497 81 100
498 81 0
499 81 0
答案:发生了方法内敛,由于我们的方法传递的参数是被final修饰的,且发现我们的结果每次都一样,所以我们的将其看作是常数不再调用方法!
内联函数
内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换
JVM内联函数
C++ 是否为内联函数由自己决定,Java 由编译器决定。Java 不支持直接声明为内联函数的,如果想让他内联,你只能够向编译器提出请求: 关键字 final 修饰 用来指明那个函数是希望被 JVM 内联的,如
public final void doSomething() {
// to do something
}
总的来说,一般的函数都不会被当做内联函数,只有声明了final后,编译器才会考虑是不是要把你的函数变成内联函数
JVM内建有许多运行时优化。首先短方法更利于JVM推断。流程更明显,作用域更短,副作用也更明显。如果是长方法JVM可能直接就跪了。
第二个原因则更重要:方法内联
如果JVM监测到一些小方法被频繁的执行,它会把方法的调用替换成方法体本身,如:
private int add4(int x1, int x2, int x3, int x4) {
//这里调用了add2方法
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;
}
public class ReflectTest {
public static void main(String[] args) throws Exception {
Method foo = ReflectTest.class.getMethod("foo");
for (int i = 0; i <= 16 ; i++) {
System.out.printf("%d\t",i);
foo.invoke(null);
}
System.in.read() ;
}
public static void foo(){
System.out.println("foo.....");
}
}
foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现
invoke 方法源码
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
//MethodAccessor是一个接口,有3个实现类,其中有一个是抽象类
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
快捷键:ctrl + alt + b 查看接口的实现类!!!
会由 DelegatingMehodAccessorImpl 去调用 NativeMethodAccessorImpl
NativeMethodAccessorImpl 源码
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private final Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method var1) {
this.method = var1;
}
//每次进行反射调用,会让numInvocation与ReflectionFactory.inflationThreshold的值(15)进行比较,并使使得numInvocation的值加一
//如果numInvocation>ReflectionFactory.inflationThreshold,则会调用本地方法invoke0方法
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
this.parent.setDelegate(var3);
}
return invoke0(this.method, var1, var2);
}
void setParent(DelegatingMethodAccessorImpl var1) {
this.parent = var1;
}
private static native Object invoke0(Method var0, Object var1, Object[] var2);
}
//ReflectionFactory.inflationThreshold()方法的返回值
private static int inflationThreshold = 15;
阿里开源工具:arthas-boot