类加载过程
Java程序中的类加载是在运行期间完成的,一个类从被加载到虚拟机内存到卸载的整个生命周期包括:加载-验证-准备-解析-初始化-使用-卸载7个阶段。其中 验证-准备-解析 3个阶段称为连接
- 1 加载
加载过程需要完成三件事情
1 通过一个类的全限定名来获取定义这个类的二进制字节流。
2 将这个字节流所代表的静态存储结构转化我方法区的运行时数据结构。
3 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区走过来的各种数据访问入口。
其中第一个过程实现方式有很多种。
1 可以通过class文件中读取。
2 可以从ZIP包中读取,Jar,Ear ,War正是依靠这种形式。
3 从网络上获取,典型的就是Applet
4 运行时计算,典型的就是动态代理反射技术。
5 由其他文件生成,典型的就是Jsp应用,由Jsp生成的对应的Class类。
- 2 验证
验证是连接的第一步,这一步的目的是为了确保Class文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。比如一个类是否继承了一个final类,一个类(非抽象类)是否实现了其继承接口的所有方法,类文件是否已0xCAFEBABE开头,符号引用中的类,字段,方法的访问性(private,protected,public,default)是否可以被当前类访问。 - 3 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。注意 这个阶段进行的内存分配仅仅包含类变量(被static修饰的变量),而不包括实例变量(也叫成员变量,但是未被static修饰),实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。另外,这里的赋值 通常情况 下都是赋0值 (常量是这里面通常情况之外的情况),比如
public class Test {
//类变量:在准备阶段进行初始化
//value1,在准备阶段过后的值是0 而不是123,因为这个时候没有执行任何Java方法
//将value1赋值为123的指令是程序被编译后 存放于类构造器<clinit>()方法中的,所以赋值123的动作是在初始化才执行的
private static int value1 = 123;
//常量类变量:与value1区别的是,其在准备阶段的赋值是123 而不是0
private static final int value2 = 123;
//实例变量 在对象实例化的时候初始化
private int value3 = 123;
}
image.png
- 4 解析
解析阶段是将常量池中的符号引用替换为直接引用的过程。 - 5 初始化
初始化是类加载的最后一步,到了初始化阶段,才是真正执行类中定义的Java代码。在准备阶段变量已经赋过一次系统要求的初始值(常量除外),在初始化阶段则根据程序员的需求来初始化值。从另外一个阶段来说,初始化阶段是执行类构造器的<clinit>()。
<clinit>()方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的,如何类或者接口,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态块中只能访问定义在语句块之前的静态变量,对于 定义在它之后的静态变量,语句块可以赋值,但是不能访问,如以下例子
public class Test1 {
static {
a = 2;
System.out.println(a); //报错 非法向前引用
}
static int a = 1 ;
}
clinit 方法与类的构造函数(或者说实例构造函数<init>()方法)不同,它不需要显示的调用父类构造器,虚拟机会保证在子类clinit 执行之前,父类的clinit 方法已经执行,所以 在虚拟机中第一个被执行的clinit 方法肯定是Object的clinit 方法。由于父类的clinit 方法优先执行,这意味着父类定义的静态块要优先于子类的变量的赋值操作,如以下的输出是2而不是1
public class Test2 {
static class Parent{
public static int a = 1;
static {
a =2;
}
}
static class Sub extends Parent{
public static int b =a;
}
public static void main(String[] args) {
System.out.println(Sub.b);
}
}
类的clinit 方法方法并不是必需的,如果一个类中没有静态语句块也没有类变量的赋值操作,那么编译器可以不为这个类生成clinit 方法。虽然接口中不能使用静态语句块,但是仍然有变量初始化的赋值操作,因此 接口和类都可以生成clinit 方法,但是接口和类不一样的是:执行接口的clinit 方法不需要优先执行父接口的clinit 方法,只有当父接口定义类变量时,父接口才会初始化。另外接口的实现类在初始化时也一样不会执行接口的clinit 方法。
虚拟机会保证一个类的clinit 方法在多线程环境中被正确的加锁,同步,如果多个线程同时去初始化一个类,那么虚拟机会保证只有一个线程去执行这个类的clinit 方法,其他线程会阻塞等待,直到方法执行完毕。
类加载器
类加载器用于类的加载动作,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立起在虚拟机中的唯一性,每一个类加载器都有其独立的空间。换句话说就是,比较两个类是否相等,只有这两个类由同一个类加载器加载的前提下才有意义,否则:即使来源同一个class文件,只要类加载器不一样,那么这两个类就不等。
上面说的相等,包括Class对象的equals()方法,isAssignableFrom()方法。isInstance()方法返回的结果,也包括instanceof关键字做对象的关系判定情况。
public class ClassLoadTest {
public static void main(String[] args) throws Exception{
ClassLoader myLoder = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null){
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
}catch (Exception e){
throw new ClassNotFoundException();
}
}
};
Object obj = myLoder.loadClass("jvm.ClassLoadTest").newInstance();
System.out.println(obj.getClass());
//此时虚拟机中存在两个ClassLoadTest类,obj 由myLoder类加载器加载出来的,jvm.ClassLoadTest由系统应用程序类加载来的 这里会返回false
System.out.println(obj instanceof jvm.ClassLoadTest);
}
}
双亲委派模型
image.png- 启动类加载器 Bootstrap ClassLoader:这个类用于负责加载<JAVA_HOME>/lib 的类库到虚拟机内存中。
- 扩展类加载器 Extension ClassLoader:这个加载器用于加载<JAVA_HOME>/lib/ext 目录中的类库到内存中。
- 应用类程序加载器 Application ClassLoader:这个加载器负责加载用户路径(ClassPath)上指定的类库,如果程序中没有指定自己的类加载器,那么一般都是默认用这个加载器来加载类。
上面的加载器结构中 Bootstrap ClassLoader是C/C++写的,是虚拟机的一部分,我们无法在程序中获取到它。
双亲委派模型的工作过程是:当一个加载器收到一个类加载的请求,它首先不会自己去尝试加载这个类,而是把这个加载的请求交给父加载器去完成,因此所有的加载请求都是从上到下依次执行的,加入最顶层的Bootstrap ClassLoader无法加载这个类,才轮得到下层的加载器来执行这个加载动作。
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println(111);
}
}
image.png
使用双亲委派模型的好处是确定了类的唯一性,因此java.lang.String类这是java的自带类,位于rt.jar的java.lang包下。
无论哪个加载器来加载它,最终都是由Bootstrap ClassLoader来开始加载的,最终是确定且唯一的。如果没有使用双亲委派模型,那边会造成系统中有多个java.lang.String类,应用程序将一片混乱。
虚拟机字节码执行引擎
运行时栈帧结构
栈帧是用于虚拟机进行方法调用和方法执行时的数据结构,它是虚拟机运行时数据区的虚拟机栈的栈元素。栈帧存储了局部变量表,操作数栈,动态连接和方法返回地址等信息,在编译的时候,栈帧需要多大的局部变量表,多深的操作数栈都已经完全确定了(这取决于用户配置的虚拟参数如内存大小等),因此一个栈帧需要分配多少内存 不会受到运行期变量数据的影响 而是已经确定好了的。每一个方法从调用开始到执行完成过程,都对应一个栈帧在虚拟机栈的入栈和出栈过程。对于线程来说 只有当前栈顶的栈帧才是有效的,这个栈帧称为当前栈帧,与这个栈帧对应的方法称为当前方法。如下图:
-
局部变量表
局部变量表是一组存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(Slot)为最小单位,虚拟机并没有说明Slot的占用内存大小只是说了Slot可以存放boolean ,byte,char,short,int,float,reference的类型数据。 -
操作数栈
操作数栈是一个先入后出的栈,方法的执行在虚拟机上就是入栈与出栈的过程。 -
动态连接
-
方法返回地址
一个方法的退出有两种情况,正常退出和抛出异常,正常退出时调用者的PC程序计数器可以作为返回地址,异常退出时,返回地址根据异常处理器表来确定。
网友评论