Class Loading Linking Initalizing
有一个class文件,它默默躺在硬盘上,需要经过一个什么样的过程才能到内存里准备好呢?
class怎么进入内存分为三大步:
- 第一步Loading
把一个class文件通过ClassLoader读取到内存,他本来是一个class文件上的一个一个的二进制,一个一个字节,装完之后就是接下来的Linking - 第二步Linking,分为三小步:
- Verification
是校验装进来的class文件是不是符合class文件的标准,假如你装进来的不是这个CA FA BA BE,在这个步骤就被拒接掉了 - Preparation
是把class文件静态变量赋默认值,不是初始值,比如你static int i =8,注意在这个步骤8并不是在把i值赋值成8,而是先赋值为0 - Resolution
把class文件常量池的符号引用转换成直接引用(内存地址)
- Verification
- 第三步Initlalizing
调用类初始化代码,给静态成员变量赋初始值
- 假如有一个成员变量private int m = 8;要想用起来的话是需要new 对象的,这点比较简单,这个new对象的过程分为2步,第一步是给这个对象申请内存的过程,申请完内存之后它里面的变量先赋默认值.new T再来分解的话他实际上中间也是两步,第一步是new出来T这个内存,new出来之后里面这个成员变量还没赋值呢,他默认值为0,下一步他才会调用构造方法,调用构造方法之后才会赋初始值变成8.
load-默认值-初始值
new-申请内存-默认值-初始值
类的加载器
- 我们来看这个类的加载器的内容,首先第一点JVM它本身有一个类加载器的层次,这个类的加载器本身就是一个普通的class,jvm有一个类加载器的层次分别来加载不同的class,jvm索引的class都是被类加载器加载到内存的,那么这个类加载器可以叫做ClassLoader.
- 每一个class在java虚拟机里面到内存里面任何一个class都是被ClassLoader load内存的,那么这个ClassLoader其实就是顶级有一个父类,这个父类就叫ClassLoader,他是一个abstract抽象类,相当于这个类是被谁领到内存里去了,他一定是ClassLoader这个类的子类,如果你想知道你的class是被谁弄到内存里的,其实很简单就是下面代码
System.out.println(类名.class.getClassLoader) - 一个class文件平时躺在硬盘上,这个内存被load到内存之后,创建了两块内容,第一块内容把class二进制扔到了内存中,第二块内容是于此生成一个class类对象,这一块对象指向第一块内容
- 以前用过反射的同学应该知道,我们通过class这个对象去拿他的有哪些方法甚至方法可以调用,分析一下执行反射的过程中,他一定是那些方法的信息存在这个对象里,然后真让这个方法执行的时候,他一定会去找那些class文件里面的二进制码,翻译成java指令一步一步来执行
- 类加载器层次
类加载器加载过程,加载过程分为不同的层次来加载,不同类的加载器来加载不同的class文件
- 第一个类加载器层次
最顶层的是BootStrap(c语言实现),他是用来加载lib里jdk最核心的内容,比如说rt.jar,charset.jar等核心类,当我们调用getClassLoader获取到null值的时候,代表已经达到了最顶层的加载器 - 第二个类加载器层次
这个是Extension加载器扩展类,加载扩展包里各种各样的文件,这些扩展包在jdk安装目录jre/lib/ext下的jar - 第三个类加载器层次
这个就是平时用的加载器application,它用于加载classpath指定的内容 - 第四类的加载器层次
这个就是自定义加载器ClassLoader,加载自定义的加载器
CustomClassLoader父类加载器>application父类加载器>Extension父类加载器>Bootstrap
注意:这个图讲的是这个classLoader语法上是从谁继承的,不是类加载器的继承关系
- 双亲委派
一个class文件需要被load内存时候是这样的
任何一个class,加入自定义classloader,这时候先去自定义的classloader里面找,他内部维护着缓存,看缓存中有没有,有就直接返回,没有的话并不会直接加载,而是就接着往上级找,有就返回,没有就继续往上级找,最后如果父类Bootstrap也没有加载过,就委托Extension去加载,如果能加载就加载,不能加载就往下传递,如果能就就直接加载,最后如果都不能加载,就抛出异常ClassNotFound找不到,这就叫双亲委派机制
为什么要搞双亲委派机制?
主要是为了安全,加入用反正法,如果任何一个class都可以把它load到内存的胡,那我就可以给你java.lang.string,我交给自定义classloader,把这个string load到内存,打包给客户,然后给密码存储成string对象,我可以偷偷摸摸的把密码发给我自己,那就不安全了
双亲委派机制就不会出现这样的情况,自定义classloader加载一个java.lang.string他就产生了警惕,他先去上面查看有没有加载过,上面有加载过,直接返回给你,不给你重新加载
父加载器不是"类加载器的加载器"也不是"类加载器的父类加载器"
Laucher就是ClassLoader的一个包装类启动类,在这个类里面你要去看源码,就会看出很多内容,比如:类的加载路径,其实这些都规定在Launcher源码里面了
你可以通过System.getProperties(“类的加载路径(上面类加载路径图地址)”)来获取对应类加载器能加载的jar包,可以加一个replace()方法把;符号;换成System.lineSeparator()
双亲委派主要的含义就是当你要求一个classloader去load class的时候,它首先去找他的父亲,注意这个父亲不是继承关系上的父亲,是通过他的成员变量parent,是源码指定的被final修饰了,自定义的classLoader默认是通过父类的无参构造方法指定的,你可以调用父类的有参构造来设置自定义classLoader的父加载器
- 自定义加载器
需要先来看一下源码:
loadclass这个方法过程,就说你要加载一个类你只需要调用classLoader的loadclass方法就能够把这个类加载到内存中,加载到内存他会给你返回一个class类的对象,也就是刚才的过程,在硬盘上找到这个类的源码,这些源码可能在那些目录下面,找到这个class文件源码之后,把他load到内存,于此同时生成一个class类对象,把这个class对象返回给你
什么时候我们需要自己去加载一个类?Tomcat在load自己的那部分肯定是要load自定义的那些class的JRebel热部署你这个class放硬盘上我怎么去给你热替换,肯定需要一个classloader手动的load到内存里面去,spring动态代理,他是一个新的class,当你要用的时候他把新的load内存里了
自定义类加载器:
ClassLoader源码执行步骤:
findInCache->parent.loadClass->findClass()
自定义类加载器很简单,需要你自己继承ClassLoader类,去重写findClass方法,在findclass方法里找到需要load进来的二进制的内容,在内存里给它完整的load进来,load完之后再把这部分内容转换成class类对象,用defindClass方法,这种模式叫钩子函数模板方法
lazyLoading
懒加载
java是懒加载,什么时候需要才加载
执行模式
一个class文件load到内存之后通过java的解释器intepreter来执行,JIT的编译器指的是有某些代码我需要就把它编译成本地代码,相当于是exe,所以当有人问你java是编译语言还是解释语言,你完全可以告诉他我想解释就解释想编译就编译看你怎么定了,默认是混合模式,就是混合使用解释器+热点代码编译(hotspot)
什么叫做热点代码编译?
多次被调用的方法,多次被调用的循环,进行编译,怎么检测呢?就是用一个计数器,每个方法上都有一个方法计数器,循环有循环计数器,结果在发现某个方法执行了超过某个设定的值(XX: CompileThreshold=值),就要对它就行编译,直接编译成本地代码,在用的话直接用本地的,不要解释器执行了
为什么不直接都编译成本地代码呢,执行效率更高?
- 因为java解释器现在效率也非常高了,在一些简单的代码执行上它不输于编译器
- 如果你有一段代码执行文件特别特别多各种各样的类库时候,好几十个class这是很正常的,你上来二话不说先给编译器编译,过程会非常的长,所以现在默认的模式是混合模式,但是完全可以用参数来指定是什么模式
扩展知识:
如何打破双亲委派机制?- 重写loadClass()
- 在JDK1.2之前,自定义ClassLoader都必须重写loadClass()
- ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定
- 模块化的时候使用热部署
比如:tomcat,都有自己的模块指定classloader(可以加在同一类库的不同版本),两个classloader可以load进来同名的类,这是完全没有问题的,这里已经打破了双亲委派机制