类的生命周期(类加载,类加载器,双亲委派机制)
类在Java内存中的变化
Java内存空间—堆,栈,方法区
Java内存空间需要了解的可以大致可分为三块区域:堆,栈,方法区。
- 堆: 存放new的对象和数组。
- 可以被所有的线程共享,不会存放别的对象引用。
- 如果堆中没有内存完成实例分配,并且堆也无法完成扩展时,将会抛出OutOfMemoryError异常。
- 堆可以处于物理上不连续的内存空间,只要逻辑上连续的即可。
- 栈: 存放基本类型变量(包含这个基本类型的具体数值),引用对象的变量(存放这个引用类型在堆里的具体地址)
- 方法区: 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 可以被所有线程共享
- 当方法区无法满足内存分配需要时,将抛出OutOfMemoryError异常。
类的生命周期
类的生命周期如下图所示:
我们需要了解的是类的加载,链接,初始化,缓存。
- 类的加载: 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
- 类的链接: 将java类的二进制代码合并到JVM的运行状态中的过程。
- 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。
- 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
- 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
- 类的初始化: 执行类构造器 <clinit>() 方法的过程。
- 类构造器<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
- 当初始化一个类时,如果其父类还没有进行初始化,则先触发其父类的初始化。
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
- 类缓存: 标准的javaSE类加载器可以按要求查找类,一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。但是JVM垃圾回收机制可以回收这些Class对象
类初始化的时机
类的主动引用会触发类的初始化,但是类的被动引用不会发生类的初始化。
- 主动引用
- 当虚拟机启动,会先初始化main方法所在的类
- new一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 使用Java.lang.reflect包的方法对类进行反射调用
- 初始化一个类时,其父类如果没有初始化,则会先初始化它的父类
- 被动引用
- 访问一个静态域时,只有真正声明这个域的类才会被初始化。如:通过子类引用父类的静态变量,不会导致子类初始化
- 通过数组定义类引用,不会触发此类的初始化
- 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
public class testClassInit {
static {
System.out.println("main类被加载");
}
public static void main(String[] args) throws ClassNotFoundException {
//主动引用
// Son son=new Son();
//通过反射主动引用
// Class.forName("com.yang.learning.AnnotationAndReflection.Son");
//不会产生类的引用的方法
// System.out.println(Son.b);
// Son[] array=new Son[5];
System.out.println(Son.M);
}
}
class Father{
static int b=2;
static {
System.out.println("父类被加载");
}
}
class Son extends Father{
static {
System.out.println("子类被加载");
m=300;
}
static int m=100;
static final int M=1;
}
类加载器
类加载器可以分为:启动/根类加载器(Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),应用程序类加载器(Application ClassLoader),自定义类加载器(User ClassLoader)。
为确保所有的类都被加载,Java中使用了一个双亲委派机制。即,如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
双亲委派机制分为向上委派和向下委派。
- 向上委派:第一步: 将自定义加载器挂载到应用程序类加载器 第二步: 应用程序类加载器将请求委托给扩展类加载器 第三步: 扩展类加载器将请求委托给启动类加载器。
- 向下委派:第一步: 启动类在加载路径下查找并加载Class文件,如果没有找到就交给扩展类加载器加载 第二步:扩展类加载器在它的加载路径下查找并加载Class文件,如果还是没有找到,再交给应用程序类加载器加载 第三步:应用程序类加载器在加载路径下查找并加载Class文件,如果还是没有找到,就交给自定义加载器进行加载 第四步:自定义加载器在用户指定的位置进行查找并加载Class文件,如果还是没有找到,JVM抛出ClassNotFund异常。这时类加载失败,JVM也启动失败。
public class testClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
//获取系统类的加载器
ClassLoader systemClassLoader=ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
//获取系统类的加载器的父类加载器--》扩展类加载器
ClassLoader parent=systemClassLoader.getParent();
System.out.println(parent);
//获取扩展类加载器的父类加载器--》根加载器(C/C++)
ClassLoader grandParent=parent.getParent();
System.out.println(grandParent);
//测试当前类是哪个类加载器加载的
ClassLoader classLoader=Class.forName("com.yang.learning.AnnotationAndReflection.testClassLoader").getClassLoader();
System.out.println(classLoader);
//测试JDK内置类是那个类加载器加载的
classLoader=Class.forName("java.lang.Object").getClassLoader();
System.out.println(classLoader);
//如何获得系统类加载器可以加载的路径
System.out.println(System.getProperty("java.class.path"));
}
}