从今天开始打卡学习JVM,第一天
本人学习过程中所整理的代码,源码地址
- 类加载
在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的
- 加载:查找并加载类的二进制数据,具体指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个Class对象用来封装类在方法区内的数据结构
- 连接:验证 -> 类被加载后,就进入连接阶段,就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去,确保被加载的类的正确性;准备 -> 为类的静态变量分配内存,并将其初始化为默认值;解析 -> 把类中的符号引用转换为直接引用
- 初始化:为类的静态变量赋予正确的初始值;当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,接口不适用,只有当程序首次使用特点接口的静态变量时,才会导致该接口的初始化
- 类实例化:为新的对象分配内存,为实例变量赋默认值,为实例变量赋正确的初始值,java编译器为它的每一个类都至少生成一个实例初始化方法,被称为<init>,针对源代码中每一个类的额构造方法,java编译器都产生一个<init>方法
- 垃圾回收和对象终结
-XX:+TraceClassLoading:用于追踪类的加载信息并在控制台打印出来
-XX:+(option):表示开启option选择
-XX:-(option):表示关闭option选择
-XX:(option)=(value):表示将option选项的值设置为value
//对于静态字段来说,只有直接定义了该字段的类才会被初始化;
//当一个类在初始化时,会先初始化其父类
public class MyTest {
public static void main(String[] args) {
System.out.println(MyChild1.str2);
}
}
class MyParent1 {
public static String str = "hello world";
static {
System.out.println("MyParent1 static block");
}
}
class MyChild1 extends MyParent1 {
public static String str2 = "welcome";
static {
System.out.println("MyChild1 static block");
}
}
/*MyParent1 static block
MyChild1 static block
welcome*/
助记符
ldc:表示将int,float或者String类型的常量值从常量池中推送至栈顶
bipush:表示将单字节[-128-127]的常量值推送至栈顶
sipush:表示将一个短整型常量值[-32768-32767]推送至栈顶
iconst_n:表示将int类型n[-1 5]推送至栈顶
//常量在编译阶段会存入到调用这个常量的方法所在类的常量池中,本质上,调用类并没有直接引用到定义变量的类,因此并不会触发定义常量的类的初始化
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str);
}
}
class MyParent2{
//被保存到了MyTest2的常量池中,之后MyTets2与MyParent2就没有任何关系了,甚至可以将MyParent2的class文件删除
public static final String str = "hello world";
static {
System.out.println("MyParent2 static block");
}
}
//hello world
//当一个常量的值在编译期间无法确定,那么其值就无法被放到调用类的常量池中,此时程序运行时,会导致主动使用这个常量所在的类,因此这个类将会被初始化
public class MyTest3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3{
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("MyParent3 static code");
}
}
/*MyParent3 static code
df50b36d-15a8-4b9a-8aa5-18de0f3a0a92*/
助记符
anewarray:表示创建一个引用类型的(如类、接口、数组)
数组,并将其引用值压入栈顶
newarray:表示创建一个基本类型的数组,并将其引用值压入栈顶
//对于数组实例来说,其类型是由JVM在运行期动态生成的,表示为class [Lcom.cqupt.jvm.classloader.MyParent4这种形式。动态生成的类型其父类型也是Object。对于数组来说,JavaDoc经常将构成数组的元素为Component,实际上就是将数组降第一个维度后的类型
public class MyTest4 {
public static void main(String[] args) {
//会初始化类
MyParent4 parent4 = new MyParent4();
//不会初始化
MyParent4[] myParent4 = new MyParent4[1];
MyParent4[][] myParent44 = new MyParent4[1][1];
System.out.println(myParent4.getClass());
System.out.println(myParent44.getClass());
System.out.println(myParent4.getClass().getSuperclass());
System.out.println(myParent44.getClass().getSuperclass());
}
}
class MyParent4 {
static {
System.out.println("MyParent4 static block");
}
}
这里楼主试了好多次,删除父接口的class文件还是不会报错,父接口难道没有被加载吗?
//当一个接口在初始化时,并不要求其父接口都完成初始化,只有在真正使用到父接口的时候(如引用接口中所定义的常量时)才会初始化
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.b);
}
}
interface MyParent5 {
public static int a = 5;
}
interface MyChild5 extends MyParent5 {
public static final int b = new Random().nextInt(3);
}
//6
下面这段代码中为什么counter2会输出0?
因为在类加载过程中的准备阶段时所有静态变量都被赋予了默认值,所以在构造函数运行完后counter2等于1,但由于初始化是按照代码的顺序执行的,因此counter2的值又会被显示式赋予的0所覆盖掉,因此最终counter2的值为0;若将counter1一开始就显示赋为1,最终counter1的输出结果为2,进一步验证了初始化的顺序问题
public class MyTest6 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("counter1:" + Singleton.counter1);
System.out.println("counter2:" + Singleton.counter2);
}
}
class Singleton {
public static int counter1;
private static Singleton singleton = new Singleton();
private Singleton() {
//准备阶段的重要意义
counter1++;
counter2++;
System.out.println(counter1);
System.out.println(counter2);
}
public static int counter2 = 0;
public static Singleton getInstance() {
return singleton;
}
}
//1 1 counter1:1 counter2:0
- Java虚拟机结束生命周期的方式
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程中止
总结:
通过以上实例主要是为了验证类加载过程中的各种问题,理解子类父类的初始化问题,理解类加载过程中的准备和初始化阶段的重要意义。