作者个人研发在高并发场景下提供了一个简单、稳定、可扩展的延迟消息队列框架,具有精准的定时任务和延迟队列处理功能。开源半年多以来,已成功为十几家中小企业提供精准定时调度解决方案,经受住了生产环境的考验。为了造福更多的童鞋,这里给出开源框架的地址:https://github.com/sunshinelyz/mykit-delay前面写的是几年前面试高级Java程序员的时候,只要你了解一点JVM基础知识,基本可以通过面试。近年来,对Java工程师的要求越来越严格。对于中级Java工程师来说,掌握JVM相关知识也是很有必要的。这不,有个读者出去面试Java职位,被问到JVM相关类的加载、链接和初始化。结果很酷。今天,我们将一起详细讨论这个问题。文章已收录于:https://github.com/sunshinelyz/technology-binghehttps://gitee.com/binghe001/technology-binghe概述在这篇文章中,我们讨论了Java类的加载、链接和初始化。Java字节码的表示形式是字节数组(byte[]),Java类在JVM中的表示形式是类java.lang.Class的对象。一个Java类从字节码到能够在JVM中使用需要经历加载、链接、初始化三个步骤。在这三个步骤中,开发者直接可见的就是Java类的加载。通过使用Java类加载器(classloader),可以在运行时动态加载一个Java类;而链接和初始化是在使用Java类之前执行的。将要发生的动作。本文将详细介绍Java类的加载、链接和初始化的过程。Java类加载Java类加载是由类加载器完成的。一般来说,类加载器分为两类:启动类加载器(bootstrap)和用户自定义类加载器(user-defined)。两者的区别在于,启动类加载器是由JVM原生代码实现的,而用户自定义类加载器继承自Java中的java.lang.ClassLoader类。在用户自定义类加载器部分,一般的JVM都会提供一些基本的实现。应用程序开发人员还可以根据需要编写自己的类加载器。JVM中最常用的是系统类加载器(system),用于启动Java应用程序的加载。类加载器对象可以通过java.lang.ClassLoader的getSystemClassLoader()方法获取。类加载器最后需要完成的功能是定义一个Java类,即将Java字节码转换为JVM中java.lang.Class类的对象。但是类加载的过程并不是那么简单。Java类加载器有两个比较重要的特点:分层组织结构和代理模式。分层组织结构意味着每个类加载器都有一个父类加载器,可以通过getParent()方法获得。类加载器以这种父-后代的方式组织在一起,形成树状层次结构。代理模式是指一个类加载器可以自己完成Java类的定义,也可以委托给其他类加载器来完成。由于代理模式的存在,启动一个类加载过程的类加载器和最终定义该类的类加载器可能不是同一个。前者称为初始类加载器,后者称为定义类加载器。两者的联系是一个Java类的定义类加载器是该类导入的其他Java类的初始类加载器。例如A类通过import导入B类,那么A类的定义类加载器负责启动B类的加载过程。一般的类加载器会先代理到它的父类加载器,然后再尝试加载一个Java类本身。当父类加载器找不到时,它会尝试自己加载。此逻辑封装在java.lang.ClassLoader类的loadClass()方法中。一般来说,父母优先的策略就足够了。在某些情况下,可能需要采取相反的策略,即先尝试自己加载,找不到时再委托给父类加载器。这种方式在JavaWeb容器中比较常见,也是Servlet规范推荐的方式。例如,ApacheTomcat为每个Web应用程序提供了一个独立的类加载器,使用它自己的优先级加载策略。IBMWebSphereApplicationServer允许Web应用程序进行选择。类加载器使用的策略类加载器的一个重要用途是在JVM中为同名的Java类创建隔离空间。在JVM中,判断两个类是否相同,不仅仅根据类的二进制名称,还需要根据两个类的类加载器的定义。只有当两个类完全相同时,它们才被认为是相同的。因此,即使相同的Java字节码被两个不同的类加载器定义,得到的Java类也是不同的。如果尝试在两个类的对象之间进行赋值操作,则会抛出java.lang.ClassCastException。这个特性为同名的Java类在JVM中共存创造了条件。在实际应用中,可能会要求JVM中可以同时存在同名的Java类的不同版本。这个需求可以通过类加载器来满足。这种技术在OSGi中被广泛使用Java类链接Java类链接是指将Java类的二进制代码合并到JVM运行状态中的过程。在链接之前必须成功加载此类。类的链接包括几个步骤,例如验证、准备和解析。验证用于确保Java类的二进制表示在结构上完全正确。如果验证过程中出现错误,将抛出java.lang.VerifyError错误。准备过程是在Java类中创建静态字段,并将这些字段的值设置为默认值。准备过程不执行代码。Java类将包含对其他类或接口的正式引用,包括其父类、实现的接口、方法的正式参数以及返回值的Java类。解析过程就是为了保证能够正确找到这些引用的类。解析过程可能会导致加载其他Java类。不同的JVM实现可能会选择不同的解析策略。一种方法是在链接期间递归地解析所有相关的正式引用。另一种方法可能是仅在真正需要正式参考时才解决。也就是说,如果一个Java类只是被引用,而没有真正被使用,那么这个类就可能无法被解析。考虑以下代码:publicclassLinkTest{publicstaticvoidmain(String[]args){ToBeLinkedtoBeLinked=null;System.out.println("Testlink.");}}类LinkTest引用类ToBeLinked,但并不真正使用它,只是声明A变量而不创建类的实例或访问其静态字段。在Oracle的JDK6中,如果删除编译后的ToBeLinkedJava字节码,然后运行??LinkTest,程序将不会抛出错误。这是因为并没有真正使用到ToBeLinked类,而Oracle的JDK6采用的链接策略阻止了ToBeLinked类的加载,所以没有发现ToBeLinked的Java字节码实际上并不存在。如果将代码更改为ToBeLinkedtoBeLinked=newToBeLinked();然后以同样的方式运行它,会抛出异常。因为此时实际使用的是ToBeLinked类,所以需要加载它。Java类的初始化当一个Java类真正第一次被使用时,JVM会对该类进行初始化。初始化过程的主要操作是执行静态代码块和初始化静态字段。在一个类可以被初始化之前,它的直接父类也需要被初始化。但是,接口的初始化不会导致其父接口的初始化。初始化时,静态代码块和静态域在源代码中按照从上到下的顺序依次初始化。考虑下面的代码:publicclassStaticTest{publicstaticintX=10;publicstaticvoidmain(String[]args){System.out.println(Y);//Output60}static{X=30;}publicstaticintY=X*2;}在上面在代码中,在初始化的时候,静态字段的初始化和静态代码块的执行会从上到下依次执行。所以变量X的值首先被初始化为10,然后赋值为30;变量Y的值被初始化为60。Java类和接口的初始化时机Java类和接口的初始化只会在特定的时间发生。这些时间包括:创建Java类的实例。例如,MyClassobj=newMyClass()调用Java类中的静态方法。例如,MyClass.sayHello()将值分配给在Java类或接口中声明的静态字段。例如MyClass.value=10访问的是Java类或接口中声明的静态字段,该字段不是常量值变量。例如,intvalue=MyClass.value在顶级Java类中执行assert语句。断言;也可以通过Java反射API引起类和接口初始化。需要注意的是,在访问Java类或接口中的静态字段时,只会初始化实际声明该字段的类或接口。如下代码所示。packageio.mykit.binghe.test;classB{staticintvalue=100;static{System.out.println("ClassBisinitialized.");//输出}}classAextendsB{static{System.out.println("ClassAisinitialized.");//不会输出}}publicclassInitTest{publicstaticvoidmain(String[]args){System.out.println(A.value);//输出100}}上面代码中,类InitTest通过A引用类B中的语句。value静态字段值。由于value是在B类中声明的,所以只有B类会被初始化,A类不会被初始化。创建自己的类加载器在Java应用程序开发过程中,您可能需要创建自己的类加载器。典型场景包括实现特定的Java字节码查找、加密/解密字节码、实现Java同名类隔离等。创建自己的类加载器并不是什么复杂的事情,只需要继承java.lang.ClassLoader类并覆盖相应的方法。java.lang.ClassLoader中提供了很多方法。下面是创建类加载器时需要考虑的几点:defineClass():该方法用于将Java字节码的字节数组转换为java.lang.Class的转换。此方法不能被覆盖,通常使用本机代码实现。findLoadedClass():此方法用于按名称查找加载的Java类。类加载器不会加载同名的类两次。findClass():此方法用于按名称查找和加载Java类。loadClass():此方法用于按名称加载Java类。resolveClass():此方法用于链接Java类。这里比较混乱的是findClass()方法和loadClass()方法的作用。前面提到,在链接Java类的过程中,需要对Java类进行解析,而解析可能会导致加载当前Java类引用的其他Java类。此时JVM通过调用当前类定义的类加载器的loadClass()方法加载其他类。findClass()方法是应用程序创建的类加载器的扩展点。具有自己的类加载器的应用程序应该覆盖findClass()方法以添加自定义类加载逻辑。loadClass()方法的默认实现将负责调用findClass()方法。前面说过,类加载器的代理模式默认使用父类优先策略。该策略的实现封装在loadClass()方法中。如果要修改此策略,则需要重写loadClass()方法。下面代码给出了自定义类加载的常见实现方式publicclassMyClassLoaderextendsClassLoader{protectedClassfindClass(Stringname)throwsClassNotFoundException{byte[]b=null;//查找或生成Java类的字节码returndefineClass(name,b,0,b.length);}}本文转载自微信公众号「冰河科技」,可通过以下二维码关注。转载本文请联系冰川科技公众号。
