我们知道java要运行需要编译和运行,javac将java源代码编译为class文件。而虚拟机把描述类的数据从class文件中加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的java类型,这就是 类加载机制 ,他在运行期间完成。
JVM加载class文件到内存有两种方式:
之前的我只知道在对象创建之前会先初始化静态的东西,也知道从父类开始初始化,但一直不懂为什么会是这样的顺序,直到我了解了虚拟机是如何实现类加载的。在开始真正了解类加载之前,我们先来看三个例子。
class SuperClass { static{ System.out.println("SuperClass Init"); } public static int value = 123; } class SubClass extends SuperClass{ static{ System.out.println("SubClass Init"); } } public class NotInitialization{ public static void main(String agrs[]){ System.out.println(SubClass.value); } } 复制代码
SuperClass Init 123 复制代码
这道例子似乎很简单,他告诉我们对于 静态字段,只有直接定义这个字段的类才会被初始化 ,所以,即使这里是通过子类来引用父类的静态属性,他也不会使子类发生初始化,而至于加载和验证,虚拟机并没有明确规范,各步骤的作用下文会谈
class SuperClass { static{ System.out.println("SuperClass Init"); } public static int value = 123; } class SubClass extends SuperClass{ static{ System.out.println("SubClass Init"); } } public class NotInitialization{ public static void main(String agrs[]){ SuperClass[] sca = new SuperClass[10]; } } 复制代码
//无输出 复制代码
是的,运行之后并没有输出,但他触发了一个叫“[Lorg.fenixsoft.classloading.SuperClass”的类初始化,而 创建动作由字节码指令newarray触发 ,从这里,我们也就直到创建一个对象数组的真实情况了
class ConstClass{ static{ System.out.println("ConstClass init"); } public static final String WORD = "Hello"; } public class NotInitialization{ public static void main(String agrs[]){ System.out.println(ConstClass.WORD); } } 复制代码
Hello 复制代码
这里WORD作为一个常量,他在编译阶段就已经生成,意思是说编译阶段经过常量传播优化,已经将他存储到了NotInitialization类的常量池中,以后所有对它的引用都是NotInitialization对常量池的引用,这就是为什么不初始化类。
下面来总结一下五种必须对类初始化的情况:
以上,都是类第一次发生初始化的情况,而对于接口的初始化,他和类的不同就是只有在真正使用到父接口的时候才会初始化父接口。
下面来具体看一下类加载的全过程分别要做哪些事情
这个时期需要完成三件事:
说直白加载的作用就是找到.class文件并把这个文件包含的字节码读取到内存中
这一步的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全,大概分为四部验证
为类变量分配内存并设置类变量初始化值,在方法区进行分配,如int为0,boolean为false,reference为null
将常量池内的符号引用替换为直接引用的过程
问,什么是符号引用,什么是直接引用?
符号引用就是一个字符串,这个字符串有足够的信息可以找到相应的位置。直接引用就是偏移量,通过偏移量可以直接在内存区域找到方法字节码的起始位置。
解析主要包括对类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符这些符号引用进行
在类中包含的静态初始化器都被执行,在这一阶段末尾静态字段被初始化为默认值,初始化遵守下面几条原则(其中是类初始化的字节码指令)
public class Test { static { i = 0; //System.out.println(i); } static int i; } 复制代码
上面注释的那一行会报错,因为在静态初始化块中只能访问到定义在静态语句块之前的变量;定义在他之后的变量,在前面的静态语句块可以赋值,不能访问,说明了第一条
public class Test { static class DeadLoopClass{ static{ if (true){ System.out.println(Thread.currentThread() + "init DeadLoopClass"); while(true){ } } } } public static void main(String agrs[]){ Runnable script = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread() + "start"); DeadLoopClass dlc = new DeadLoopClass(); System.out.println(Thread.currentThread() + "run over"); } }; Thread t1 = new Thread(script); Thread t2 = new Thread(script); t1.start(); t2.start(); } } 复制代码
Thread[Thread-0,5,main]start Thread[Thread-1,5,main]start Thread[Thread-0,5,main]init DeadLoopClass 复制代码
他会打印上面的语句并会发生阻塞,这个例子说明了初始化的时候会保证类会被正确加锁
接下来我们具体看一下类加载器有哪些特点,它的作用就是动态加载类到Java虚拟机的内存空间中,就是上文说的“通过一个类的全限定名来获取描述此类的二进制字节流”, 并且这个动作是放到Java虚拟机外部实现的,就是说应用程序自己决定如何去获取需要的类
在JVM中标识两个class对象是否为同一个类对象存在两个必要条件
一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销
类加载器可以大致分为三类:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
注意,这里叫双亲不是因为继承关系而是组合关系
很容易想到,双亲委派模型的层级可以避免重复加载,尤其是java的核心类库不会被替换,例如自己定义了一个java.lang.Integer,双亲委派模型不会去初始化他,而是直接返回加载过的Integer.class。当然,如果强行用defineClass()方法(这个方法将byte字节流解析成JVM能够识别的Class对象)去加载java.lang开头的类也不会成功,会抛出安全异常
ClassLoader的loadClass(),只列出了关键的
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ //首先,检查请求的类是否已经被加载过了 Class c = findLoadedClass(name); if (c == null){ try{ if (parent != null){ c = parent.loadClass(name,false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e){ //如果父类加载器抛出ClassNotFoundException,说明父类加载器无法完成加载请求 } if (c == null){ //在父类加载器无法加载的时候 //再调用本身的findClass方法来进行类加载 c = findClass(name); } } if (resolve){ //使用类的Class对象创建完成也同时被解析 resolveClass(c); } return c; } 复制代码
ClassLoader的findClass(),
//直接抛出异常 protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } 复制代码
ClassLoader的defineClass
protected Class<?> findClass(String name) throws ClassNotFoundException { //获取类的class文件字节数组 byte[] classData = getClassData(name); if (classData == null){ throw new ClassNotFoundException(); } else { //直接生成class对象 return defineClass(name,classData,0,classData.length); } } 复制代码
ClassLoader的resolveClass()
protected final void resolveClass(Class<?> c) { if (c == null) { throw new NullPointerException(); } } 复制代码
下面再来看一下关键方法的具体作用:
先看以下loadClass()方法,通过以上代码可以看到逻辑并不复杂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass(),若父加载器为空让启动类加载器为父加载器,若父类加载失败,抛出异常,再调用自己的findClass()方法
在JDK1.2之后,如果我们自定义类加载器的话我们将不再重写loadClass(),因为ClassLoader已经实现loadClass(),并且用它来达到双亲委派的效果。我们自定义类加载器需要重写的是findClass(),知道findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。
双亲委派模型不是一个强制性的约束模型,双亲委派模型也有不太适用的时候,这时根据具体的情况我们就要破坏这种机制,双亲委派模型主要出现过三次被破坏的情况
因为双亲委派模型是在JDK1.2的时候出现的,所以,在JDK1.2之前,是没有双亲委派的,为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的protected的findClass()方法,这个方法的唯一逻辑就是调用自己的loadClass(),前文分析代码实现的时候我们知道双亲委派模型就是根据loadClass()来实现的,所以为了使用双亲委派模型,我们应当把自己的类加载逻辑写道findClass()中。
我们有一些功能是java提供接口,而其他的公司提供实现类,例如我们的JDBC、JNDI(由多个公司提供自己的实现)所以像JDBC、JNDI这样的SPI(服务提供者接口),就需要第三方实现,这些SPI的接口属于核心库,由Bootstrap类加载器加载,那么如何去加载那些公司提供的实现类呢?这就是我们的 线程上下文类加载器 ,下图是整体大概的工作流程
这里,线程上下文加载器默认是父类加载器是ApplicationClassLoader
第三次破坏委派双亲模型就是由于用户追求 动态性 导致的,“动态性”就是指代码 热替换、模块热部署 等,就是希望程序不需要重启就可以更新class文件,最典型的例子就是SpringBoot的热部署和OSGi。这里拿OSGi举例,OSGi实现模块化热部署的关键就是它自定义类加载机制的实现,每一个程序模块(OSGi中称为Bundle)都有自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉实现热部署
所以,在OSGi环境下,类加载器不再是层次模型,而是网状模型,如图
当OSGi收到一个类加载的时候会按照以下的顺序进行搜索:
以上前两点仍符合双亲委派规则,其余都是平级类加载器查找
前文我们了解了Java中类加载器的运行方式;但主流的Web服务器都会有自己的一套类加载器,为什么呢?因为对于服务器来说他要自己解决一些问题:
显然,如果Tomcat使用默认的类加载机制是无法满足上述要求的
++一个WebAppClassLoader下可能还对应多个JspClassLoader++
CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
Tomcat 6.x把/common、/server和/shared三个目录默认合并到一起变成一个/lib目录,这个目录里的类库相当于以前/common目录中类库的作用
现在我们再来看Tomcat时如何解决之前的四个问题的:
前文我们说过破坏委托模型,这里就是一个例子,可以采用线程上下文加载器,让父类加载器请求子类加载器完成加载类作用
这个错误是说当JVM加载指定文件的字节码到内存时,找不到相应的字节码。解决办法为在当前classpath目录下找有没有指定文件(this.getClass().getClassLoader().getResource("").toString()可以查看当前classpath)
这种错误出现的情况就是使用了new关键字、属性引用某个类、继承某个接口或实现某个类或某个方法参数引用了某个类,这时虚拟机隐式加载这些类发现这些类不存在的异常。解决这个错误的办法就是确保每个类引用的类都在当前的classpath下面
可能是在JVM启动的时候不小心在JVM中的某个lib删了
无法转型,这个可能对于初学者来说会很常见(比如说我,哈哈),解决办法时转型前先用instanceof检查是不是目标类型再转换
这个异常是由于类加载过程中静态块初始化过程失败所导致的。由于它出现在负责启动程序的主线程中,因此你最好从主类中开始分析,这里说的主类是指你在命令行参数中指定的那个,或者说是你声明了public static void main(String args[])方法的那个类。这个异常很大可能会伴随NoClassDefFoundError,所以出现NoClassDefFoundError时我们先看ExceptionInInitializerError出现没。
接下来我们要自己写一个类加载器,在开始写之前,我们要知道为什么需要我们自己写类加载器呢?
下面我们开始自定义类加载器吧
package SelfClassLoader; import java.io.*; public class FileClassLoader extends ClassLoader { private String rootDir; public FileClassLoader(String rootDir){ this.rootDir = rootDir; } /** * 编写findClass方法的逻辑 * @param name * @return * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //获取类的class文件字节数组 byte[] classData = getClassData(name); if (classData == null){ throw new ClassNotFoundException(); } else { //直接生成class对象 return defineClass(name,classData,0,classData.length); } } /** * 编写获取class文件并转换为字节码流的逻辑 * @param className * @return */ private byte[] getClassData(String className){ //读取类文件的字节 String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[2048]; int bytesNumRead = 0; // 读取类文件的字节码 while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } /** * 类文件的完整路径 * @param className * @return */ private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } /** * 读取文件 */ public static void main(String[] args) throws ClassNotFoundException { String rootDir="C://java//JVM//JVMInstruction//src"; //创建自定义文件类加载器 FileClassLoader loader = new FileClassLoader(rootDir); try { //加载指定的class文件,加上包名 Class<?> object1=loader.loadClass("SelfClassLoader.DemoObj"); System.out.println(object1.newInstance().toString()); //输出结果:I am DemoObj } catch (Exception e) { e.printStackTrace(); } } } 复制代码
我们通过getClassData()方法找到class文件并转换为字节流,并重写findClass()方法,利用defineClass()方法创建了类的class对象。在main方法中调用了loadClass()方法加载指定路径下的class文件,由于启动类加载器、拓展类加载器以及系统类加载器都无法在其路径下找到该类,因此最终将有自定义类加载器加载,即调用findClass()方法进行加载。
还有一种方式是继承URLClassLoader类,然后设置自定义路径的URL来加载URL下的类,这种方式更常见
package SelfClassLoader; import java.io.File; import java.net.*; public class PathClassLoader extends URLClassLoader { private String packageName = "net.lijunfeng.classloader"; public PathClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } public PathClassLoader(URL[] urls) { super(urls); } public PathClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) { super(urls, parent, factory); } protected Class<?> findClass(String name) throws ClassNotFoundException{ Class<?> aClass = findLoadedClass(name); if (aClass != null){ return aClass; } if (!packageName.startsWith(name)){ return super.loadClass(name); } else { return findClass(name); } } public static void main(String[] args) throws ClassNotFoundException, MalformedURLException { String rootDir="C://java//JVM//JVMInstruction//src"; //创建自定义文件类加载器 File file = new File(rootDir); //File to URI URI uri=file.toURI(); URL[] urls={uri.toURL()}; PathClassLoader loader = new PathClassLoader(urls); try { //加载指定的class文件 Class<?> object1=loader.loadClass("SelfClassLoader.DemoObj"); System.out.println(object1.newInstance().toString()); //输出结果:I am DemoObj } catch (Exception e) { e.printStackTrace(); } } } 复制代码
。