咱们知道java要运行须要编译和运行,javac将java源代码编译为class文件。而虚拟机把描述类的数据从class文件中加载到内存,并对数据进行校验、转换解析、初始化,最终造成能够被虚拟机直接使用的java类型,这就是类加载机制,他在运行期间完成。java
JVM加载class文件到内存有两种方式:web
以前的我只知道在对象建立以前会先初始化静态的东西,也知道从父类开始初始化,但一直不懂为何会是这样的顺序,直到我了解了虚拟机是如何实现类加载的。在开始真正了解类加载以前,咱们先来看三个例子。数组
class SuperClass {
static{
System.out.println("SuperClass Init");
}
public static int value = 123;
}
class SubClass extends SuperClass{
static{
System.out.println("SubClass Init");
}
}
public class NotInitialization{
public static void main(String agrs[]){
System.out.println(SubClass.value);
}
}
复制代码
输出:tomcat
SuperClass Init
123
复制代码
这道例子彷佛很简单,他告诉咱们对于静态字段,只有直接定义这个字段的类才会被初始化,因此,即便这里是经过子类来引用父类的静态属性,他也不会使子类发生初始化,而至于加载和验证,虚拟机并无明确规范,各步骤的做用下文会谈安全
class SuperClass {
static{
System.out.println("SuperClass Init");
}
public static int value = 123;
}
class SubClass extends SuperClass{
static{
System.out.println("SubClass Init");
}
}
public class NotInitialization{
public static void main(String agrs[]){
SuperClass[] sca = new SuperClass[10];
}
}
复制代码
输出:bash
//无输出
复制代码
是的,运行以后并无输出,但他触发了一个叫“[Lorg.fenixsoft.classloading.SuperClass”的类初始化,而建立动做由字节码指令newarray触发,从这里,咱们也就直到建立一个对象数组的真实状况了服务器
class ConstClass{
static{
System.out.println("ConstClass init");
}
public static final String WORD = "Hello";
}
public class NotInitialization{
public static void main(String agrs[]){
System.out.println(ConstClass.WORD);
}
}
复制代码
输出:网络
Hello
复制代码
这里WORD做为一个常量,他在编译阶段就已经生成,意思是说编译阶段通过常量传播优化,已经将他存储到了NotInitialization类的常量池中,之后全部对它的引用都是NotInitialization对常量池的引用,这就是为何不初始化类。数据结构
下面来总结一下五种必须对类初始化的状况:多线程
以上,都是类第一次发生初始化的状况,而对于接口的初始化,他和类的不一样就是只有在真正使用到父接口的时候才会初始化父接口。
下面来具体看一下类加载的全过程分别要作哪些事情
这个时期须要完成三件事:
这里,非数组类的加载阶段和数组类有些不一样:
说直白加载的做用就是找到.class文件并把这个文件包含的字节码读取到内存中
这一步的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全,大概分为四部验证
为类变量分配内存并设置类变量初始化值,在方法区进行分配,如int为0,boolean为false,reference为null
将常量池内的符号引用替换为直接引用的过程
问,什么是符号引用,什么是直接引用?
符号引用就是一个字符串,这个字符串有足够的信息能够找到相应的位置。直接引用就是偏移量,经过偏移量能够直接在内存区域找到方法字节码的起始位置。
解析主要包括对类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符这些符号引用进行
在类中包含的静态初始化器都被执行,在这一阶段末尾静态字段被初始化为默认值,初始化遵照下面几条原则(其中是类初始化的字节码指令)
public class Test {
static {
i = 0;
//System.out.println(i);
}
static int i;
}
复制代码
上面注释的那一行会报错,由于在静态初始化块中只能访问到定义在静态语句块以前的变量;定义在他以后的变量,在前面的静态语句块能够赋值,不能访问,说明了第一条
public class Test {
static class DeadLoopClass{
static{
if (true){
System.out.println(Thread.currentThread() + "init DeadLoopClass");
while(true){
}
}
}
}
public static void main(String agrs[]){
Runnable script = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + "start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + "run over");
}
};
Thread t1 = new Thread(script);
Thread t2 = new Thread(script);
t1.start();
t2.start();
}
}
复制代码
输出
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]init DeadLoopClass
复制代码
他会打印上面的语句并会发生阻塞,这个例子说明了初始化的时候会保证类会被正确加锁
接下来咱们具体看一下类加载器有哪些特色,它的做用就是动态加载类到Java虚拟机的内存空间中,就是上文说的“经过一个类的全限定名来获取描述此类的二进制字节流”,而且这个动做是放到Java虚拟机外部实现的,就是说应用程序本身决定如何去获取须要的类
在JVM中标识两个class对象是否为同一个类对象存在两个必要条件
一个应用程序老是由n多个类组成,Java程序启动时,并非一次把全部的类所有加载后再运行,它老是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销
类加载器能够大体分为三类:
若是一个类加载器收到了类加载请求,它并不会本身先去加载,而是把这个请求委托给父类的加载器去执行,若是父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,若是父类加载器能够完成类加载任务,就成功返回,假若父类加载器没法完成此加载任务,子加载器才会尝试本身去加载,这就是双亲委派模式。
注意,这里叫双亲不是由于继承关系而是组合关系
很容易想到,双亲委派模型的层级能够避免重复加载,尤为是java的核心类库不会被替换,例如本身定义了一个java.lang.Integer,双亲委派模型不会去初始化他,而是直接返回加载过的Integer.class。固然,若是强行用defineClass()方法(这个方法将byte字节流解析成JVM可以识别的Class对象)去加载java.lang开头的类也不会成功,会抛出安全异常
ClassLoader的loadClass(),只列出了关键的
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null){
try{
if (parent != null){
c = parent.loadClass(name,false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e){
//若是父类加载器抛出ClassNotFoundException,说明父类加载器没法完成加载请求
}
if (c == null){
//在父类加载器没法加载的时候
//再调用自己的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve){
//使用类的Class对象建立完成也同时被解析
resolveClass(c);
}
return c;
}
复制代码
ClassLoader的findClass(),
//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
复制代码
ClassLoader的defineClass
protected Class<?> findClass(String name) throws ClassNotFoundException {
//获取类的class文件字节数组
byte[] classData = getClassData(name);
if (classData == null){
throw new ClassNotFoundException();
} else {
//直接生成class对象
return defineClass(name,classData,0,classData.length);
}
}
复制代码
ClassLoader的resolveClass()
protected final void resolveClass(Class<?> c) {
if (c == null) {
throw new NullPointerException();
}
}
复制代码
下面再来看一下关键方法的具体做用:
先看如下loadClass()方法,经过以上代码能够看到逻辑并不复杂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass(),若父加载器为空让启动类加载器为父加载器,若父类加载失败,抛出异常,再调用本身的findClass()方法
在JDK1.2以后,若是咱们自定义类加载器的话咱们将再也不重写loadClass(),由于ClassLoader已经实现loadClass(),而且用它来达到双亲委派的效果。咱们自定义类加载器须要重写的是findClass(),知道findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用本身的findClass()方法来完成类加载,这样就能够保证自定义的类加载器也符合双亲委托模式。
双亲委派模型不是一个强制性的约束模型,双亲委派模型也有不太适用的时候,这时根据具体的状况咱们就要破坏这种机制,双亲委派模型主要出现过三次被破坏的状况
由于双亲委派模型是在JDK1.2的时候出现的,因此,在JDK1.2以前,是没有双亲委派的,为了向前兼容,JDK1.2以后的java.lang.ClassLoader添加了一个新的protected的findClass()方法,这个方法的惟一逻辑就是调用本身的loadClass(),前文分析代码实现的时候咱们知道双亲委派模型就是根据loadClass()来实现的,因此为了使用双亲委派模型,咱们应当把本身的类加载逻辑写道findClass()中。
咱们有一些功能是java提供接口,而其余的公司提供实现类,例如咱们的JDBC、JNDI(由多个公司提供本身的实现)因此像JDBC、JNDI这样的SPI(服务提供者接口),就须要第三方实现,这些SPI的接口属于核心库,由Bootstrap类加载器加载,那么如何去加载那些公司提供的实现类呢?这就是咱们的线程上下文类加载器,下图是总体大概的工做流程
第三次破坏委派双亲模型就是因为用户追求动态性致使的,“动态性”就是指代码热替换、模块热部署等,就是但愿程序不须要重启就能够更新class文件,最典型的例子就是SpringBoot的热部署和OSGi。这里拿OSGi举例,OSGi实现模块化热部署的关键就是它自定义类加载机制的实现,每个程序模块(OSGi中称为Bundle)都有本身的类加载器,当须要更换一个Bundle时,就把Bundle连同类加载器一块儿换掉实现热部署
因此,在OSGi环境下,类加载器再也不是层次模型,而是网状模型,如图
当OSGi收到一个类加载的时候会按照如下的顺序进行搜索:
以上前两点仍符合双亲委派规则,其他都是平级类加载器查找
前文咱们了解了Java中类加载器的运行方式;但主流的Web服务器都会有本身的一套类加载器,为何呢?由于对于服务器来讲他要本身解决一些问题:
显然,若是Tomcat使用默认的类加载机制是没法知足上述要求的
CommonClassLoader能加载的类均可以被Catalina ClassLoader和SharedClassLoader使用,而CatalinaClassLoader和Shared ClassLoader本身能加载的类则与对方相互隔离。WebAppClassLoader可使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并经过再创建一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
Tomcat 6.x把/common、/server和/shared三个目录默认合并到一块儿变成一个/lib目录,这个目录里的类库至关于之前/common目录中类库的做用
如今咱们再来看Tomcat时如何解决以前的四个问题的:
前文咱们说过破坏委托模型,这里就是一个例子,能够采用线程上下文加载器,让父类加载器请求子类加载器完成加载类做用
这个错误是说当JVM加载指定文件的字节码到内存时,找不到相应的字节码。解决办法为在当前classpath目录下找有没有指定文件(this.getClass().getClassLoader().getResource("").toString()能够查看当前classpath)
这种错误出现的状况就是使用了new关键字、属性引用某个类、继承某个接口或实现某个类或某个方法参数引用了某个类,这时虚拟机隐式加载这些类发现这些类不存在的异常。解决这个错误的办法就是确保每一个类引用的类都在当前的classpath下面
多是在JVM启动的时候不当心在JVM中的某个lib删了
没法转型,这个可能对于初学者来讲会很常见(好比说我,哈哈),解决办法时转型前先用instanceof检查是否是目标类型再转换
这个异常是因为类加载过程当中静态块初始化过程失败所致使的。因为它出如今负责启动程序的主线程中,所以你最好从主类中开始分析,这里说的主类是指你在命令行参数中指定的那个,或者说是你声明了public static void main(String args[])方法的那个类。这个异常很大可能会伴随NoClassDefFoundError,因此出现NoClassDefFoundError时咱们先看ExceptionInInitializerError出现没。
接下来咱们要本身写一个类加载器,在开始写以前,咱们要知道为何须要咱们本身写类加载器呢?
下面咱们开始自定义类加载器吧
package SelfClassLoader;
import java.io.*;
public class FileClassLoader extends ClassLoader {
private String rootDir;
public FileClassLoader(String rootDir){
this.rootDir = rootDir;
}
/**
* 编写findClass方法的逻辑
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//获取类的class文件字节数组
byte[] classData = getClassData(name);
if (classData == null){
throw new ClassNotFoundException();
} else {
//直接生成class对象
return defineClass(name,classData,0,classData.length);
}
}
/**
* 编写获取class文件并转换为字节码流的逻辑
* @param className
* @return
*/
private byte[] getClassData(String className){
//读取类文件的字节
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int bytesNumRead = 0;
// 读取类文件的字节码
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 类文件的完整路径
* @param className
* @return
*/
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
/**
* 读取文件
*/
public static void main(String[] args) throws ClassNotFoundException {
String rootDir="C:\\java\\JVM\\JVMInstruction\\src";
//建立自定义文件类加载器
FileClassLoader loader = new FileClassLoader(rootDir);
try {
//加载指定的class文件,加上包名
Class<?> object1=loader.loadClass("SelfClassLoader.DemoObj");
System.out.println(object1.newInstance().toString());
//输出结果:I am DemoObj
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
咱们经过getClassData()方法找到class文件并转换为字节流,并重写findClass()方法,利用defineClass()方法建立了类的class对象。在main方法中调用了loadClass()方法加载指定路径下的class文件,因为启动类加载器、拓展类加载器以及系统类加载器都没法在其路径下找到该类,所以最终将有自定义类加载器加载,即调用findClass()方法进行加载。
还有一种方式是继承URLClassLoader类,而后设置自定义路径的URL来加载URL下的类,这种方式更常见
package SelfClassLoader;
import java.io.File;
import java.net.*;
public class PathClassLoader extends URLClassLoader {
private String packageName = "net.lijunfeng.classloader";
public PathClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public PathClassLoader(URL[] urls) {
super(urls);
}
public PathClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
protected Class<?> findClass(String name) throws ClassNotFoundException{
Class<?> aClass = findLoadedClass(name);
if (aClass != null){
return aClass;
}
if (!packageName.startsWith(name)){
return super.loadClass(name);
} else {
return findClass(name);
}
}
public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
String rootDir="C:\\java\\JVM\\JVMInstruction\\src";
//建立自定义文件类加载器
File file = new File(rootDir);
//File to URI
URI uri=file.toURI();
URL[] urls={uri.toURL()};
PathClassLoader loader = new PathClassLoader(urls);
try {
//加载指定的class文件
Class<?> object1=loader.loadClass("SelfClassLoader.DemoObj");
System.out.println(object1.newInstance().toString());
//输出结果:I am DemoObj
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码