上海监理建设协会网站,番禺网站制作技术,七牛云对象存储,wordpress 糗事百科类加载过程详解 类的生命周期 类从被加载到虚拟机内存到开始卸载出内存为止#xff0c;生命周期可以简单概括为7个阶段#xff1a;加载#xff08;Loading#xff09;、验证#xff08;Verification#xff09;、准备#xff08;Preparation#xff09;、解析#xff…类加载过程详解 类的生命周期 类从被加载到虚拟机内存到开始卸载出内存为止生命周期可以简单概括为7个阶段加载Loading、验证Verification、准备Preparation、解析Resolution、初始化Initialization、使用Using和卸载Unloading。其中前三个阶段可以统称为连接Linking。
类加载过程
类加载过程描述的是类的生命周期从加载到初始化的阶段。 加载 通过全类名获取定义此类的二进制字节流。将字节流所代表的静态存储结构转换为方法区的运行时数据结构。在内存中生成一个代表该类的 Class 对象作为方法区这些数据的访问入口。 加载这一步主要是通过 类加载器 完成的。类加载器有很多种当我们想要加载一个类的时候具体是哪个类加载器加载由 双亲委派模型 决定。Java中的每个类都有一个引用指向它的ClassLoader。数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。 加载阶段与连接阶段是交叉进行的加载尚未结束连接阶段可能就开始了。 验证 文件格式验证Class 文件格式检查比如版本号是否在当前虚拟机的处理范围之内 基于类的二进制字节流进行目的是保证输入的字节流能正确地解析并存储在方法区内。 而其他三个验证阶段都是基于方法区的存储结构上进行的不会再直接读取、操作字节流。 元数据验证字节码语义检查比如这个类是否有父类是否继承了final类等 字节码验证程序语义检查 符号引用验证类的正确性检查比如该类使用的其他类是否存在字段是否存在等 发生在类加载过程中的解析阶段具体点说就是JVM将符号引用转化为直接引用的时候。 用来确保解析阶段能够正常执行。如果无法通过符号引用验证JVM会抛出异常。 准备 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段这些内存都将在方法区中分配。对于该阶段有以下几点需要注意 此时分配的变量仅包括静态/类变量static 修饰的变量而不包括实例变量实例变量会在对象实例化时随着对象一块分配在Java堆中。 JDK7之前类变量使用的内存都在方法区永久代中分配JDK7之后HotSpot使用元空间来代替方法区而字符串常量池和静态变量移动到了堆中。 那么类变量就随着Class对象一起存放在Java堆中。 这里所设置的初始值通常情况下是数据类型默认的零值如 0、0L、null、false 等比如我们定义了public static int value111 那么 value 变量在准备阶段的初始值就是 0 而不是 111初始化阶段才会赋值。特殊情况比如给 value 变量加上了 final 关键字public static final int value111 那么准备阶段 value 的值就被赋值为 111。 解析 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 直接引用直接引用是可以直接指向目标的指针。 举个例子在程序执行方法时系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置从而使得方法可以被调用。 综上解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程也就是得到类或者字段、方法在内存中的指针或者偏移量。 初始化 初始化阶段是执行初始化方法 clinit ()方法的过程是类加载的最后一步这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。 对于初始化阶段虚拟机严格规范了有且只有 5 种情况下必须对类进行初始化(只有主动去使用类才会初始化类) 当遇到 new、getstatic、putstatic 或 invokestatic 这 4 条直接码指令时比如 new一个类读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量常量会被加载到运行时常量池)。当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname(...), newInstance() 等等。如果类没初始化需要触发其初始化。初始化一个类如果其父类还未初始化则先触发该父类的初始化。当虚拟机启动时用户需要定义一个要执行的主类 (包含 main 方法的那个类)虚拟机会先初始化这个类。当一个接口中定义了 JDK8 新加入的默认方法被 default 关键字修饰的接口方法时如果有这个接口的实现类发生了初始化那该接口要在其之前被初始化。 类卸载 卸载类需要满足如下要求 该类的所有的实例对象都已被 GC也就是说堆不存在该类的实例对象。该类没有在其他任何地方被引用该类的类加载器的实例已被 GC 所以在 JVM 生命周期内由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
类加载器详解
加载
通过全类名获取定义此类的二进制字节流。将字节流所代表的静态存储结构转换为方法区的运行时数据结构。在内存中生成一个代表该类的 Class 对象作为方法区这些数据的访问入口。
类加载器 类加载器作用 类加载器的主要作用就是加载Java类字节码.class文件到JVM中在内存中生成CLass对象代表该类。类加载器实现了类加载过程中的加载这一步。 每个 Java 类都有一个引用指向加载它的 ClassLoader。 不过数组类不是通过 ClassLoader 创建的而是 JVM 在需要的时候自动创建的数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。 类加载器加载规则 大部分类在具体用到才会去加载这样对内存更为友好。对于已经加载的类会被存放在ClassLoader中在类加载的时候系统会首先判断当前类是否被加载过。已经被加载的类会直接返回否则才会尝试加载。也就是说对于一个类加载器来说相同二进制名称的类只会被加载一次。 类加载器总结 JVM 中内置了三个重要的 ClassLoader BootstrapClassLoader(启动类加载器) 最顶层的加载类由 C实现通常表示为 null并且没有父级主要用来加载 JDK 内部的核心类库 %JAVA_HOME%/lib目录下的 rt.jar、resources.jar 、charsets.jar等 jar 包和类以及被 -Xbootclasspath参数指定的路径下的所有类。 rt.jar是Java基础类库包含Java doc里面看到的所有的类的类文件。也就是说我们常用内置库 java.xxx.* 都在里面比如java.util.*、java.io.*、java.nio.*、java.lang.*、java.sql.*、java.math.*。 ExtensionClassLoader(扩展类加载器) 主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。AppClassLoader(应用程序类加载器) 面向我们用户的加载器负责加载当前应用 classpath 下的所有 jar 包和类。 除了 BootstrapClassLoader 是 JVM 自身的一部分之外其他所有的类加载器都是在 JVM 外部实现的并且全都继承自 ClassLoader 抽象类。这样做的好处是用户可以自定义类加载器以便让应用程序自己决定如何去获取所需的类。 每个 ClassLoader 可以通过getParent()获取其父 ClassLoader如果获取到 ClassLoader 为null的话那么该类是通过 BootstrapClassLoader 加载的。 因为是用C 实现的。 自定义类加载器 除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器很明显需要继承 ClassLoader抽象类。 ClassLoader 类有两个关键的方法 protected Class loadClass(String name, boolean resolve)加载指定二进制名称的类实现了双亲委派机制 。name 为类的二进制名称resolve 如果为 true在加载时调用 resolveClass(Class? c) 方法解析该类。protected Class findClass(String name)根据类的二进制名称来查找类默认实现是空方法。 建议 ClassLoader的子类重写 findClass(String name)方法而不是loadClass(String name, boolean resolve) 方法。 也就是如果不想打破双亲委派模型使用findClass()方法即可无法被父类加载器加载的类最终会通过这个方法被加载。否则重写loadClass()方法。
双亲委派模型
双亲委派模型用来判断我们使用哪个类加载器来加载类。反过来说就是类加载器ClassLoader使用委派模型来搜索类和资源。
双亲委派模型Parents Delegation Model如下所示
模型要求顶层的启动类加载器除外其余的类加载器必须有自己的父类加载器。
另外在查找类或资源之前搜索类和资源的任务会委托给父类加载器。
类加载器之间的父子关系一般不是以继承的关系来实现的而是通过组合关系来复用父加载器的代码。
因为组合优于继承多用组合少用继承。
双亲委派模型的执行流程
双亲委派模型的实现代码非常简单逻辑非常清晰都集中在 java.lang.ClassLoader 的 loadClass() 中相关代码如下所示。
protected Class? loadClass(String name, boolean resolve)throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {//首先检查该类是否已经加载过Class c findLoadedClass(name);if (c null) {//如果 c 为 null则说明该类没有被加载过long t0 System.nanoTime();try {if (parent ! null) {//当父类的加载器不为空则通过父类的loadClass来加载该类//--》这里进入递归调用c parent.loadClass(name, false);} else {//当父类的加载器为空则调用启动类加载器来加载该类c findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//非空父类的类加载器无法找到相应的类则抛出异常}if (c null) {//当父类加载器无法加载时则调用findClass方法来加载该类//用户可通过覆写该方法来自定义类加载器long t1 System.nanoTime();c findClass(name);//用于统计类加载器相关的信息sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {//对类进行link操作resolveClass(c);}return c;}
}
每当一个类加载器接收到加载请求时它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下该类加载器才会尝试去加载。
结合上面的源码简单总结一下双亲委派模型的执行流程
在类加载的时候系统会首先判断当前类是否被加载过。已经被加载的类会直接返回否则才会尝试加载每个父类加载器都会走一遍这个流程。 判断两个Java类是否相同主要在两个方面全类名 类加载器都一致。 类加载器在进行类加载的时候它首先不会自己去尝试加载这个类而是把这个请求委派给父类加载器去完成调用父加载器 loadClass()方法来加载类。这样的话所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。只有当父加载器反馈自己无法完成这个加载请求它的搜索范围中没有找到所需的类时子加载器才会尝试自己去加载调用自己的 findClass() 方法来加载类。
双亲委派模型好处
双亲委派模型保证了 Java 程序的稳定运行可以避免类的重复加载JVM 区分不同类的方式不仅仅根据类名相同的类文件被不同的类加载器加载产生的是两个不同的类也保证了 Java 的核心 API 不被篡改。
打破双亲委派模型
自定义加载器的话需要继承 ClassLoader 。如果我们不想打破双亲委派模型就重写 ClassLoader 类中的 findClass() 方法即可无法被父类加载器加载的类最终会通过这个方法被加载。但是如果想打破双亲委派模型则需要重写 loadClass() 方法。
因为类加载器进行类加载的过程中它首先不会自己去尝试加载这个类而是把这个请求委派给父类加载器去完成调用父加载器 loadClass()方法来加载类。
比如 Tomcat 服务器为了能够优先加载 Web 应用目录下的类然后再加载其他目录下的类就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。使得 Tomcat 可以加载不同Web应用下相同名的Servlet类。Tomcat中可以运行多个Web应用程序并且不会冲突