编译期优化称为语法糖
java基本类型和包装类型之间的自动转换。
在字节码中可以看见,泛型擦除就是字节码中的执行代码不区分是String还是Integer了,统一用Object.
对于取出的Object,这个checkcast用于转换Object为Integer类型。
可以看见局部变量类型表里面包含了方法参数的泛型信息。
输出里拿到了两个参数的原始类型List和Map。
这里除了能拿到方法参数上的泛型信息,还能拿到返回值的泛型信息。
如果遍历的是List集合,生成代码如下
一个switch变成了两个switch,通过字符串的哈希码进行匹配。
这玩意的本质也是一个class,里面的两个值就是这个class的两个实例对象。
跟普通类的最大区别:普通类的实例个数是无穷,枚举类的实例个数有限。
转换后因为这个class不能再被继承,所以加上了final关键字。并继承了一个支持泛型的枚举父类
主要作用是简化资源的关闭,只要按照下面的语法在try后面括号内进行资源的创建就可以省略finally中的资源关闭。
编译器会帮助生成finally的代码。
编译器多加了一个真正的方法重写,在其内部调用了我们重写的m方法返回Integer。
该合成方法可以重名,只在jvm内部使用。
因为转换后的类已经变成有两个方法了,所以要加多一个参数接收传进来的x,并传给run方法。
匿名内部类引用外部局部变量必须是finall的原因。
类的生命周期有如下阶段
加载(Loading):在加载阶段,类的字节码被加载到JVM的内存中。这包括从文件系统、网络等位置加载类的字节码,并将其转换为JVM能够理解的数据结构。加载阶段的结果是在方法区(Method Area)创建一个代表该类的Class对象。
连接(Linking):连接阶段将加载的类与其他类和资源进行关联,以便正确地解析和执行。连接阶段可以进一步分为三个子阶段:
初始化(Initialization):初始化阶段是类生命周期中的关键阶段,它负责执行类的静态初始化代码块和静态变量的赋值。在该阶段,JVM确保静态变量按照预期值进行初始化,静态初始化块被执行,构造器也可以被调用,但仅限于初始化静态字段。初始化阶段标志着类已准备好被使用。
使用(Usage):在初始化阶段之后,类就可以被使用了。这包括通过创建对象、调用类的方法、访问静态变量等方式来使用类。
卸载(Unloading):如果某个类不再被引用,JVM可能会在某些情况下将其卸载,释放对应的内存和资源。类的卸载是一个可选的过程,通常由垃圾回收器决定。
需要注意的是,类的生命周期可能因为具体的JVM实现、加载方式、类加载器等因素而有所不同。上述阶段的顺序和细节可能会因JVM的版本和配置而有所变化。
从大方面来说分为加载,连接,和初始化阶段。
加载阶段Java虚拟机需要完成以下三件事:
java_mirror属性起到桥梁作用(c++数据结构和java的桥梁),java的对象,要想访问它的Klass信息不能直接访问,要通过java_mirror镜像访问。
常用的XXX.class不是指Klass,而是java_mirror。
如果该类有父类或接口没有加载,会先加载父类和接口,才会轮到这个类加载。
类的字节码被加载到元空间,构成instanceKlass数据结构。其中 _java_mirror是java的类镜像,它持有该类在堆内存中的地址,堆内存中的类对象也持有instanceKlass在本地内存中的地址。
用new关键字创建person的实例对象时,每个实例对象都有自己的对象头是16个字节,其中八个字节对应着对象的class地址。
如果要通过实例对象获取其class信息,会先访问对象头,然后通过class地址先找到堆中的类对象,再间接找到元空间的instanceKlass.
扩展:我们通过类模版创建对象后,此时对象头中有8个字节对应 类模版(class)在堆内存的地址,如果想调用普通方法,或者 get,set 方法就要通过对象头中类模版地址找到元空间中的instanceKlass,再通过instanceKlass获取 field,methods 信息。
instanceKlass(元空间中的类元信息): instanceKlass
是在HotSpot虚拟机的元空间中存储的一种数据结构,它表示已加载的类的元信息。这个数据结构包含了类的名称、修饰符、方法、字段、父类、接口等信息。在HotSpot的内部实现中,instanceKlass
是用于支持虚拟机的运行时类型信息和方法的查找调用等操作。它和Java中的Class对象类似,但是在内存结构上可能有所不同。
Class对象(堆中的类元信息): 在Java中,每个已加载的类都有一个与之对应的Class对象,用来表示类的元信息。Class对象存储了类的名称、修饰符、方法、字段、父类、接口等信息,也支持反射操作。这个Class对象是存在于堆内存中的,它是Java反射机制的核心。
虽然instanceKlass
和Class对象在某种程度上都用来表示类的元信息,但它们的实现和作用在内部有所区别。instanceKlass
主要在HotSpot虚拟机的内部实现中使用,而Class对象是Java编程中常用的反射机制的一部分。这两者的关系是,instanceKlass
是HotSpot虚拟机内部用于支持类的元数据管理的一部分,而Class对象则是供Java程序员在运行时获取和操作类信息的关键工具。
验证阶段确保类的字节码符合JVM规范,并且不会引发安全问题或错误。
随便修改一下编译后class文件就会验证报错。
在准备阶段,为类的静态变量分配内存并初始化为默认值,例如数值类型初始化为0,引用类型初始化为null。
静态变量跟类对象存储在一起,都在堆中。早期的jvm,静态变量跟着instanceKlass存储在方法区。
public class Example {
static int staticVariable = 42;
public static void main(String[] args) {
Example instance = new Example();
// 通过类名访问静态变量
System.out.println(Example.staticVariable); // 输出: 42
// 通过类对象访问静态变量
System.out.println(instance.staticVariable); // 输出: 42
}
}
在这个示例中,通过类对象 instance
访问了静态变量 staticVariable
,并且与通过类名直接访问的结果相同。
需要注意的是,尽管在JDK 8及之后版本中支持通过类对象访问静态变量,但这并不是很常见的用法,通常还是使用类名来访问静态变量更加清晰和符合约定。
通过上面代码可以知道,a只是分配了空间,没有赋值。b在准备阶段没有赋值,只是准备好了语句,还要等到初始化阶段,而c,d已经赋完值了。
解析阶段将常量池中的符号引用(如类、方法、字段的符号名称)解析为直接引用,以便在后续的执行阶段中能够准确访问相关内容。
默认情况下,类的加载都是懒惰式的,用到了类C,没有用到类D的话,类D不会主动加载。
上面代码准备了一个类加载器,调用了loadClass方法加载类C,但这只会,进行类C的加载,不会进行解析和初始化。
new C()会进行C的加载解析和初始化,并把D也给加载解析和初始化。
类D未解析的情况时
在HSDB中连接到该进程上,只能看见有类C的存在,类C的常量池中可以看见类D前面标注了常量_未经解析类,仅仅是个符号。
new C(),类D被解析时
现在C,D都有了,并且,类C的常量池里面也知道了类D的内存地址。
这就是一个符号引用,一个直接引用。解析阶段就是让符号引用变成直接引用。
初始化阶段是类生命周期中的关键阶段,它负责执行类的静态初始化代码块和静态变量的赋值。在该阶段,JVM确保静态变量按照预期值进行初始化,静态初始化块被执行,构造器也可以被调用,但仅限于初始化静态字段。初始化阶段标志着类已准备好被使用。
初始化即调用
加载阶段就已经生成了mirror_class(也就是类.class),所以不会触发静态代码块。数组存的也还是Minor对象,在加载时就创建了。
a,b都不会导致类的初始化,两个都是final的基本类型,在准备阶段就已经赋值了。
c会导致类的初始化,这是包装类型,底层会调用Integer.valueOf(20).
字节码的静态代码块中能看见有关C的操作,说明C是在初始化阶段进行。
单例模式就是保证在jvm中,这个类的对象只有一个。
静态内部类的好处是可以访问外部类的资源,构造方法。所以私有构造方法也可以调用。
只要不调getIntance(),就不会触发LazyHolder的加载连接和初始化。