一、类加载基础
1.1.类加载理论介绍
1.1.1、什么是类加载
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型.
1.1.2、类加载过程
类加载机制过程分为以下几步
- 加载类文件
- 连接:验证、准备、解析
- 初始化
- 使用
- 卸载
1.1.2.1、加载类文件
加载阶段完成以下内容
- 根据一个类的全限定名称来获取该类的二进制字节流
- 将这个字节流代表的静态存储结构转换为方法区运行时数据结构
- 在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的信息的访问入口
1.1.2.2、连接
1.1.2.1.1、验证阶段
验证字节流的正确性
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
1.1.2.1.2、准备阶段
该阶段主要为类变量分配内存和设置类变量的初始值
1.1.2.1.3、解析阶段
将符号引用解析为直接引用.
主要包括:
- 类或接口解析
- 字段解析
- 类方法解析
- 接口方法解析
1.1.2.3、初始化
执行方法.
- 方法收集了类变量的赋值和静态代码块,执行顺序,按代码书写顺序
- 方法,在类加载时执行,方法在构造方法执行时执行
- 父类的方法先执行,也就是父类的静态变量赋值、静态代码块执行优于子类
- 接口不会有static代码块,但是也会有静态变量赋值,也会生成方法,但是只有在用到该变量时才会初始化,并且不会去先初始化父接口的静态变量
- 在多线程环境下,虚拟机会保证一个类的方法被正确的加锁、同步,只会执行一次,如果执行很耗时,会造成阻塞
对与接口的例子:
public interface SonInterfaceInit {
String s = StringDefine.define("son a");
}
public class SonInterfaceImpl implements SonInterfaceInit {
private static String son= StringDefine.define("son impl var");
}
类加载过程和初始化过程:
[Loaded jdk.net.ExtendedSocketOptions$PlatformSocketOptions from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded jdk.net.ExtendedSocketOptions$PlatformSocketOptions$1 from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded jdk.net.MacOSXSocketOptions from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
//加载了接口
[Loaded com.source.jvm.data.SonInterfaceInit from file:/Users/dev/workspace-study/source-study/source-jvm/target/classes/]
[Loaded com.source.jvm.data.SonInterfaceImpl from file:/Users/dev/workspace-study/source-study/source-jvm/target/classes/]
[Loaded java.util.Collections$UnmodifiableSet from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded jdk.net.ExtendedSocketOptions$1 from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.Collections$UnmodifiableCollection$1 from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.source.jvm.data.StringDefine from file:/Users/dev/workspace-study/source-study/source-jvm/target/classes/]
[Loaded java.util.HashMap$KeySet from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.HashMap$HashIterator from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.HashMap$KeyIterator from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
//初始化类里面的
接口初始化:son impl var
-----------------------
//初始化接口
接口初始化:son a
[Loaded java.net.StandardSocketOptions from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.net.StandardSocketOptions$StdSocketOption from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.net.NetworkInterface from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
从上面的例子可以看出,当类初始化时,不会去执行接口的方法,也不会去执行父接口的方法.
1.1.3、何时触发类加载
- 当遇到new (new 一个对象)、getstatic(使用类的静态变量)、putstatic(为类的静态变量赋值)、invokestatic(执行类的静态方法)这个四个字节码指令时,会触发
- 当使用反射调用时
- 初始化一个类,发现其父类没有初始化
- 虚拟机启动时,执行指定的主类
- 当使用动态语言支持时
1.1.4、ClassLoader源码
1.1.4.1、双亲委派机制
双亲委派机制:
- 当加载一个类时,首先自己不去尝试加载该类,把加载类饿请求委派给父类加载器去完成
- 当父类加载器无法加载时,才自己尝试去加载该类,如果都无法加载,jvm虚拟机会报java.lang.ClassNotFoundException异常
1.1.4.2、ClassLoader源码
1.1.4.2.1、loadClass
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//获取锁
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//检查当前class是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//去父加载器加载
c = parent.loadClass(name, false);
} else {
//在BootstrapClassLoader 中尝试加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//当前classloader尝试加载
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;
}
}
从上面看出加载一个类的流程:
- 获取加载当前类的锁,保证多线程情况下,加载类是线程安全的
- 检查当前classloader是否已经加载过类
- 如果有父加载器,委托父加载器加载
- 如果没有父加载器,说明当前已经是ExtClassLoader,尝试在BootstrapClassloader下加载
- 如果父加载器没有加载成功,执行findClass方法,当前加载器尝试加载,如果加载不成功抛出java.lang.ClassNotFoundException异常
- 如果resolve为true,解析该类
加载锁,如果parallelLockMap不为null,可以并行加载.否者锁就是当前classloader
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
1.1.4.2.2、findLoadedClass
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
private native final Class<?> findLoadedClass0(String name);
查看是否已经加载过该类,最终调用native方法
1.1.4.2.3、findClass方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
UrlClassLoader实现:
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//根据二进制流获取class
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
1.1.4.3、打破双亲委派模型
1.1.4.3.1、双亲委派模型三次大规模破坏
- jdk1.2 定义了findClass(),自己实现的类加载器,可以通过实现该方法来加载指定位置的类
- JNDI服务的需求,需要加载JNDI接口提供者(SPI)的代码,添加了通过线程Thread类来设置classLoader的方法setContextClassLoader
- 用户对程序动态性的追求而导致,如代码热替换、模块热部署
1.1.4.3.2、破坏双亲委派模型实战
- tomcat classloader设计
- spi原理
接下来详细说明下这两种情况.
二、类加载使用
2.1.SPI机制
SPI是Service Provider Interface 的简称,即服务提供者接口的意思。SPI说白了就是一种扩展机制,我们在相应配置文件中定义好某个接口的实现类,然后再根据这个接口去这个配置文件中加载这个实例类并实例化
2.1.1.Java对SPI的实现
java自带的java.util.ServiceLoader实现了SPI机制.
2.1.1.1.ServiceLoader使用
定义接口
package com.study.jvm.spi;
public interface PayInterface {
/**
* 支付接口
* @param uid
* @param money
* @return
*/
boolean pay(Long uid,long money);
}
定义实现类:
package com.study.jvm.spi;
public class WxPayInterface implements PayInterface {
@Override
public boolean pay(Long uid, long money) {
System.out.println("通过微信支付");
return true;
}
}
package com.study.jvm.spi;
public class ZfbPayInterface implements PayInterface {
@Override
public boolean pay(Long uid, long money) {
System.out.println("通过支付宝支付");
return true;
}
}
配置文件:
路径:META-INF/services/com.study.jvm.spi.PayInterface
com.study.jvm.spi.WxPayInterface
com.study.jvm.spi.ZfbPayInterface
测试:
package com.study.jvm.spi;
import java.util.Iterator;
import java.util.ServiceLoader;
public class SpiTest {
public static void main(String[] args) {
ServiceLoader<PayInterface> serviceLoader = ServiceLoader.load(PayInterface.class);
Iterator<PayInterface> iterator = serviceLoader.iterator();
while (iterator.hasNext()){
PayInterface payInterface = iterator.next();
payInterface.pay(1L,1L);
}
}
}
使用总结:
- 定义需要通过spi实现的接口
- 一般来说实现类与当前接口不在同一个jar,那么在实现类的jar下创建:META-INF/services/目录,并且以接口类权限定名创建文件
- 文件内是当前实现类的权限定名
- 当使用时,需要在当前jar中引用接口所在的jar和实现类所在的jar,然后通过ServiceLoader.load(类)加载实现类到当前classloader中.
2.1.1.2.ServiceLoader原理
先看下ServiceLoader源码
public final class ServiceLoader<S>
implements Iterable<S>
{
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
}
从源码可以看出我们可以通过ServiceLoader ,将META-INF/services/目录下对应接口的实现,加载到给定的ClassLoader中.
为什么要设置ClassLoader.
因为在jvm中,判断两个类是否相同的条件是:
- 全限定名相同
- 由同一个类加载器加载
所以ClassLoader 配合 SPI可以得到以下功能:
我们想象一下下面这个场景:
存在一个接口,但是由于版本更替,存在两个实现类,但是实现类名称相同,某一个时刻,线上需要根据不同的参数,调用其中一个实现类.如何实现.
还以上面的例子,现在加一层代理.具体实现如下:
实现逻辑如下:
获取接口实现类的工厂类
package com.study.jvm.spi;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.ConcurrentHashMap;
public class ZfbPayFactory implements PayInterface{
private final ConcurrentHashMap<String,ZfbPayClassLoader> loaderMap = new ConcurrentHashMap<>(8);
private final static String ZFB_PAY_VERSION_1 = "ZFB_PAY_VERSION_1_CLASS_LOADER";
private final static String ZFB_PAY_VERSION_2 = "ZFB_PAY_VERSION_2_CLASS_LOADER";
@Override
public boolean pay(Long uid, long money) {
String loaderName = uid>10? ZFB_PAY_VERSION_2 : ZFB_PAY_VERSION_1;
ZfbPayClassLoader loader = loaderMap.get(loaderName);
if(loader == null){
synchronized (ZfbPayFactory.class){
if(loaderMap.get(loaderName) == null){
URL[] urls = new URL[1];
String location = getLocation(uid);
try {
urls[0] = new URL("file", "",location);
ZfbPayClassLoader newLoader = new ZfbPayClassLoader(urls,Thread.currentThread().getContextClassLoader());
loaderMap.put(loaderName,newLoader);
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
loader = loaderMap.get(loaderName);
}
try {
Class clazz= loader.loadClass("com.study.jvm.spi.ZfbPayInterface");
PayInterface payInterface = (PayInterface) clazz.newInstance();
payInterface.pay(uid,money);
return true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return false;
}
public String getLocation(Long uid) {
String baseLocation = "/Users/dev/workspace-study/study/jvm/src/main/java/com/study/jvm/spi/";
return baseLocation + (uid>10 ? "version2/" :"version1/");
}
public static void main(String[] args) {
ZfbPayFactory factory = new ZfbPayFactory();
factory.pay(11L,1L);
factory.pay(1L,1L);
}
}
自定义ClassLoader
package com.study.jvm.spi;
import sun.misc.Resource;
import sun.misc.URLClassPath;
import java.io.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
public class ZfbPayClassLoader extends URLClassLoader {
private String[] locations ;
public ZfbPayClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
this.locations = new String[urls.length];
for(int i=0;i<urls.length;i++){
locations[i] = urls[i].getPath();
super.addURL(urls[i]);
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = loadMyClass(name);
if(data == null){
throw new ClassNotFoundException(name);
}
return defineClass(name,data, 0, data.length);
}
private byte[] loadMyClass(String name) {
for(String location : locations){
byte[] data = loadByPath(location,name);
if(data!=null){
return data;
}
}
return null;
}
private byte[] loadByPath(String location, String name) {
InputStream ins = null;
try {
String path =location+name.replace('.', '/').concat(".class");;
ins = new FileInputStream(new File(path));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
几个实现细节,需要注意:
- 代码中有一些变量是写死的,可以放到配置中心
- 如存放实现类的文件夹地址
- 实现类的全限定名
- 根据参数获取ClassLoader名称的转换可以抽象出具体的转换规则.
2.1.2.SpringBoot 对SPI机制的扩展
在SpringBoot中如果想自动引入其他jar的配置文件,并根据这些配置扫描指定路径下的包,将包下的bean注册到当前容器,可以使用SpringBoot提供的SPI机制.
步骤:
- 在你自定义的jar的classpath下创建META-INF目录,并创建spring.factories文件
- 你可以在spring.factories文件中定义org.springframework.boot.autoconfigure.EnableAutoConfiguration=\自定义的@Configuration类
这样spring就会自动加载这些@Configuration类,并加载里面定义的bean,当然你也可以在@Configuration类里面,定义自己的扫描路径,进行扫描Bean.
2.1.3.Tomcat的SPI机制
在Tomcat中提供了一个Common ClassLoader,它主要负责加载Tomcat使用的类和Jar包以及应用通用的一些类和Jar包,例如CATALINA_HOME/lib目录下的所有类和Jar包。
Tomcat会为每个部署的应用创建一个唯一的类加载器,也就是WebApp ClassLoader,它负责加载该应用的WEB-INF/lib目录下的Jar文件以及WEB-INF/classes目录下的Class文件。由于每个应用都有自己的WebApp ClassLoader,这样就可以使不同的Web应用之间相互隔离,彼此之间看不到对方使用的类文件。即使不同项目下的类全限定名有可能相等,也能正常工作。