前言
小编最近在写精通mybatis的博客,大家有兴趣的可以点开来看一下,如果认为写的可以的话记得三连啊。
解析阶段补充
上次小编讲了类加载机制,那今天稍微补充一下解析阶段
1、解析时机:一般做初始化的时候去解析
2、解析什么: 只要是直接引用都需要解析,方法,接口,类,字段
3、如何避免重复解析:利用缓存,ConstantPoolCache运行时常量池,底层是hashtable,解析之前判断是否已经解析解析完毕后存入缓存
静态变量是如何存储的,大家先看下问题:
public class Test{
public static void main(String[] args) {
System.out.printf(TestB.str);
}
}
class TestA {
public static String str = "A str";
static {
System.out.println("A Static Block");
}
}
class TestB extends TestA {
static {
System.out.println("B Static Block");
}
}
这边会打印什么呢,答案是
A Static Block
A str
首先静态属性是存储在堆区的 ,静态属性的访问: 1、去缓存去找,如果有直接返回 2、如果没有就触发解析 。其底层实现:1、找到直接引用 2、存储到常量池缓存中,储存的结构是:key为常量池的索引 ,value为ConstantPoolCacheEntry,上面就是将String包装成ConstantPoolCacheEntry。
类加载器
包含启动类加载器,扩展类加载器,应用类加载器,自定义加载器。JVM中有两种类型的类加载器,由C++编写的以及由Java编写的。除了启动类加载器(Bootstrap Class Loader)是由C++编写的,其他都是由Java编写的。由Java编写的类加载器都继承⾃类java.lang.ClassLoader。根类加载器通过java代码打印为null,是因为C++编写所以没有能表示的java代码,看源码时扩展类加载器传入父类加载器的时候就传入null。又因为根类加载器为null,所以无法被java程序所调用。
各种类加载器之间存在着逻辑上的⽗⼦关系,但不是真正意义上的⽗⼦关系,因为它们直接没有从属关系。类加载器加载的jar的范围以及双亲委派如下图所示:
启动类加载器
启动类加载器不像其他类加载器有实体,它是没有实体的,JVM将C++处理类加载的⼀套逻辑定义为启动类加载器。
查看启动类加载器的加载路径:
public static void main(String[] args) {
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL);
}
}
也可以通过-Xbootclasspath指定路径
扩展类加载器
查看类加载器加载的路径:
public static void main(String[] args) {
ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();
URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
URL[] urls = urlClassLoader.getURLs();
for (URL url : urls) {
System.out.println(url);
}
}
可以通过java.ext.dirs指定
应用类加载器
默认加载⽤户程序的类加载器 查看类加载器加载的路径:
public static void main(String[] args) {
String[] urls = System.getProperty("java.class.path").split(":");
for (String url : urls) {
System.out.println(url);
}
System.out.println("================================");
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
URL[] urls1 = classLoader.getURLs();
for (URL url : urls1) {
System.out.println(url);
}
}
可以通过java.class.path指定
大家都知道main方法是应用类加载器加载的,但是明明是jvm调用为什么不是根类加载器而是应用类加载器:其实启动类加载器做的事情 是:加载类sun.launcher.LauncherHelper,执行该类的方法checkAndLoadMain……启动类、扩展类、 应用类加载器逻辑上的⽗⼦关系就是在这个方法的调用链中生成的。
类加载器加载的类如何存储
上一篇博客中讲到klass模型存储在元空间中,其实各自加载器加载的类都在元空间中开辟一个空间,然后空间内储存自己加载的类信息。如下图所示:
双亲委派
如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。同时避免了类的重复加载。
源码阅读
java.lang.ClassLoader#loadClass
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
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) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
打破双亲委派
在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服 务商来提供,比如mysql的就写了 MySQL Connector ,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载, 只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派。 类似这样的情况就需要打破双亲委派。 打破双亲委派的意思其实就是不委派、向下委派。
源码阅读
java.sql.DriverManager#loadInitialDrivers
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
下面就用到了spi。待会儿小编在spi中写个示例大家就明白了
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
SPI
是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件中所定义的类。这种机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。其实打破双亲委派机制也使用了SPI。
示例代码
pay-center
public class PayMethodTest {
public static void main(String[] args) {
ServiceLoader<PayService> load = ServiceLoader.load(PayService.class);
for (PayService payService : load) {
payService.pay();
}
}
}
pom
<dependencies>
<dependency>
<groupId>xxx.xxx.xxx</groupId>
<artifactId>pay-common</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>xxx.xxx.xxx</groupId>
<artifactId>pay-ali</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>xxx.xxx.xxx</groupId>
<artifactId>pay-wx</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
pay-common
共同引入的jar包
public interface PayService {
/**
* 支付方法
*/
void pay();
}
pay-ali
public class AliPayService implements PayService {
@Override
public void pay() {
System.out.println("支付宝支付");
}
}
配置文件,前面为接口的包路径xxx.xxx.xxx
内容为实现类的路径:
xxx.xxx.xxx.AliPayService
这样就是spi的用法了
反射底层原理
这边小编简单说明一下。反射forName,getField,getMethod原理,大家其实知道了底层如何存储的,那基本也就知道它是如何获取的,类的存储,底层叫Directory存储是hashtable,key是根据类全限定名加类加载器计算得出index,然后value为klass模型。然后反射的时候根据类全限定名以及类加载器计算index然后我们就可以找到klass模型,klass模型中什么都有了(还需要找到他的字段和方法),之后可以根据权限拿到对应的值即可。
总结
今天小编主要补充了解析阶段,然后是类加载器,加载类的存储以及双亲委派打破双亲委派,反射底层原理。小编这边其实还没完整说明,类加载器的整个流程,也没说明源码,其实这边需要大家搭建一个openjdk的环境,启动类加载器在hotspot里面就是一个逻辑块命名为classLoader,里面的方法都是静态方法,然后是类加载器创建,创建链中何时赋值线程上下文类加载器等等。小伙伴得自己看代码然后总结。加油努力吧。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/13561.html