中山做网站哪家好,企业官网有哪些网站,织梦只显示网站首页,wordpress加载图片的速度原文#xff1a;juejin.im/post/5cffa528e51d4556da53d091一、引言当程序使用某个类时#xff0c;如果该类还未被加载到内存中#xff0c;则JVM会通过加载、链接、初始化三个步骤对该类进行类加载。二、类的加载、链接、初始化1、加载类加载指的是将类的class文件读入内存juejin.im/post/5cffa528e51d4556da53d091一、引言当程序使用某个类时如果该类还未被加载到内存中则JVM会通过加载、链接、初始化三个步骤对该类进行类加载。二、类的加载、链接、初始化1、加载类加载指的是将类的class文件读入内存并为之创建一个java.lang.Class对象。类的加载过程是由类加载器来完成类加载器由JVM提供。我们开发人员也可以通过继承ClassLoader来实现自己的类加载器。1.1、加载的class来源从本地文件系统内加载class文件从JAR包加载class文件通过网络加载class文件把一个java源文件动态编译并执行加载。2、类的链接通过类的加载内存中已经创建了一个Class对象。链接负责将二进制数据合并到 JRE中。链接需要通过验证、准备、解析三个阶段。2.1、验证验证阶段用于检查被加载的类是否有正确的内部结构并和其他类协调一致。即是否满足java虚拟机的约束。2.2、准备类准备阶段负责为类的类变量分配内存并设置默认初始值。2.3、解析我们知道引用其实对应于内存地址。思考这样一个问题在编写代码时使用引用方法时类知道这些引用方法的内存地址吗显然是不知道的因为类还未被加载到虚拟机中你无法获得这些地址。举例来说对于一个方法的调用编译器会生成一个包含目标方法所在的类、目标方法名、接收参数类型以及返回值类型的符号引用来指代要调用的方法。解析阶段的目的就是将这些符号引用解析为实际引用。如果符号引用指向一个未被加载的类或者未被加载类的字段或方法那么解析将触发这个类的加载(但未必会触发解析与初始化)。3、类的初始化类的初始化阶段虚拟机主要对类变量进行初始化。虚拟机调用 clinit方法进行类变量的初始化。java类中对类变量进行初始化的两种方式在定义时初始化在静态初始化块内初始化3.1、 clinit方法相关虚拟机会收集类及父类中的类变量及类方法组合为 clinit方法根据定义的顺序进行初始化。虚拟机会保证子类的 clinit执行之前父类的 clinit方法先执行完毕。因此虚拟机中第一个被执行完毕的 clinit方法肯定是java.lang.Object方法。public class Test { static int A 10; static { A 20; }}class Test1 extends Test { private static int B A; public static void main(String[] args) { System.out.println(Test1.B); }}//输出结果//20从输出中看出父类的静态初始化块在子类静态变量初始化之前初始化完毕所以输出结果是20不是10。如果类或者父类中都没有静态变量及方法虚拟机不会为其生成 clinit方法。接口与类不同的是执行接口的clinit方法不需要先执行父接口的clinit方法。只有当父接口中定义的变量使用时父接口才会初始化。另外接口的实现类在初始化时也一样不会执行接口的clinit方法。public interface InterfaceInitTest { long A CurrentTime.getTime();}interface InterfaceInitTest1 extends InterfaceInitTest { int B 100;}class InterfaceInitTestImpl implements InterfaceInitTest1 { public static void main(String[] args) { System.out.println(InterfaceInitTestImpl.B); System.out.println(---------------------------); System.out.println(当前时间InterfaceInitTestImpl.A); }}class CurrentTime { static long getTime() { System.out.println(加载了InterfaceInitTest接口); return System.currentTimeMillis(); }}//输出结果//100//---------------------------//加载了InterfaceInitTest接口//当前时间1560158880660从输出验证了对于接口只有真正使用父接口的类变量才会真正的加载父接口。这跟普通类加载不一样。虚拟机会保证一个类的 clinit方法在多线程环境中被正确地加锁和同步如果多个线程同时去初始化一个类那么只有一个线程去执行这个类的 clinit方法其他线程都需要阻塞等待直到活动线程执行 clinit方法完毕。public class MultiThreadInitTest { static int A 10; static { System.out.println(Thread.currentThread()init MultiThreadInitTest); try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { Runnable runnable () - { System.out.println(Thread.currentThread() start); System.out.println(MultiThreadInitTest.A); System.out.println(Thread.currentThread() run over); }; Thread thread1 new Thread(runnable); Thread thread2 new Thread(runnable); thread1.start(); thread2.start(); }}//输出结果//Thread[main,5,main]init MultiThreadInitTest//Thread[Thread-0,5,main]start//10//Thread[Thread-0,5,main]run over//Thread[Thread-1,5,main]start//10//Thread[Thread-1,5,main]run over从输出中看出验证了只有第一个线程对MultiThreadInitTest进行了一次初始化第二个线程一直阻塞等待等第一个线程初始化完毕。3.2、类初始化时机当虚拟机启动时初始化用户指定的主类当遇到用以新建目标类实例的new指令时初始化new指令的目标类当遇到调用静态方法或者使用静态变量初始化静态变量或方法所在的类子类初始化过程会触发父类初始化如果一个接口定义了default方法那么直接实现或者间接实现该接口的类的初始化会触发该接口初始化使用反射API对某个类进行反射调用时初始化这个类Class.forName()会触发类的初始化3.3、final定义的初始化注意对于一个使用final定义的常量如果在编译时就已经确定了值在引用时不会触发初始化因为在编译的时候就已经确定下来就是“宏变量”。如果在编译时无法确定在初次使用才会导致初始化。public class StaticInnerSingleton { /** * 使用静态内部类实现单例 * 1线程安全 * 2懒加载 * 3非反序列化安全即反序列化得到的对象与序列化时的单例对象不是同一个违反单例原则 */ private static class LazyHolder { private static final StaticInnerSingleton INNER_SINGLETON new StaticInnerSingleton(); } private StaticInnerSingleton() { } public static StaticInnerSingleton getInstance() { return LazyHolder.INNER_SINGLETON; }}看这个例子单例模式静态内部类实现方式。我们可以看到单例实例使用final定义但在编译时无法确定下来所以在第一次使用StaticInnerSingleton.getInstance()方法时才会触发静态内部类的加载也就是延迟加载。这里想指出如果final定义的变量在编译时无法确定则在使用时还是会进行类的初始化。3.4、ClassLoader只会对类进行加载不会进行初始化public class Tester { static { System.out.println(Tester类的静态初始化块); }}class ClassLoaderTest { public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader ClassLoader.getSystemClassLoader(); //下面语句仅仅是加载Tester类 classLoader.loadClass(loader.Tester); System.out.println(系统加载Tester类); //下面语句才会初始化Tester类 Class.forName(loader.Tester); }}//输出结果//系统加载Tester类//Tester类的静态初始化块从输出证明ClassLoader只会对类进行加载不会进行初始化使用Class.forName()会强制导致类的初始化。三、类加载器类加载器负责将.class文件(不管是jar,还是本地磁盘还是网络获取等等)加载到内存中并为之生成对应的java.lang.Class对象。一个类被加载到JVM中就不会第二次加载了。那怎么判断是同一个类呢每个类在JVM中使用全限定类名(包名类名)与类加载器联合为唯一的ID所以如果同一个类使用不同的类加载器可以被加载到虚拟机但彼此不兼容。1、JVM类加载器分类1.1、Bootstrap ClassLoaderBootstrap ClassLoader为根类加载器负责加载java的核心类库。根加载器不是ClassLoader的子类是有C实现的。public class BootstrapTest { public static void main(String[] args) { //获取根类加载器所加载的全部URL数组 URL[] urLs Launcher.getBootstrapClassPath().getURLs(); Arrays.stream(urLs).forEach(System.out::println); }}//输出结果//file:/C:/SorftwareInstall/java/jdk/jre/lib/resources.jar//file:/C:/SorftwareInstall/java/jdk/jre/lib/rt.jar//file:/C:/SorftwareInstall/java/jdk/jre/lib/sunrsasign.jar//file:/C:/SorftwareInstall/java/jdk/jre/lib/jsse.jar//file:/C:/SorftwareInstall/java/jdk/jre/lib/jce.jar//file:/C:/SorftwareInstall/java/jdk/jre/lib/charsets.jar//file:/C:/SorftwareInstall/java/jdk/jre/lib/jfr.jar//file:/C:/SorftwareInstall/java/jdk/jre/classes根类加载器负责加载%JAVA_HOME%/jre/lib下的jar包(以及由虚拟机参数 -Xbootclasspath 指定的类)。我们将rt.jar解压可以看到我们经常使用的类库就在这个jar包中。1.2 、Extension ClassLoaderExtension ClassLoader为扩展类加载器负责加载%JAVA_HOME%/jre/ext或者java.ext.dirs系统熟悉指定的目录的jar包。大家可以将自己写的工具包放到这个目录下可以方便自己使用。1.3、 System ClassLoaderSystem ClassLoader为系统(应用)类加载器负责加载加载来自java命令的-classpath选项、java.class.path系统属性或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器。如果没有特别指定则用户自定义的类加载器默认都以系统类加载器作为父加载器。四、类加载机制1.1、JVM主要的类加载机制。全盘负责当一个类加载器负责加载某个Class时该Class所依赖和引用的其他Class也由该类加载器负责载入除非显示使用另一个类加载器来载入。父类委托(双亲委派)先让父加载器试图加载该Class只有在父加载器无法加载时该类加载器才会尝试从自己的类路径中加载该类。缓存机制缓存机制会将已经加载的class缓存起来当程序中需要使用某个Class时类加载器先从缓存区中搜寻该Class只有当缓存中不存在该Class时系统才会读取该类的二进制数据并将其转换为Class对象存入缓存中。这就是为什么更改了class后需要重启JVM才生效的原因。注意类加载器之间的父子关系并不是类继承上的父子关系而是实例之间的父子关系。public class ClassloaderPropTest { public static void main(String[] args) throws IOException { //获取系统类加载器 ClassLoader systemClassLoader ClassLoader.getSystemClassLoader(); System.out.println(系统类加载器 systemClassLoader); /* 获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定如果操作系统没有指定 CLASSPATH环境变量则默认以当前路径作为系统类加载器的加载路径 */ Enumeration eml systemClassLoader.getResources();while (eml.hasMoreElements()) { System.out.println(eml.nextElement()); }//获取系统类加载器的父类加载器得到扩展类加载器 ClassLoader extensionLoader systemClassLoader.getParent(); System.out.println(系统类的父加载器是扩展类加载器 extensionLoader); System.out.println(扩展类加载器的加载路径 System.getProperty(java.ext.dirs)); System.out.println(扩展类加载器的parant extensionLoader.getParent()); }}//输出结果//系统类加载器sun.misc.Launcher$AppClassLoader18b4aac2//file:/C:/ProjectTest/FengKuang/out/production/FengKuang///系统类的父加载器是扩展类加载器sun.misc.Launcher$ExtClassLoader1540e19d//扩展类加载器的加载路径C:\SorftwareInstall\java\jdk\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext//扩展类加载器的parantnull从输出中验证了系统类加载器的父加载器是扩展类加载器。但输出中扩展类加载器的父加载器是null这是因为父加载器不是java实现的是C实现的所以获取不到。但扩展类加载器的父加载器是根加载器。1.2、类加载流程图图中红色部分可以是我们自定义实现的类加载器来进行加载。五、创建并使用自定义类加载器1、自定义类加载分析除了根类加载器所有类加载器都是ClassLoader的子类。所以我们可以通过继承ClassLoader来实现自己的类加载器。ClassLoader类有两个关键的方法protected Class loadClass(String name, boolean resolve)name为类名resove如果为true在加载时解析该类。protected Class findClass(String name) 根据指定类名来查找类。所以如果要实现自定义类可以重写这两个方法来实现。但推荐重写findClass方法而不是重写loadClass方法因为loadClass方法内部会调用findClass方法。我们来看一下loadClass的源码protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { //第一步先从缓存里查看是否已经加载 Class c findLoadedClass(name); if (c null) { long t0 System.nanoTime(); try { //第二步判断父加载器是否为null if (parent ! null) { c parent.loadClass(name, false); } else { c findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c null) { //第三步如果前面都没有找到就会调用findClass方法 long t1 System.nanoTime(); c findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }loadClass加载方法流程判断此类是否已经加载如果父加载器不为null则使用父加载器进行加载反之使用根加载器进行加载如果前面都没加载成功则使用findClass方法进行加载。所以为了不影响类的加载过程我们重写findClass方法即可简单方便的实现自定义类加载。2、实现自定义类加载器基于以上分析我们简单重写findClass方法进行自定义类加载。public class Hello { public void test(String str){ System.out.println(str); }}public class MyClassloader extends ClassLoader { /** * 读取文件内容 * * param fileName 文件名 * return */ private byte[] getBytes(String fileName) throws IOException { File file new File(fileName); long len file.length(); byte[] raw new byte[(int) len]; try (FileInputStream fin new FileInputStream(file)) { //一次性读取Class文件的全部二进制数据 int read fin.read(raw); if (read ! len) { throw new IOException(无法读取全部文件); } return raw; } } Override protected Class findClass(String name) throws ClassNotFoundException { Class clazz null; //将包路径的(.)替换为斜线(/) String fileStub name.replace(., /); String classFileName fileStub .class; File classFile new File(classFileName); //如果Class文件存在系统负责将该文件转换为Class对象 if (classFile.exists()) { try { //将Class文件的二进制数据读入数组 byte[] raw getBytes(classFileName); //调用ClassLoader的defineClass方法将二进制数据转换为Class对象 clazz defineClass(name, raw, 0, raw.length); } catch (IOException e) { e.printStackTrace(); } } //如果clazz为null,表明加载失败抛出异常 if (null clazz) { throw new ClassNotFoundException(name); } return clazz; } public static void main(String[] args) throws Exception { String classPath loader.Hello; MyClassloader myClassloader new MyClassloader(); Class aClass myClassloader.loadClass(classPath); Method main aClass.getMethod(test, String.class); System.out.println(main); main.invoke(aClass.newInstance(), Hello World); }}//输出结果//Hello WorldClassLoader还有一个重要的方法defineClass(String name, byte[] b, int off, int len)。此方法的作用是将class的二进制数组转换为Calss对象。此例子很简单我写了一个Hello测试类并且编译过后放在了当前路径下(大家可以在findClass中加入判断如果没有此文件可以尝试查找.java文件并进行编译得到.class文件或者判断.java文件的最后更新时间大于.class文件最后更新时间再进行重新编译等逻辑)。六、总结本篇从类加载的三大阶段加载、链接、初始化开始细说每个阶段的过程详细讲解了JVM常用的类加载器的区别与联系以及类加载机制流程最后通过自定义的类加载器例子结束本篇。【面试题专栏】2019年面试官最喜欢问的28道ZooKeeper面试题2019年全网最热门的123个Java并发面试题总结全网最热门的119个Spring问题哪些你还不会2020面试还搞不懂MyBatis看看这27道面试题(含答案和思维导图)2020年去一线大厂面试先过SSM框架这一关Spring CloudSpring Boot高频面试题解析2019年常见的Linux面试题及答案解析哪些你还不会2019年常见Elasticsearch面试题答案解析18道kafka高频面试题哪些你还不会(含答案和思维导图)2019年12道RabbitMQ高频面试题你都会了吗(含答案解析)2019年Dubbo你掌握的如何快看看这30道高频面试题2019年228道Java中高级面试题(8)哪些你还不会 你在看吗