类加载


在class文件中的描述信息都需要加载到jvm才能运行和使用。


 


jvm的类加载机制:jvm把描述类的数据从class文件中加载到内存,并对数据进行校验,转换解析和初始化,最终形成被jvm使用的Java类型。


 


生命周期:加载-》验证-》准备-》解析-》初始化-》使用-》卸载


 


加载到初始化都是在程序的与运行期间完成的。验证,准备,解析也叫连接过程,Java的特性是依赖在运行期动态加载和动态连接。


 


Java类加载 会初始化的情况有且仅有以下五种:(也称为主动引用)


 


1.遇到new(用new实例对象),getStatic(读取一个静态字段),putstatic(设置一个静态字段),invokeStatic(调用一个类的静态方法)这四条指令字节码命令时


 


2.使用Java.lang.reflect包的方法对类进行反射调用时,如果此时类没有进行init,会先init。


 


3.当初始化一个类时,如果其父类没有进行初始化,先初始化父类


 


4.jvm启动时,用户需要指定一个执行的主类(包含main的类)虚拟机会先执行这个类


 


5.当使用JDK1.7的动态语言支持的时候,当java.lang.invoke.MethodHandler实例后的结果是REF-getStatic/REF_putstatic/REF_invokeStatic的句柄,并且这些句柄对应的类没初始化的话应该首先初始。


 


注意:除以上5种方法外,所有引用类的方法都不会触发初始化,称为被动引用。


 


被动引用的例子:


1.通过子类来引用父类的静态字段,只会触发父类的初始化,不会触发子类的初始化。


2.superclass () sc = new superclass[];//不会触发superclass初始化,因为底层实现是直接生成object子类。


3.引用一个类的静态常量也不会触发初始化,因为常量在编译阶段已经确认。


 


=====》接口也会有初始化的过程,但接口中不能有static块,但编译器也会为接口生成<clinit>类构造器,用于初始化接口中成员变量,接口子类的不同仅是上边3的不同,因为接口不要求父接口全部实现,而是用到哪些实现哪些。


 

类加载的五个过程 


1.加载:   


               1、通过类的全限定名来获取类的二进制字节流(用户可操作,自定义类加载器(实现通过一个类的全限定名获取类的二进制字节流的动作放在jvm外部实现的模块))


               2、将这个字节流所代表的静态存储结构转化为在方法区的运行时数据结构。


               3、在内存中生成代表这个类的Java.lang.class类对象,作为这个数据访问的入口。


              注:数组类本身不是通过类加载器创建,而是有jvm直接创建


2.验证:  防止危害jvm安全,目的是确保class文件中字节流中包含的信息符合当前虚拟机的要求。主要有四个方面的验证:


             1、文件格式验证,是否以魔数开始,版本信息是否为jvm接受,常量池中是否有不支持的类型。


             2、元数据验证:进行语法分析,是否每个类都有父类,是否有语法错误,是否继承自final。


             3、字节码验证:语义分析,通过数据流和控制流分析,确保语义是合法的。


             4、符号引用验证:是否能找到对应的类。发生在讲符号引用转化为直接引用时,在解析中产生。


 


3.准备:正式为类变量分配内存,在方法区中分配


           注意:static+ final修饰的变量在准备阶段之后就是用户指定的值。


4.解析:将符号引用转化为直接引用(可选择)包括类,接口, 字段,方法的解析。


5.初始化:真正执行Java程序中的代码(字节码),是执行类构造器的过程,对类的静态变量和代码块执行初始化工作。


            1、<clinit>()方法是由于编译器自动收集类中所有类变量赋值,静态语句块合并产生的,顺序是语句在源文件中的顺序。


            2、<clinit>方法与类的构造器不同,他不需要显示的调用父类的构造方法,因为jvm会保证在子类的clinit方法执行之前,


父类已经执行了。所以jvm执行的clinit一定是object类。


            3、如果一个类或者接口中没有静态语句或者静态块,则可以没有clinit方法。


            4、jvm会保证类的clinit方法加锁。 


     注意:静态语句块中只能访问定义在静态语句之前的变量,不能访问语句之后的,但可以为后边的变量赋值。 




eg: class Test{
static{ i = 4; //正常
System.out.println(i)//报错,因为不能访问,只能赋值
}
static int i = 2;
}


底层实现: 


也可以参考:​​Java 类加载机制(阿里面试题)-何时初始化类​


 


类装载工作是有ClassLoader及其子类负责的,ClassLoader是一个运行时组件,它负责在运行时查找和装入Class字节码文件。


jvm有三个ClassLoader:依次是: 


 


1、启动类加载器, 根装载器,用户不可访问,Bootstrap ClassLoader,加载JAVA_HOME\lib,或者被-Xbootclasspath参数限定的类

2、扩展类加载器,Extension ClassLoader,加载\lib\ext,或者被java.ext.dirs系统变量指定的类

3、应用程序类加载器,Application ClassLoader,加载ClassPath中的类库

4、自定义类加载器,通过继承ClassLoader实现,一般是加载我们的自定义类 


 


jvm使用“”全盘负责委托机制”来装载:是指装载是一定是下通过其父类去找目标类,找不到才去找子类,这是出于安全的考虑,如果反过来,我们每个人都可以随意修改API了,不安全。


String 类就是这样的。


 


ClassLoader 是Java.lang下的一个抽象方法,


               1.Class loadClass(String name);name必须是参数的全限定名,


               2.Class defineClass(String name,bytr[],int off,int len);


               3.Class findLoadedClass(String name),查看ClassLoader是否已经装入某个类,装了返回class。,没有装返回null.


               


注意:每一个类在jvm中都拥有一个Java.lang.class对象,是对象在装载时jvm通过调用类加载器中的defineClass()方法自动构造的。


 


Java反射机制:


  Class反射对象描述的是类的语义结构,通过class对象,可以获取构造器,成员变量,方法等类元素的反射对象,并且可以用编程的方法通过这些反射对象对目标对象进行操作。


这些反射类在java.lang.reflect包中定义,下面是最主要的三个类:


               1.Constructor:类的构造函数反射类:通过Class#getConstructors()方法可以获得类的所有构造函数的反射对象数组。其中最主要的方法是newInstance(Object[] args),通过该方法可以创建一个对象类的实例,功能和new一样。在jdk5.0之后,提供了newInstance(Object...args)更为灵活。 


               2.Method:类方法的反射类。通过Class#getDeclaredMethods()方法可以获取所有方法的反射类对象数组Method[].其中最主要的方法是


               invoke(String name,class parameterTypes),和invoke(Object obj,Object...args)。同时也还有很多其他方法:


                         A:Class getReturnType():获取方法的返回值类型


                         B:Class[] getParameterTypes():获取方法的参数数组


               3.Field:类成员变量的反射类,通过Class#getDeclareFields()可以获取类成员变量反射的数组。


                         Class#getDeclareField(String  name)获取某特定名称的反射对象。


                         最主要的方法是:set(Object obj,Object value),为目标对象的成员变量赋值。如果是基础类型还可以这样赋值


                              setInt(),setString()................


java还提供了包的反射类和注解的反射类。。


 


总结:java反射体系保证了通过程序化的方式访问目标对象的所有元素,对于private 和protected成员变量或者方法,也是可以访问的。


  主要就是取消访问检查




Field c = cls.getDeclaredField("a");
c.setAccessible(true);//这里如果不设置的话,会抛出IllegalAccessException()异常
c.set(car, 888);
...


下面是代码:




package com.ioc;  

public class Car {
private String brant;
private String color;
private int maxSpeed;
private int a;
public String getBrant() {
return brant;
}
public void setBrant(String brant) {
this.brant = brant;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getMaxSpeed() {
return maxSpeed;
}
public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}

public Car(){}
public Car(String brant,String color,int max) {
this.brant = brant;
this.color = color;
this.maxSpeed = max;

}

public String toString(){
String a = brant+color+maxSpeed;
return a;
}

public void print(){
System.out.println(brant+color+maxSpeed+a);
}


}


 反射调用的代码: 







package com.ioc;  

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import com.ioc.Car;
public class ReflectTest {
public static Car initByDefaultConst()throws Throwable{
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class cls = loader.loadClass("com.ioc.Car");

Constructor cons = cls.getDeclaredConstructor((Class[])null);
Car car = (Car) cons.newInstance();

Field c = cls.getDeclaredField("a");
c.setAccessible(true);
c.set(car, 888);
Method setBrand = cls.getMethod("setBrant",String.class);
setBrand.invoke(car, "aaaaa");
Method setColor = cls.getMethod("setColor",String.class);
setBrand.invoke(car, "red");
Method setMaxSpeed = cls.getMethod("setMaxSpeed",int.class);
setBrand.invoke(car, "11");

return car;
}
public static void main(String[] args) throws Throwable{
Car car = initByDefaultConst();
car.print();
System.out.println(car.toString());
}
}


 QA:在编程中我们常会遇到java.lang.NoSuchMethodError,的错误信息,为什么?


--------->>>>>其实就是由于全盘负责委托机制引发的,比如:既引入 了commons.lang3.x.jar又引入了,commons.lang.4.x.jar那就会抛出这个异常。


 验证一下: 

验证:

1)当类被初始化时,其静态代码块会执行。



class ClassLoadTime{

  static{

    System.out.println("ClassLoadTime类初始化时就会被执行!");

  }

  public ClassLoadTime(){

    System.out.println("ClassLoadTime构造函数!");

  }

}

class ClassLoadDemo{

  public static void main(String[] args){

    ClassLoadTime clt = new ClassLoadTime();

  }

}


 

输出结果:

 ClassLoadTime类初始化时就会被执行!

 ClassLoadTime构造函数!



 2) 读取一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)



class ClassLoadTime{

  static{

    System.out.println("ClassLoadTime类初始化时就会被执行!");

  }

  public static int max = 200; (防止测试类和此类不在一个包,使用public修饰符)

  public ClassLoadTime(){

    System.out.println("ClassLoadTime构造函数!");

  }

}

class ClassLoadDemo{

  public static void main(String[] args){

    int value = ClassLoadTime.max;

    System.out.println(value);

  }

}


 输出:

ClassLoadTime类初始化时就会被执行!

200



 3)设置一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)



class ClassLoadTime{

  static{

    System.out.println("ClassLoadTime类初始化时就会被执行!");

  }

  public static int max = 200; (防止测试类和此类不在一个包,使用public修饰符)

  public ClassLoadTime(){

    System.out.println("ClassLoadTime构造函数!");

  }

}

class ClassLoadDemo{

  public static void main(String[] args){

    ClassLoadTime.max = 100;

  }

}


 输出:

ClassLoadTime类初始化时就会被执行! 

4)调用一个类的静态方法



class ClassLoadTime{

  static{

    System.out.println("ClassLoadTime类初始化时就会被执行!");

  }

  public static int max = 200; (防止测试类和此类不在一个包,使用public修饰符)

  public ClassLoadTime(){

    System.out.println("ClassLoadTime构造函数!");

  }

  public static void method(){

    System.out.println("静态方法的调用!");

  }

}

class ClassLoadDemo{

  public static void main(String[] args){

    ClassLoadTime.method();

  }

}


 输出:

ClassLoadTime类初始化时就会被执行!

静态方法的调用!



 被final修饰静态字段在操作使用时,不会使类进行初始化,因为在编译期已经将此常量放在常量池。

测试:



class ClassLoadTime{

  static{

    System.out.println("ClassLoadTime类初始化时就会被执行!");

  }

  public static final int MIN = 10; (防止测试类和此类不在一个包,使用public修饰符)

}

class ClassLoadDemo{

  public static void main(String[] args){

   System.out.println(ClassLoadTime.MIN);

  }

}


 

输出:

10



 

子类调用或者设置父类的静态字段或者调用父类的静态方法时仅仅初始化父类,而不初始化子类。同样读取final修饰的常量不会进行类的初始化。



class Fu{

  public static int value = 20;
  static{
    System.out.println("父类进行了类的初始化!");
  }
}
class Zi{
  static{
    System.out.println("子类进行了类的初始化!");
  }
}
class LoadDemo{
  public static void main(String[] args){
    System.out.println(Zi.value); 
  }
}


 

输出:

父类进行了类的初始化!

20



 java类中各种成员的初始化时机,此处不一一测试:

类变量(静态变量)、实例变量(非静态变量)、静态代码块、非静态代码块 的初始化时机:

* 由 static 关键字修饰的(如:类变量[静态变量]、静态代码块)将在类被初始化创建实例对象之前被初始化,而且是按顺序从上到下依次被执行;



public static int value =34;

static{

  System.out.println("静态代码块!");

 }

 public 类名(){

  System.out.println("构造函数!");

 }


 

   一旦这样写,在类被初始化创建实例对象之前会先初始化静态字段value,然后执行静态代码块,当实例化对象时会执行构造方法中的代码

* 没有 static 关键字修饰的(如:实例变量[非静态变量]、非静态代码块)初始化实际上是会被提取到类的构造器中被执行的,但是会比类构造器中的

   代码块优先执行到,其也是按顺序从上到下依次被执行。



public int value =34;

{

  System.out.println("非静态代码块!");

 }

 public 类名(){

  System.out.println("构造函数!");

 }


 

在使用构造函数实例化一个对象时,会先初始化value,然后执行非静态代码块,最后执行构造方法里面的代码。

*在存在父类的时候,调用子类的构造时,会先调用父类的默认构造(空参构造),进行父类的初始化。