ADVANCE
类加载器 #
类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet(已经被淘汰) 的需要。后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。
定义 #
1). 类加载器是一个负责加载类的对象。
ClassLoader
是一个抽象类。给定一个类的二进制名称,类加载器尝试定位或者生成构成类定义的数据。一个典型的策略是:转换一个名字到一个文件然后读取名字对应的类文件从文件系统。
2). 每个类对象都包含一个定义它的类加载器ClassLoader
的引用。
3). 数组类的类对象不是通过类加载器创建的。但会根据java运行时的需要自动创建(也就是JVM自己创建)。对于数组的Class.getClassLoader()
返回的类加载器和它加载它元素类型的加载器一样。如果元素是primitive type
原始类型(有别于复合类型),则数组类没有类加载器。
4). 应用程序实现 ClassLoader 的子类,以扩展 Java 虚拟机动态加载类的方式。
5).ClassLoader
使用委托模式查找类或资源(文本,图像,配置文件,视频等)通过getResource
,每一个ClassLoader
都关联一个父加载器。当请求查找一个类或资源的时候。在它自己加载之前会先委托父加载器去加载。JVM内建的加载器叫作bootstrap class loader
, 它自己没有父加载器,但可以作为换一个父加载器实例。
6). 通常,JVM虚拟机加载类从本地文件系统已平台相关的方式。例如:在UNIX系统中。JVM加载类通过定义的CLASSPATH
环境变量。
7). 然而,一些类获取不是来源于文件,而是来自诸如网络的情况,或者被一个应用程序构造生成。defineClass
转换一个字节数组到类的实例。可以使用Class.newInstance
创建新定义的类的实例。
8). 类加载器创建的对象的方法和构造函数可以引用其他类。为了确定引用的类,Java 虚拟机调用最初创建该类的类加载器的 loadClass 方法。 参考 , 代码实现网络类加载器demo #
加载流程 #
- 加载loading,类的加载过程这一阶段,通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象。
- 验证verification,目的在于确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全,主要包括四种验证, 文件格式,元数据,字节码,符号引用。
- 准备preparation,为类变量,(既static 修饰的字段变量)分配内存并且设置该变量的初始值即0(如static int i = 5 ;这里只将i初始化为0。 至于5的值将在初始化时赋值,这里不包含用final修饰的static,因为final在编译的时候会分配,注意这里不会为实例变量 分配初始化,类变量会分配在方法区中,而实例变量时会随着对象一起分配到Java堆中。
- 解析resolution,主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号用来描述目标,可以是任何字面量,而直接引用 就是直接指向目标的指针,相对偏移量或者一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析, 接口方法解析等。
- 初始化initialization、类加载的最后阶段,如该类具有超类,则对其进行初始化,执行类的初始化
<clinit>()
,包括静态初始化器和静态初始化成员变量(如前面只初始化了 默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。
加载规则 #
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。通过注释可以看出:
JVM 会自己调用addClass
方法用来记录当前类加载器已经加载的类。分类 #
JVM 中内置了三个重要的 ClassLoader:
1). BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,没有对应的Java类。所以在Java中是取不到的。如果一个类的classloader是null。已经足可以证明他就是由BootStrapClassLoader 加载的,通常表示为 null,并且没有父级,主要用来加载JDK内部的核心类库%JAVA_HOME%/lib目录下的rt.jar、resources.jar、charsets.jar等 jar 包和类
以及被-Xbootclasspath
参数指定的路径下的所有类。
2). ExtClassLoader(扩展类加载器) :主要负责加载%JRE_HOME%/lib/ext
目录下的 jar 包和类以及被java.ext.dirs
系统变量所指定的路径下的所有类。
3). AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath
下的所有 jar 包和类。属性parent是通过
ClassLoader(Void unused, ClassLoader parent)
方法传进去的。AppClassLoader
和ExtClassLoader
并非继承关系。双亲委派模型 #
参考 定义5). 。如果一个类加载器收到了类加载器的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类时,子加载类才会尝试自己去加载)。
自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。
由于双亲委派模型以及类加载器的一些特性(使用当前类加载器去加载引用类),会出现一些情况。
比如:
使用当前类加载器CustomClassLoader1
去加载子类CustomClassLoader2
管理路径下的class。会找不到类,以至于加载不了。 代码实现
解决办法:
Java设计团队只好引入了一个不太优雅的设计 线程上下文件类加载器(Thread Context ClassLoader) 。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个。如: SPI初始化顺序 #
父,子,静态代码块 – 和静态变量 等级初始化
父,子,普通代码块 – 和普通变量 – 构造方法(最后) 等级初始化
继续new Parent(),只执行父类的普通属性赋值和普通代码块,最后父类构造方法
继续new Child(), 执行父类的普通属性和代码块,构造方法,然后是子类的Reference #
动态代理 #
简介 #
动态代理就是,在程序运行期,创建目标对象的代理对象,并对目标对象中的方法进行功能性增强的一种技术。
在生成代理对象的过程中,目标对象不变,代理对象中的方法是目标对象方法的增强方法。
可以理解为运行期间,对象中方法的动态拦截,在拦截方法的前后执行功能操作。两种方式 #
JDK #
基于接口的动态代理,代理类必须实现某个接口。
代理配置: -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true 源码:ProxyGenerator
。可以生成代理类字节文件,方便反编译查看生成内容。
repo: jdk_dynamicCGLIB #
基于ASM字节码操作框架。由于继承关系,不能代理final类。
代理类生成配置: -Dcglib.debugLocation=/path/to/somewhere 源码:DebuggingClassWriter
cglib生成代理类的过程当中会生成一些辅助类,所以打断点获取Class字节数组的时候需要注意,一般来说最后一次生成的才是正真的Proxy。
1. class net.sf.cglib.proxy.Enhancer$EnhancerKey$$KeyFactoryByCGLIB$$7fb24d72
2. class net.sf.cglib.core.MethodWrapper$MethodWrapperKey$$KeyFactoryByCGLIB$$d45e49f7
3. class _base.proxy.cglib_dynamic.StudentNoIntfs$$EnhancerByCGLIB$$81f8e23d
repo: cglib-dynamic
反射 #
序列化 #
SPI #
定义 #
wiki: Service provider interface (SPI) 是一种API,打算让第三方厂商实现和扩展。它可以用来扩展和替换组件。
换句话说:是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。样例 #
测试代码
1). 定义接口ISpiTest
.
2). 接口ISpiTest
实现类SpiTestImpl
.
3). 按照规范在META-INF/services/
目录下放置接口名_jvm.classLoader.spi.service.ISpiTest
,内容为_jvm.classLoader.spi.service.impl.SpiTestImpl
实现类的文件。
4). 使用ServiceLoader
加载接口进行测试结构 #
目前有两种实现技术,一种是JDK自带的
java.util.ServiceLoader
,另外一种是sun公司提供的sun.misc.Service
。
下面主要已JDK的进行分析。使用场景 #
JDBC #
JDBC 4.0 开始增加了 Autoloading of JDBC drivers 自动加载驱动的特性。主要使用的是 SPI 以及破坏双亲委派机制的 Thread Context ClassLoader 技术。
Autoloading of JDBC drivers. In earlier versions of JDBC, applications had to manually register drivers before requesting Connections. With JDBC 4.0, applications no longer need to issue a Class.forName() on the driver name; instead, the DriverManager will find an appropriate JDBC driver when the application requests a Connection.新老版本之间的区别就是,不用手动注册驱动了。新版本可以直接通过
DriverManager.getConnection()
获取连接,只要classpath存在合适的驱动包。
比如mysql-connector-java-5.1.34.jar
或者sqljdbc4.jar
。之所以可以这样,是因为:
1). 调用DriverManager.getConnection()
代码会进行DriverManager
类加载。而DriverManager
属于java.sql.包。根据类加载器的特性,这个类会被BootstrapClassLoader
进行加载并且会初始化静态方法。近而调用loadInitialDrivers()
方法。
2). 方法里面分为两部分,一个是获取系统属性jdbc.drivers
的字符串进行:分割,使用Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());
进行加载并 初始化 ,注意使用的是ClassLoader.getSystemClassLoader()
系统类加载器,因为BootstrapClassLoader
本身加载不了。
3). 另外一个是使用SPI进行加载的。使用spi加载的时候由前面介绍的 ServiceLoader初始化第11行 知道。它会使用当前线程的类加载器。一般默认为AppClassLoader
也既系统类加载器。然后查找classpath下面jar包中的META-INF/services/
目录下的java.sql.Driver
的实现。然后使用线程类加载器加载里面定义的类[并没有初始化]c = Class.forName(cn, false, loader);
,获取到类后通过反射S p = service.cast(c.newInstance());
实例化对象并进行 初始化 。
4). 不管上述哪一部分,都是 初始化 调用三方厂商实现的驱动里面的静态方法将自己注册到驱动管理器里面。近而省略手动加载的步骤,实现自动加载。
Reference #
范型&通配符 #
注解 #
语法糖 #
Double colon operator(::) #
双引号,也叫 方法引用操作符 (method reference operator),写法如下:
ClassName::methName
,instanceRef::methName
。
区别:1).
对于实例方法来说,如果使用ClassName::methName
,则必须传递实例对象为第一个泛型参数,如果使用instanceRef::methName
,不需要实例参数,对于泛型来说只有返回值就行。
如person::getAddress
,因为已经绑定对象实例了,也不需要参数,可以理解为引用的是某个实例的方法。
如Person::getAddress
,并没有绑定实例,也就是不知道哪个对象。所以需要传递第一个泛型参数,可以理解为实例化对象才能调用此方法。2).
对于类方法来说,使用ClassName::methName
就可以,因为不需要任何实例,如果有参数和返回会值,补全泛型就行。如果非要使用instanceRef::methName
,
如:person::getAddressStatic
,则会编译错误。虽然在普通语法中,使用实例调用静态方法没问题,但是此处语法糖不允许。一些常见的双冒号
Supplier<Person> person = Person::new;
int[][] array = Arrays.stream(new int[2][2]).filter(it -> it[0] != 0 && it[1] != 0).toArray(int[][]::new);
Consumer<String> consumer = System.out::println;
BiFunction<String,String,Integer> compareTo = String::compareTo;
新特性 #
Reference #