(一)类加载过程:
当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。
类从被加载到虚拟机到内存中开始,到卸载内存为止,它的整个生命周期包括:
Java代码------------>编译成class文件---------->加载虚拟机内存中
加载虚拟机内存(JVM占用最多留出2G内存)中:
首先根据class文件的全限定名(class文件名称)获取二进制的字节流,随后把字节流代表的静态存储结构(class类文件结构)在解析阶段转换为直接引用放到JVM运行时数据区每一个区域(在物理内存中单独划分一块区域)(jdk1.8有所不同元空间使用的是直接内存)
1:加载
加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
类的加载由类加载器完成,类加载器通常由JVM提供。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
- 从JAR包加载class文件,这种方式也是很常见的,前面介绍
- JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
- 通过网络加载class文件。
- 把一个Java源文件动态编译,并执行加载。
类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。
2:链接
(1)验证:
验证阶段用于检验被加载的类是否有正确的内部结构。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
(2)准备:
类准备阶段负责为类的静态变量分配内存,并设置默认初始值。
举例:在我们Run完之后通过javap -v可以看到(必然存在的) (不一定存在 static{}或者static int a 等静态的才存在, 这两个是代表 :private static int a = 5 这种静态的,class init 类的初始化。 是代表private int a = 5 这种非静态的 ,object init 对象或实例的初始化。但是在准备阶段static int a 并没有执行所以此时static int a=0而不是5。
(3)解析:
将类的二进制数据中的符号引用替换成直接引用。说明一下:符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。通常分为这几种
-
接口和类的解析
-
字段的解析。找到字段的访问地址,父类
举例:class A extends B implements C{
int a = 10;
}
顺序是:先找A找到就结束,找不到A 则找C 一样是找见就结束,若找不到则找B 若是还找不见咋去Object中找,若是还找不到则报NosuchfileException 。若是在某一个环节找到了但是没有权限则报:illegalAccessError。 -
类方法的解析
举例:class A extends B implements C{
int a = 10;
}
顺序是:先A->B->C 找不到则报NoSuchMethod 没有权限则报:illegalAccessError。
3:初始化
初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。
javap -v 静态的初始化,不一定存在有静态才存在。
静态执行顺序:父类静态变量,父类静态块,子类的静态变量,子类的静态块。这样一个执行顺序。
javap -v 永远存在。
实例初始化顺序:父类的变初始化,父类的构造函数,子类的变量初始化,子类的构造函数。这样一个执行顺序。
(二)类加载时机:
- 创建类的实例,也就是new一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(Class.forName(“com.lyj.load”))
- 初始化一个类的子类(会首先初始化子类的父类)
- JVM启动时标明的启动类,即文件名和类名相同的那个类。
除此之外,下面几种情形需要特别指出:
对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。
(三)类加载器:
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:
(1)根类加载器(bootstrapclassloader)
它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
下面程序可以获得根类加载器所加载的核心类库,并会看到本机安装的Java环境变量指定的jdk中提供的核心jar包路径:
public class ClassLoaderTest {
public static void main(String[] args) {
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for(URL url : urls){
System.out.println(url.toExternalForm());
}
}
}
运行结果:
(2)扩展类加载器(extclassloader)
它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null
(3)应用类加载器(appclassloader)(默认先启动)
被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
类加载器加载Class大致要经过如下8个步骤:
- 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
- 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
- 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
- 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
- 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
- 从文件中载入Class,成功后跳至第8步。
- 抛出ClassNotFountException异常。
- 返回对应的java.lang.Class对象。
(四)类加载机制:
(1)JVM的类加载机制主要有如下3种:
- 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
- 双亲委派:所谓的双亲委派(这里双亲两字没有任何意义知识翻译而已),则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
(2)这里说明一下双亲委派机制(类加载器的默认机制):
appclassloade(子)>extclassloader(父)>bootstrapclassloader(爷)
代码如下:
if(parent !=null){
c = parent.loadClass(name,false);
}else {
c = findBootstrapClassOrNull(name);
}
loadClass
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 {
//双亲委派 start
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
//双亲委派 end
} 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;
}
}
findBootstrapClassOrNull
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
// 调用JNI 本地方法库
private native Class<?> findBootstrapClass(String name);
- 仔细看loadClass 源码你会发现里面 throw
ClassNotFoundException,可以明白一般报类找不到异常都是在类加载阶段报的。同时里面方法也有加锁(双亲委派在锁的方法里)保证统一时间只有一个线程执行这个块。 - parent !=null交给 appclassload 去执行,此时parent是不为null的,所以 c =parent.loadClass(name,false);
这行代码是交给extclassload去执行,但是此时extclassload为null的 所以执行 c=findBootstrapClassOrNull(name);
交给bootstrapclassloader,也就是说不会死循环
,实际上做事情的是bootstrapclassloader 。
工作原理是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
双亲委派机制的优势:避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,**发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,(保护基类)**这样便可以防止核心API库被随意篡改。、
如何打断双亲委派机制:
1:可以自己写一个classloader 覆盖原来的classloader
public class TestClass {
public static void main(String[] args) throws ClassNotFoundException,IllegalAccessException,InstantiationException{
//自定义类加载器
ClassLoader loader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream resourceAsStream = getClass().getResourceAsStream(filename);
if (resourceAsStream == null){
return super.loadClass(name);
};
byte[] b = new byte[resourceAsStream.available()];
resourceAsStream.read(b);
return defineClass(name,b,0,b.length);
}catch (IOException e){
throw new ClassNotFoundException();
}
}
};
String className = "com.springcloud.consumer.controller.TestClass";
Object o1 = TestClass.class.getClassLoader().loadClass(className).newInstance();
//默认输出AppClassLoader
System.out.println(o1.getClass().getClassLoader());
Object o2 = loader.loadClass(className).newInstance();
System.out.println(o2.getClass().getClassLoader());
System.out.println(o1 == o2);
System.out.println(o1.equals(o2));
//输出true
System.out.println(o1 instanceof TestClass);
//输出false
System.out.println(o2 instanceof TestClass);
}
}
2:把 jdk 中 rt.jar 删了话则双亲委任安全性就被破坏了