tips: JVM有8个原子性的指令,分别是:
lock(锁定), read(读取),load(加载),use(使用),
assign(赋值),store(存储),write(写入),unlock(解锁)
Class Loading Linking Initializing
本章主要回顾class加载的完整过程,这个点也是经常容易被提及。
class 加载的过程主要分为三大步:
1. Loading
2. Linking
3. Initializing
这其中 Linking这一步又包含了三个步骤:
1. Verification
2. Preparation
3. Resolution
1.Loading
把一个class文件load到内存装到内存里去,本来是class文件上的一个个二进制,一个个字节,装完之后接下来就是Linking。
2.Linking
-
Verification
是校验装进来class文件是不是否符合class文件的标准,比如class文件里的magic number 不是ca fe ba be那就在这一步被拒绝了。 -
Preparation
把class文件静态变量赋默认值,不是赋初始值。比如static int i =8,这个步骤就是把 i 先赋值成0 -
Resolution
是把class文件常量池里面用到的符号引用,要给他转换为直接内存地址,直接可以访问到内容
3.Initializing
将静态变量赋值成初始值,也就是 static int i = 8 在此时才真正将 8 赋值给 i 。执行静态语句块。
4.类加载器
public class ClassLoaderDemo {
public static void main(String[] args) {
// 获取系统加载器(也叫作应用加载器)
ClassLoader applicationLoader = ClassLoader.getSystemClassLoader();
// 输出 sun.misc.Launcher$AppClassLoader@xxxx
System.out.println(applicationLoader);
// 获取扩展加载器
ClassLoader extensionLoader = applicationLoader.getParent();
// 输出 sun.misc.Launcher$ExtClassLoader@xxxx
System.out.println(extensionLoader);
//获取引导加载器(也叫做启动加载器) BootStrap加载器
ClassLoader bootstrap = extensionLoader.getParent();
//输出null 因为BootStrap 是通过C++实现的,Java里没有对应的class
System.out.println(bootstrap);
// 系统加载器是默认的加载器,一般用户自定义的类都是由这个加载器加载
// 输出 sun.misc.Launcher$AppClassLoader@xxxx
System.out.println(ClassLoaderDemo.class.getClassLoader());
// 输出 null String 是被引导加载器 BootStrap加载的
System.out.println(String.class.getClassLoader());
// 输出 null 因为这个是核心类库,是被引导加载器 BootStrap加载的
System.out.println(sun.awt.HKSCS.class.getClassLoader());
// 输出 sun.misc.Launcher$ExtClassLoader@xxxx 这个类位于 ext 目录下某个jar文件里,所以被扩展加载器加载
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
// 输出 null Ext的classLoader调用它的getClass()本身也是一个class,就这个classLoader的classLoader是被最顶层的引导加载器加载的,所以为null
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader());
}
}
-
引导加载器 BootStrap ClassLoader
由 c++ 实现,是最顶层的加载器,用来加载核心库,并不继承java.lang.ClassLoader,没有父加载器。
当我们调用getClassLoader()返回是null的时候,其实这个加载器就是BootStrap,最顶层的加载器了。 -
扩展加载器 Extension ClassLoader
由Java 实现,扩展加载器,父加载器是 BootStrap ClassLoader,一般用来加载扩展包里的各种各样的文件,扩展包路径在jdk安装目录jre/lib/ext下的jar -
应用加载器(系统加载器)Application ClassLoader
由Java实现,平常最常用的加载器,也叫作系统加载器,用于加载classpath指定目录 -
用户自定义加载器 Custom ClassLoader
自定义加载器,加载自己定义的加载器。
自定义加载器的好处:隔离加载类
修改类加载的方式
扩展加载源
防止源码泄漏自定义加载器实现过程:
- 可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass ()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass ()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,然后调用defineClass()
- 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLCIassLoader类,AppClassLoader和 ExtClassLoader 都是继承URLCIassLoader 。这样就可以避免自己去编写findclass ()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁
// 继承 Class Loader 实现
public class CustomClassLoaderDemo extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getCustomPath("your path"+name);
if (result == null) {
throw new FileNotFoundException();
}
return defineClass(name, result, 0, result.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException();
}
private byte[] getCustomPath(String url) {
// 根据路径读取二进制流的方式读取类到内存中,成为byte数组
// 如果指定字节码已经加密,需要先解密然后读取到内存中
return null;
}
}
// 继承 URLClassLoader实现
public class CustomByUrlClassLoaderDemo extends URLClassLoader {
public CustomByUrlClassLoaderDemo(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public CustomByUrlClassLoaderDemo(URL[] urls) {
super(urls);
}
public CustomByUrlClassLoaderDemo(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
}
Custom ClassLoader 的父加载器是 Application ClassLoader 父加载器是 Extension ClassLoader 父加载器是 BootStrap ClassLoader。四者并不是继承关系。
4.1 双亲委派
定义在ClassLoader.LoadClass()里
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
private final ClassLoader parent;
/**
* ClassLoader类中的静态内部类,他可以决定指定的类是否具备并行的能力,
* 若具备并行能力则ClassLoader下面的parallelLockMap便会被初始化,
* 该静态内部类在ClassLoader被加载的时候便被初始化了
*/
private static class ParallelLoaders {
private ParallelLoaders() {}
//存放具备并行能力的加载器类型的Set集合
private static final Set<Class<? extends ClassLoader>> loaderTypes =
Collections.newSetFromMap(
new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
static {
synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
}
/**
* 将指定的类加载器注册成为一个具备并行能力的类加载器,注册成功则返回true,否则返回false
*/
static boolean register(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
//只有当该类加载器的父类具备并行能力时,该类加载器方可成功注册
if (loaderTypes.contains(c.getSuperclass())) {
loaderTypes.add(c);
return true;
} else {
return false;
}
}
}
/**
* 判断指定的classloader类加载器是否具备并行的能力
*/
static boolean isRegistered(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
return loaderTypes.contains(c);
}
}
}
//若当前类加载器具备并行能力,则该属性会被初始化
private final ConcurrentHashMap<String, Object> parallelLockMap;
/**
* ClassLoader类的构造函数,在这里主要确定该类加载器是
* 否具备并行能力与其他变量的初始化操作
* @param unused
* @param parent
*/
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
//若该类加载器具备并行能力(如何判断见ClassLoader类中的静态内部类)
if (ParallelLoaders.isRegistered(this.getClass())) {
//初始化parallelLockMap
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
//不具备并行能力,parallelLockMap置为null
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
/**
* 使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类:调用findLoadedClass(String)方法
* 检查这个类是否被加载过,使用父加载器调用loadClass(String)方法,如果父加载器为Null,类加载器装载虚拟机
* 内置的加载器调用findClass(String)方法装载类,如果按照以上的步骤成功的找到对应的类,并且该方法接收的resolve
* 参数的值为true,那么就调用resolveClass(Class)方法来处理类,ClassLoader的子类最好覆盖findClass(String)而不是
* 这个方法,除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//这里根据当前类加载器是否具备并行能力而获取对应的锁对象
synchronized (getClassLoadingLock(name)) {
/**
* 在加载类之前先调用findLoadClass方法查找看该类是否加载过,
* 若被加载过则直接返回该对象,无需二次加载,未加载过则返回null,
* 进行下一个流程
* PS:ClassLoader类中的findLoaderClass方法是一个本地方法,
* 自定义的类加载器需要重写该方法
*/
Class c = findLoadedClass(name);
//若该类未被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//如果父类加载器不为空,则调用父类加载器的loadClass方法加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//若父类加载器为空,则调用虚拟机的加载器Bootstrap ClassLoader来加载类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
//若以上的操作都没有成功加载到类
if (c == null) {
long t1 = System.nanoTime();
//则调用该类自己的findClass方法来加载类
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) {
//这个方法用来给ClassLoader链接一个类,解析类:符号引用转变为直接访问地址
resolveClass(c);
}
return c;
}
}
/**
* 这个方法是根据当前类加载器是否具备并行能力而决定是否返回锁对象,
* 当该类加载器不具备并行能力,则无需返回一个加锁对象,若具备并行能
* 力,则返回一个新的加锁对象
*/
protected Object getClassLoadingLock(String className) {
Object lock = this;
//根据parallelLockMap是否被初始化来判断当前类加载器是否具备并行能力
if (parallelLockMap != null) {
//若该类加载器具备并行能力,则创建新的锁对象返回
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
}
任何一个class,加入了你自定义的 Custom ClassLoader,这时候先尝试去Custom ClassLoader里面去找,如果内部维护着缓存,说你有没有,帮我加载进来。如果加载进来了,那么就不需要第二遍加载,如果没有加载进来,那么赶紧把我加载进来。
如果没有在Custom ClassLoader 的缓存里找到的话,并不是直接加载这块内存,会去他的父加载器Application ClassLoader,说爸爸你有没有把这个类加载进来啊,这时候会在 Application ClassLoader 缓存里查找这个class。如果有就返回,如果没有,委托 Application ClassLoader 的父亲 Extension ClassLoader,如果有就返回,如果还是没有 委托他(Extension ClassLoader)的父亲BootStrap ClassLoader,如果有就返回。如果没有,则就往回去委托Extension ClassLoader,说我这里没有,你去加载。 Extension ClassLoader 说 我只负责扩展jar包部分,你的类我找不到,然后就继续往下委托 Application ClassLoader。Application ClassLoader说我只负责加载classpath指定的内容,其他的我找不到啊,然后继续向下委托 Custom ClassLoader去找。
这整个过程经过了一圈,才真正把这个类加载进来,当我们能够吧这个类加载进来的时候叫做成功,如果加载不进来就抛出ClassNotFoundException。这整个过程就叫做双亲委派。
双亲委派的作用:
保护程序安全,防止核心api被随意篡改
避免类的重复加载
tips: 父加载器 并不是 加载器的加载器,也不是加载器的父类,这是一个层级概念。是ClassLoader A 里有一个parent指向另一个ClassLoader B ,我们称这个B就是A的父加载器。
双亲委派 :孩子向父亲的方向,父亲向孩子方向的双亲委派过程。
4.2 打破双亲委派
-
如何打破双亲委派:重写loadClass()
public class MyClassLoader extends ClassLoader{ @Override public Class<?> loadClass(String name) throws ClassNotFoundException { File f = new File("/Users/xikzhang/myProject/jvm/src/main/java/"+ name.replace(".","/").concat(".class")); if (!f.exists()) return super.loadClass(name); try{ InputStream is = new FileInputStream(f); byte[] b = new byte[is.available()]; is.read(b); return defineClass(name,b,0,b.length); }catch (IOException e){ e.printStackTrace(); } return super.loadClass(name); } public static void main(String[] args) throws Exception { String name = "JavaClassView"; MyClassLoader m = new MyClassLoader(); Class clazz = m.loadClass(name); m = new MyClassLoader(); Class clazzNew = m.loadClass(name); //false System.out.println(clazz == clazzNew); Class clazzBySystem = MyClassLoader.class.getClassLoader().loadClass(name); Class clazzNewBySystem = MyClassLoader.class.getClassLoader().loadClass(name); //true System.out.println(clazzBySystem == clazzNewBySystem); } }
-
什么时候打破双亲委派机制?
- 在JDK1.2 之前,自定义ClassLoader都必须重写 loadClass()
- ThreadContextClassLoader可以实现基础类调用实现类代码,thread.setContextClassLoader
- 模块化的时候使用热启动,热部署
osgi tomcat有自己的模块指定classLoader(可以在同一类库的不同版本)两个classLoader都可以load进同名的类
4.3 类加载器的范围
Launcher就是ClassLoader一个包装类启动类。Launcher类里已经定义好了来加载器的范围。
-
BootStrap ClassLoader 加载路径
sun.boot.class.path -
Extension ClassLoader 加载路径
java.ext.dirs -
AppClassLoader 加载路径
java.class.path
5. LazyLoading
懒初始化也叫作懒加载,JVM规范并没有规定何时加载,JVM虚拟机的实现都是用的懒加载,就是什么时候需要用就把这个类什么时候才去加载,并没有说一个jar文件里几百个类,只用到一个类就全部加载进来,这是没啥意义的。
JVM 严格规定了什么时候必须初始化
- new getstatic putstatic invokestatic 指令,访问final变量除外
- java.lang.reflect对类进行反射调用时
- 初始化子类的时候,父类首先初始化
- 虚拟机启动时,被执行的主类必须初始化
- 动态语言支持java.lang,invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化
6.编译器
Java是解释执行的,一个class文件load到内存之后通过Java的解释器interpreter来执行。JIT(Juest In-Time)的编译器指的是有某些代码我们需要把它编译成本地代码,相当于exe。
- 解释器 bytecode interpreter
- JIT Juest In-Time compiler
混合模式:解释器 + 热点代码编译
什么是热点代码编译,就是有一段代码,这个代码里有循环或者一个方法在执行的时候刚开始使用解释器执行,结果后来发现整个执行过程中有段方法或代码执行频率特别高,就会把这段代码编译成本地代码,那么之后在运行这段代码就不需要通过解释器,转而执行本地代码,提高效率。整个模式就是叫做混合模式。
- 起初使用解释执行
- 热点代码检测
- 多次被调用的方法(方法计数器:检测方法执行的频率)
- 多次被调用的循环(循环计数器:检测循环执行的频率)
- 进行编译
tips: 既然即时编译JIT效率高,为什么不将代码全部执行成本地代码,从而提高执行效率? 因为现在Java的解释器已经效率很高,在某些场景下可以媲美JIT,其次如果一个类中调用很多的其他类,而把这个类全部编译成本地代码,那么在编译启动会花费巨大的时间,得不偿失。
可以通过命令参数的形式指定编译类型:
- Xmixed 默认为混合模式
开始解释执行,启动速度快,对热点代码实行检测和编译 - Xint 使用解释执行
启动很快,执行稍慢 - Xcomp 使用纯编译模式
执行很快,启动很慢
小结
这一章主要回顾了下Loading的主要的内容
-
ClassLoading使用了双亲委派机制:从下到上(孩子往父解释器)向上找,再从上到下(父亲委托孩子)向下委托,主要处于安全考虑
-
LazyLoading 五种情况
-
ClassLoader源码
findCache() -> parent.LoadClass() -> findClass() -
自定义加载器
- extend ClassLoader ; overwrite findClass() -> defineClass(byte[] -> Class clazz)
- extend URLClassLoader
-
在自定义的加载器里做加密
-
混合执行 解释执行 + 热点代码编译(JIT编译执行)
评论区