类的加载是在运行过程中动态加载的,而非编译器一次加载,一次加载会占用很多的内存。
一、类的生命周期
1.加载:
这个是类加载的一个阶段
主要任务:①根据类的全限定名将类转化为二进制字节流
②将二进制流的静态存储结构转化为方法区的运行时存储结构(运行时常量池)
③在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据访问的接口
二进制字节流的加载方式:
从Jar,war,zip等压缩包中读取
从网络中读取,最典型的为Applet
从运算获得,例如使用动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
由其他文件生成,例如JSP生成对应的Class类
2.验证
验证加载过程中生成的二进制字节流是否符合虚拟机的要求,保证其不会对虚拟机造成危害。
3.准备
为类变量分配空间(static修饰的变量),并设置初始值,使用的是方法区的内存。
实例变量在这个阶段不进行初始化,static修饰的类变量只赋予初值,例如由如下代码
static int a = 1111;
类加载在所有实例化操作之前,且类加载只进行一次,a此时被赋为处置0。
需要注意的时,若时final static修饰的常量,则类加载过程一步到位!
final static int a = 1111; //a=1111
4.解析
将运行时常量池的符号引用转换为直接引用。
符号引用理解为一个标识,而直接引用则是指向常量池中对象的地址。
5.初始化
需要初始化的两种情况:
主动引用
①在关键字new(创建新的实例),putstatic,getstatic,(存取静态字段)invokestatic(执行静态方法)后需要对类进行初始化
②当子类进行初始化时,父类没有进行初始化,则会对父类进行初始化
③当用到java.lang.reflect.*进行反射调用的时候,如果类没有进行初始化,则需要初始化
④jvm启动时,需要初始化一个main主类
⑤当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
被动引用
①子类调用父类的静态变量,不会使子类初始化(可能和clinit的顺序有关吧,在clinit()中要先执行父类的静态变量和静态代码块的执行,所以应该到父类之后就不会初始化子类了)
②调用常量的时候,因为常量是在加载阶段已经存入到常量池的,所以直接去常量池引用,不进行初始化操作。
③开辟数组,比如A[] a = new A [10],不会对A进行初始化操作。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法
初始化时虚拟机执行类的构造方法中的clinit()方法
准备过程已为类变量赋最初值,在初始化阶段主要时对其进行进一步的赋值,如上述代码从0变为111。
clinit()是由编译器自动收集类中所有的变量赋值动作和静态语句的代码块执行(就是static的变量赋值,static的静态代码块执行),需要注意的是,若静态变量定义在静态代码块后,则静态代码块只能赋值,不能进行输出或者运算的动作。
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
clinit()方法不是必须的,若一个类或接口中无静态变量或者无类变量的赋值操作,编译器可以不生成clinit()。
接口中clinit()方法,不用先调用父类接口的clinit()方法,只有在父类的接口中的变量被使用的时候,才进行初始化,同样,接口实现类在不适用接口中定义的变量时,也不会调用接口的clinit()方法。
虚拟机可以保证不同线程执行同一个类的clinit()方法时是线程安全的。多个线程同时初始化一个类,则这个类的clinit()方法只能被一个线程所执行,其他线程进入阻塞状态。
二、类于类加载器
两个类相等需要类本身相等,并由同一个类加载器加载,每一个类加载器都有独立的类名称空间。
这里的相等,包括类对象的equals()方法,isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
三、类加载器分类
1.从jvm来看
启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;
所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
2.从开发者角度
启动类加载器(Bootstrap ClassLoader)
主要加载jvm自身需要的类,将存放在<JAVA_HOME>\lib目录下的,或者被-Xbootclasspath参数指定的路径下的可以被虚拟机识别的类库加载到虚拟机。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
扩展类加载器(Extension ClassLoader)
展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader
类,它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
应用程序加载类(Applicantion ClassLoader)
这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它是ClassLoader的getSystemClassLoader()的返回值,因此称为系统类加载器。它加载用户定义的ClassPath下的指定的库,一般情况下该类加载是程序中默认的类加载器
在实际的使用过程中,通常是三种类加载器配合使用,而且还可以使用自定义的类加载器,类加载的过程是按需加载,即当需要使用时,才将class文件加载到内存,生成class对象,同时,按照双亲委派的方式加载,先找父类。
四、双亲委派机制
1.过程
如上图所示,一个类加载器收到加载请求,则先将请求向上传递给父类加载器,然后再依次向上传递,若父类加载器无法加载,则返回给子类进行加载。(过程理解为坑爹)
2.优点
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一,通过这种优先级关系避免重复加载
安全性提升,当一个外来的,比如网络传递过来的一个类,通过双亲委派的方式,通过已加载的类型来进行加载,避免了对系统核心api库的篡改。假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class