JVM类装载体系结构
本文章涉及的代码在github上:https://github.com/singgel/eight-sorting-algorithms/tree/master/src/test/java/com/hks/eightsortingalgorithms/classLoaderJVM的class转载方式java的class只在需要的时候才内转载入内存,并由java虚拟机的执行引擎来执行,而执行...
本文章涉及的代码在github上:https://github.com/singgel/eight-sorting-algorithms/tree/master/src/test/java/com/hks/eightsortingalgorithms/classLoader
JVM的class转载方式
java的class只在需要的时候才内转载入内存,并由java虚拟机的执行引擎来执行,而执行引擎从总的来说主要的执行方式分为四种,
第一种,一次性解释代码,也就是当字节码转载到内存后,每次需要都会重新的解析一次,
第二种,即时解析,也就是转载到内存的字节码会被解析成本地机器码,并缓存起来以提高重用性,但是比较耗内存,
第三种,自适应优化解析,即将java将使用最贫乏的代码编译成本地机器码,而使用不贫乏的则保持字节码不变,一个自适应的优化器可以使得java虚拟机在80%-90%的时间里执行优化过的本地代码,而只需要执行10%-20%对性能有影响的代码。
第四种,一种能够利用本地方法直接解析java字节码的芯片。
java类装载器的体系结构
java的类装载器从三方面对java的沙箱起作用:
1.它防止恶意的代码区干扰善意的代码
怎么理解这句话,不同的类装载器装入同样的类的时候会产生一个唯一的命名空间,java虚拟机维护着这些命名空间,同一类,一个命名空间只能装载一次,也只会装载一次,不同命名空间之间的类就如同各自有一个防护罩,感觉不到彼此的存在,如下图3-1所示
2.它守护了被信任的类库边界
这里有两个需要理解的概念,一,双亲委托模式,二运行时包,java虚拟机通过这两个方面来界定类库的边界
什么是双亲委托模式
先来看一个图和一段代码
这个图说明了类装载的过程,但是光这么看还是没有那么的清晰,我们只知道虚拟机启动的时候会启动bootStrapClassLoader,它负责加载java的核心API,然后bootStrapClassLoader会装载
Launcher.java 之中的 ExtClassLoader(扩展类装载器),并设定其 Parent 为 null ,代表其父加载器为 BootstrapLoaderExtClassLoader再有ExtClassLoader去装载ext下的拓展类库,然后 Bootstrap Loader 再要求加载 Launcher.java 之中的 AppClassLoader(用户自定义类装载器) ,并设定其 Parent 为之前产生的 ExtClassLoader 实体。这两个加载器都是以静态类的形式存在的,下面我们找到java.lang.ClassLoader的loadClass这个方法
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
这个方法告诉我们双亲委托模式的过程,当虚拟机去装载一个类的时候会先调用一个叫loadClass的方法,接着在这个方法里它会先调用findLoadedClass来判断要装载的类字节码是否已经转入了内存,如果没有的话,它会找到它的parent(这里的parent指装载自己的那个类加载器,一般我们的应用程序类的parent是AppClassLoader),然后调用parent的loadClass,重复自己loadClass的过程,如果parent没有装载过着这个类,就调用findBootstrapClass(这里是指bootStrap,启动装载器)来尝试装载这个类的字节码,如果bootStrap也没有办法装载这个类,则调用自己的findClass来尝试装载这个类,如果还是没办法装载则抛出异常。
上面就是对双亲模式的简单描述,那么双亲委托描述有什么好处?
你尝试一下自己写个java.lang.String的类,然后在ecplise跑一下,有没有发现抛出了异常,来看看这个异常
java.lang.NoSuchMethodError: main
运行这个我们自己定义的类的java.lang.String的双亲委托模式加载过程如下AppClassLoader -> ExtClassLoader -> BootstrapLoader,由于BootstrapLoader只会加载核心API里的类,它匹配到核心API(JAVA_HOME\jre\lib)里的String类,所以它以为找到了这个类就直接去寻找核心API里的String类里的main函数,所以就抛出异常了,而我们自己写的那个String根本就没有机会被加载入内存,这就防止了我们自己写的类对java核心代码的破坏。
什么是运行时包
要了解运行时包,我们先来设想一个问题,如果你自己定义了一个java.lang.A的类,能不能访问到java.lang.String类的friend成员?
不行,为什么?这就是运行时包在起作用,java的语法规定,包访问权限的成员能够被同一个包下的类访问,那是为什么不能够访问呢,这同样是为了防止病毒代码的破坏,java虚拟机只允许由同一个类装载器装载到同一包中的类型互相访问,而由同一类装载器装载,属于同一个包的,多个类型的集合就是我们所指的运行时包了。
3.将代码归入某类(保护域),该类确定了代码能够执行那些操作
除了1.屏蔽不同的命名空间,2.保护信任类库的边界外,类装载器的第三个重要的作用就是保护域,类装载器必须把代码放入到保护域中以限定这些代码运行时能够执行的操作的权限,这也如我上面讲的,像一个监狱一样,不让它在监狱意外的范围活动。
class文件 校验器
保证class文件内容有正确的内部结构,java虚拟机的class文件检验器在字节码执行之前对文件进行校验,而不是在执行中进行校验
class文件校验器要进行四趟独立的扫描来完成校验工作
class文件校验器分成四趟独立的扫描来完成校验。
第一趟扫描,在类被装载时进行,校验class文件的内部结构,保证能够被正常安全的编译
第二趟和第三趟在连接的过程中进行,这两趟基本上是语法校验,词法校验
第四趟是解析符号引用和直接引用时进行的,这次校验确认被引用的类,字段以及方法确实存在
实践写自己的类装载器
下面我们先来动态扩展一个类装载器,当然这只是一个比较小的demo,旨在让大家有个比较形象的概念。
第一步,首先定义自己的类装载器,从ClassLoader继承,重写它的findClass方法,至于为什么要这么做,大家如果看过笔记三就知道,双亲委托模式下,如果parent没办法loadClass,bootStrap也没把办法loadClass的时候,jvm是会调用ClassLoader对象或者它子类对象的findClass来装载。
package com.hks.eightsortingalgorithms.classLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* 第一步,首先定义自己的类装载器,从ClassLoader继承,重写它的findClass方法,
* 至于为什么要这么做,大家如果看过笔记三就知道,双亲委托模式下,
* 如果parent没办法loadClass,bootStrap也没把办法loadClass的时候,jvm是会调用ClassLoader对象或者它子类对象的findClass来装载。
*/
public class MyClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = getByteArray(name);
if (data == null) {
throw new ClassNotFoundException();
}
return defineClass(name, data, 0, data.length);
}
private byte[] getByteArray(String name){
String filePath = name.replace(".", File.separator);
byte[] buf = null;
try {
FileInputStream in = new FileInputStream(filePath);
buf = new byte[in.available()];
in.read(buf);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return buf;
}
}
第二步,定义一个类,专门用于被装载,这里我们定义了一个静态代码块,待会用到它
package com.hks.eightsortingalgorithms.classLoader;
/**
* 第二步,定义一个类,专门用于被装载,这里我们定义了一个静态代码块,待会用到它
*/
public class TestBeLoader {
static{
System.out.println("TestBeLoader init");
}
public void sayHello(){
System.out.println("hello");
}
}
第三步,定义一个有main函数入口的public类来做验证
package com.hks.eightsortingalgorithms.classLoader;
/**
* 第三步,定义一个有main函数入口的public类来做验证
*/
public class TestClassLoaderDemo {
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
Class thisCls = TestClassLoaderDemo.class;
MyClassLoader myClassLoader = new MyClassLoader();
System.out.println(thisCls.getClassLoader());
System.out.println(myClassLoader.getParent());
try {
//用自定义的类装载器来装载类,这是动态扩展的一种途径
Class cls2 = myClassLoader.loadClass("com.hks.eightsortingalgorithms.classLoader.TestBeLoader");
System.out.println(cls2.getClassLoader());
TestBeLoader test=(TestBeLoader)cls2.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 第四步,查看运行结果
*/
}
第四步,查看运行结果
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
TestBeLoader init
说明:
第一个输出:装载TestClassLoaderDemo的类是AppClassLoder
第二个输出:装载myClassLoader的装载器也是AppClassLoader,这里在同个线程中,动态连接模式会运用当前线程的类加载器来加载所需的class文件,因为第一个和第二个输出是同一个对象的对象名
第三个输出:是TestBeLoader的类加载器,这个输出验证了,双亲委托模式下的动态连接模式,由于myClassLoader是由AppClassLoader装载的,所以它会委托自己的parent来装载com.yfq.test.TestBeLoader这个类,加载成功所以就不再调用自己的findClass方法。
第四个输出:如果我们将TestBeLoader test=(TestBeLoader)cls2.newInstance();这句话注掉,则不会有第四个输出,为什么?
类的装载大致分为三步,装载,连接,初始化。而初始化这一步,是在我们第一次创建对象的时候才进行初始化分配内存,这一点需要注意,并不是class被load内存后就立刻初始化。
更多推荐
所有评论(0)