Java虚拟机的类加载机制知识点记录

博客:虚拟机的类加载机制:

https://blog.csdn.net/ns_code/article/details/17881581

类的初始化的时机:

http://blog.csdn.net/ns_code/article/details/17845821

1.209页虚拟机将Class文件加载到内存,并对数据进行校验,转换解析和初始化,最后形成能被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

Java语言中,类型的加载,连接初始化都是在程序运行期间完成

2.209页语言上的约定:每个Class文件都有可能代表Java语言中的一个类或接口,后文中直接对类的描述都包括了类和接口的可能性。

3.类被加载到虚拟机内存开始到卸载为止,类的整个生命周期包括:加载,验证,准备,解析,初始化,使用和卸载7个阶段。

其中验证,准备,解析3个部分统称为连接

连接阶段并不会等加载完全完成后才开始,而是交叉进行,可能一个类只加载了一部分后 ,连接阶段就已经开始了,自己的理解是为了提高速度

4.类加载的时机:没有明确定义类加载的时机,但是有一个原则,就是JVM预期到一个类将被使用的时候,就会在使用它之前对这个类进行加载。HotSpot虚拟机在用到一个类的时候,才会对它加载。在初始化阶段前,要完成加载,验证,准备5种情况必须立即对类进行初始化

1)使用new 关键字实例化一个对象的时候,读取或设置一个类的静态字段(被final修饰)的时候,以及调用一个类的静态方法的时候。自己的理解:在初始化的时候要对类的静态字段进行设值,所以读取或设置一个类的静态字段的时候必须保证立即对类进行初始化,调用一个类的静态方法的时候,类的静态方法在解析的时候会从符号引用解析为直接引用,为其分配内存,所以在想要调用一个类的静态方法,必须保证这个类已经被解析了,所以必须立即对其初始化

2)使用java.lang.refect包对类进行反射调用的时候

3)当初始化一个类的时候,发现其父类还没有进行初始化,则先要初始化父类

4)当虚拟机启动时,包含main()方法的那个类,虚拟机会先初始化这个类

5)看不懂这个

这5中场景中的行为称为对一个类的主动引用,除此以外,所有引用类大的方式都不会触发初始化,称为被动引用。

说明:被动引用的例子:

1)课本212页对于第一条读取一个类的静态字段,只有直接定义这个字段的类才会被初始化

2)通过数组定义来引用类,不会触发此类的初始化。

3)213页常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类

课本214页:类加载的过程:

加载:1)通过一个类的全定限名来获取此类的二进制字节流

          2)将这个字节流所代表的静态存储结构转换为方法区的运行是数据结构

          3)内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类数据的个中数据的访问入口

214页:加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中,方法区的数据存储格式由虚拟机实现自行定义(也就是不同的虚拟机的存储格式是不同的),然后在内存中实例化一个java.lang.Class对象(HotSpot中这个对象放在方法区中),这个对象作为程序方法访问方法区中这些数据的外部入口。

这个Class对象就是反射对象——它就像一面镜子,指向方法区运行的数据,如果我们要访问方法区的运行数据,只能通过这个Class对象。

216页:验证——确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

217页:准备阶段:正式为类变量分配内存并设置类变量初始值的阶段。第一:类变量指的是被static修饰的变量,而实例变量会在对象初始化时对着对象一起分配在Java堆中。第二:这时候的初始值为0.第三:常量在编译阶段就会存入调用类的常量池中——看213页的常量在在被引用后并没有被初始化

准备阶段就是为类的静态变量分配内存并设为JVM默认的初值(非静态变量,例如成员变量,现在类),例如 : static int a = 3,在准备阶段只会赋值为a=0,a=3会在初始化的时候赋值为3。

4.课本168页:常量池存放两大类常量:字面量和符号引用

字面量:类似于Java语言层面的常量的概念,如文本字符串、声明为final的常量值等。

符号引用:类和接口的全定限名   字段的名称和描述符    方法的名称和描述符

解释:关于字段和全定限名的解释:

说明:什么时字段,字段通常叫做"类成员",或"类成员变量",而Java中的属性,同行藏可以理解为get和set方法。

详细参考博客:https://blog.csdn.net/chenchunlin526/article/details/71424844

关于全定限名的解释:

在常量池中, 一个类型的名字并不是我们在源文件中看到的那样, 也不是我们在源文件中使用的包名加类名的形式。 源文件中的全限定名和class文件中的全限定名不是相同的概念。 源文件中的全新定名是包名加类名, 包名的各个部分之间,包名和类名之间, 使用点号分割。 如Object类, 在源文件中的全限定名是java.lang.Object 。 而class文件中的全限定名是将点号替换成“/” 。 例如, Object类在class文件中的全限定名是 java/lang/Object 。 如果读者之前没有接触过class文件格式, 是class文件格式的初学者, 在这里不必知道全限定名在class文件中是如何使用的, 只需要知道, 源文件中一个类的名字, 在class文件中是用全限定名表述的

220页:解析是虚拟机将常量池的符号引用替换为直接引用的过程。首先符号引用的概念:以一组符号来描述所引用的目标

直接引用:可以直接指向目标的指针,句柄等。

举例类解析:课本221页过程:首先课本168页常量池方两大类常量:字面量和符号引用,符号引用包括了类和接口的全限定名,解析的时候:当前代码处于类为D,将符号引用解析为类C的直接引用,虚拟机将N的全限定名传递给D的类加载器去加载这个类C,如果没有任何异常,那么C在虚拟机中成为一个有效的类。解析完成之前还要进行符号引用验证,确认D是否具备C的访问权限。

解析的通俗理解:把常量池的符号引用转换为直接引用。比如我们要在内存中找一个类里面叫做show的方法,显然找不到,但是在解析阶段,jvm就会把show这个名字转换为指向方法区的一块内存地址,比如0x12245,通过这个地址就可以找到show这个方法具体分配在内存的哪块区域了。这个show就是符号引用,而0x12245就是直接引用。在解析阶段,jvm将所有类名、接口名、字段名、方法名转换为具体的内存地址。

各种方法被解析的时间:

课本220页:解析:虚拟机将常量池(Class常量池)内的符号引用替换为直接引用的过程。虚拟机规范中没有规定解析阶段发生的具体时间,所以虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

class Test{
	public void f1(){        //普通方法
		
	}
	public static void f2(){    //静态方法
		
	}
}

对于方法f1(),当类的字节码文件加载到内存中时,类的实例方法并没有被解析(符号引用没有转为直接引用,即类的实例方法没有分配入口地址),只有当该类对象创建后,实例方法才分配了入口地址。还有一点注意,创建第一个类的对象时,实例方法的会被解析(分配了入口地址),当后续在创建对象时,不会再分配新的入口地址,可以说,该类的所有对象共享实例方法的入口地址。

而对于方法f2(),它在类加载的时候就会把符号引用解析为直接引用,静态方法的入口地址就会分配完成,所以静态方法不仅可以被该类对象调用,也可以直接通过类名完成调用。。深入理解Java虚拟机245页:非虚方法类加载时就会解析

1). 在类方法中不能引用实例变量

实例变量的定义类似实例方法,没有用static修饰的变量,实例变量的创建与实例方法的创建相同,也是在类的对象创建时完成,所以在类方法中是不能引用实例变量的,因为这个时候实例变量还没有分配内存地址。

2). 在类方法中不能使用super和this关键字

这是因为super和this都指向的是父类和本类的对象,而在类方法中调用的时候,这些指代的对象有可能都还没有创建。

3). 类方法中不能调用实例方法

原因同1。

与类方法相比,实例方法的定义就没有什么限制了:

1). 实例方法可以引用类变量和实例变量

2). 实例方法可以使用super和this关键字

3). 实例方法中可以调用类方法

以上参考:https://blog.csdn.net/mhady/article/details/51377579#commentsedit

课本225页:

初始化,深入理解Java虚拟机课本225页:类初始化是类加载的过程的最后一个阶段,到初始阶段,才开始真正的执行类中的Java程序代码,在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。从另一个角度来看:初始化阶段是执行构造器<clinit>()方法的过程<clinit>()方法是由编译器自动收集类中的所有类变量(静态变量)的赋值动作和静态语句块中的语句合并产生的

自己的理解:初始化的时候会执行<clinit>()方法。初始化并不一定会调用构造方法,new一个对象会触发初始化,同时调用构造方法,但是调用类的静态变量会触发初始化,并不会调用构造方法。理解构造方法和类初始化之间。

//课本226页
class DeadLoopClass{
	static
	{
		if(true){
			System.out.println(Thread.currentThread()+"init DeadLoopClass");
			while(true){
				
			}
		}
	}
}

class TestGe{
	public static void main(String[] args){
		Runnable script = new Runnable(){
			public void run(){
				System.out.println(Thread.currentThread()+"start");
				DeadLoopClass dlc = new DeadLoopClass();
				System.out.println(Thread.currentThread()+"end");
			}
		};
		
		Thread thread1 = new Thread(script);
		Thread thread2 = new Thread(script);
		thread1.start();
		thread2.start();
	}
}

将类DeadLoopClass类中的static去掉就不会导致阻塞,而不去掉就会导致阻塞,两点原因:1)new会触发初始化,初始化会执行static同步代码块2)第二点有static,会调用<clinit>,<clinit>是线程安全的,一个线程没有执行完成,不会允许别的线程执行

示例代码:改代码重现了触发初始化动作的五种情况,但是注意没有获取类的静态方法和静态变量会触发类的初始化,但是并不会调用构造方法。(其实也很好理解,我要调用静态变量的,肯定要先触发初始化对静态变量赋值

class InitClass{  
    static {  
        System.out.println("初始化InitClass");  
    }  
    
    InitClass(){
    	System.out.println("执行了没有啊");
    }
    
    public static String a = null;  
    public static void method(){}  
}  
  
class SubInitClass extends InitClass{}  
  
public class Test {  
  
    /** 
     * 主动引用引起类的初始化的第四种情况就是运行Test1的main方法时 
     * 导致Test1初始化,这一点很好理解,就不特别演示了。 
     * 本代码演示了前三种情况,以下代码都会引起InitClass的初始化, 
     * 但由于初始化只会进行一次,运行时请将注解去掉,依次运行查看结果。 
     * @param args 
     * @throws Exception 
     */  
    public static void main(String[] args) throws Exception{  
    //  主动引用引起类的初始化一: new对象、读取或设置类的静态变量、调用类的静态方法。  
    //	new InitClass();  		//使用new关键字
    //  InitClass.a = "";  		//设置类的静态字段
    //  String a = InitClass.a; //读取类的静态字段
      InitClass.method();  	//调用类的静态方法
          
    //  主动引用引起类的初始化二:通过反射实例化对象、读取或设置类的静态变量、调用类的静态方法。  
    // Class cls = InitClass.class;  
    //  cls.newInstance();  
          
    //  Field f = cls.getDeclaredField("a");  
    //  f.get(null);  
    //  f.set(null, "s");  
      
    //  Method md = cls.getDeclaredMethod("method");  
    //  md.invoke(null, null);  
              
    //  主动引用引起类的初始化三:实例化子类,引起父类初始化。  
    //  new SubInitClass();  		//初始化一个类的时候,如果发现其父类还没有进行初始化,则先触发父类的初始化
  
    }  
}

227页:类加载器:通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到Java虚拟机外部实现,以便让应用程序自己决定如果去获取所需要的类。

课本168页:Java代码在编译的时候,没有连接的步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说Class文件不会保存各个方法、字段的最终布局信息,而是当虚拟机运行时,需要从Class常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中


详解Java类的生命周期博客地址:https://blog.csdn.net/zhengzhb/article/details/7517213

猜你喜欢

转载自blog.csdn.net/chenkaibsw/article/details/80248469
今日推荐