1、什么是类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
我们平时进行程序开发工作只是做到了第一步的java文件的编写,而后面的事情不是一般程序员所关心的事情。当我们在进行Ctrl + S操作时,我们的IDE工具(eclipse)会对我们编写的java文件进行编译工作,生成一个class文件,如果我们的程序写的没有问题,那么eclipse可以对我们的java文件进行正常的编译。
比如我们在eclipse中写下以下的java文件:
通过反编译软件(jd-gui)可以看到编译后的class文件:
但是当我们修改程序,让程序通不过编译:
F11操作,控制台输出执行结果:
而此时我们通过反编译软件(jd-gui)同样可以看到编译后的class文件中的内容:
我们可以看到反编译输出的和控制台输出的内容很类似。
当我们的java文件正常的通过编译后生成一个class文件,在程序启动运行时,进入java虚拟机模块:
1、
类加载器被设计成一种层级结构关系,也就是父子关系,其中bootstrap类加载器是所有类加载器的父类
--Bootstrapclass loader:
当运行java虚拟机时,这个类加载器被创建,它加载一些基本的Java API,包括Object这个类。需要注意的是,这个类加载器不是用java语言写的,而是用C/C++写的。
--Extensionclass loader:
这个加载器加载出了基本API之外的一些拓展类,包括一些与安全性能相关的类。
--SystemClass Loader:
它加载应用程序中的类,也就是在你的classpath中配置的类。
--User-DefinedClass Loader:
这是开发人员通过拓展ClassLoader类定义的自定义加载器,加载程序员定义的一些类。
委派模式(Delegation Mode)
仔细看上面的层次结构,当JVM加载一个类的时候,下层的加载器会将将任务委托给上一层类加载器,上一层加载检查它的命名空间中是否已经加载这个类,如果已经加载,直接使用这个类。如果没有加载,继续往上委托直到顶部。检查完了之后,按照相反的顺序进行加载,如果Bootstrap加载器找不到这个类,则往下委托,直到找到类文件。对于某个特定的类加载器来说,一个Java类只能被载入一次,也就是说在Java虚拟机中,类的完整标识是(classLoader,package,className)。一个类可以被不同的类加载器加载。
举个具体的例子来说明,现在加入我有一个自己定义的类MyClass需要加载,如果不指定的话,一般交App(System)加载。接到任务后,System检查自己的库里是否已经有这个类,发现没有之后委托给Extension,Extension进行同样的检查,发现还是没有继续往上委托,最顶层的Bootstrap发现自己库里也没有,于是根据它的路径(Java 核心类库,如java.lang)尝试去加载,没找到这个MaClass类,于是只好(人家看好你,交给你完成,你无能为力,只好交给别人啦)往下委托给Extension,Extension到自己的路径(JAVA_HOME/jre/lib/ext)是找,还是没找到,继续往下,此时System加载器到classpath路径寻找,找到了,于是加载到Java虚拟机。
现在假设我们将这个类放到JAVA_HOME/jre/lib/ext这个路径中去(相当于交给Extension加载器加载),按照同样的规则,最后由Extension加载器加载MyClass类,看到了吧,同一个类被两次加载到JVM,但是每次都是由不同的ClassLoader完成。
可见性限制
下层的加载器能够看到上层加载器中的类,反之则不行,也就是是说委托只能从下到上。
不允许卸载类
类加载器可以加载一个类,但是它不能卸载一个类。但是类加载器可以被删除或者被创建。
当类加载完毕之后,JVM继续按照下图完成其他工作:
框图中各个步骤简单介绍如下:
Loading:文章前面介绍的类加载,将文件系统中的Class文件载入到JVM内存(运行数据区域)
Verifying:检查载入的类文件是否符合Java规范和虚拟机规范。
Preparing:为这个类分配所需要的内存,确定这个类的属性、方法等所需的数据结构。(Prepare adata structure that assigns the memory required by classes and indicates thefields, methods, and interfaces defined in the class.)
Resolving:将该类常量池中的符号引用都改变为直接引用。(不是很理解)
Initialing:初始化类的局部变量,为静态域赋值,同时执行静态初始化块。
那么,ClassLoader在加载类的时候,究竟做了些什么工作呢?
要了解这其中的细节,必须得先详细介绍一下运行数据区域。
2、 运行时数据区域:
(1) 程序计数:负责分支、循环、跳转、异常处理、线程恢复等
(2) Java虚拟机栈
(3) 本地方法栈
(4) Java堆
(5) 方法区
Java虚拟机栈:Java栈也是每个线程单独拥有,线程启动时创建。这个栈中存放着一系列的栈帧(Stack Frame),JVM只能进行压入(Push)和弹出(Pop)栈帧这两种操作。每当调用一个方法时,JVM就往栈里压入一个栈帧,方法结束返回时弹出栈帧。如果方法执行时出现异常,可以调用printStackTrace等方法来查看栈的情况。栈的示意图如下:
OK。现在我们再来详细看看每一个栈帧中都放着什么东西。从示意图很容易看出,每个栈帧包含三个部分:本地变量数组,操作数栈,方法所属类的常量池引用
Java堆:几乎存放着所有的对象实例,java堆不需要在物理上连续,只要在逻辑上连续即可。
方法区:用于存储类信息、常量、静态变量、即时编译器编译后的代码、有关域和方法的信息、类和方法的字节码等数据。
运行时常量池:属于方法区的一部分,用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后进入方法取得运行时常量池中存放。
比如静态变量、实例变量、常量:静态变量存放在方法区,在类加载时进行初始化工作,并为静态变量分配内存,生命周期由该类决定;实例变量:存放于堆区,每新建一个对象,都会对该类中的实例变量分配内存,实例变量属于该对象,生命周期由对象决定;常量:存放于方法区中的运行时常量池。
对象的访问定位:
1、通过句柄访问对象:好处:引用中存放的是具体的句柄地址,在对象被移动是只会改变句柄中的实例数据指针 ,而引用本身不需要修改;
2、通过指针进行对象访问:节省了一次指针定位的时间开销,速度更加快。
1、通过句柄访问对象:
2、通过指针访问对象: