第一章 类的加载过程和对象的创建过程


类的加载过程,这是一个经常会被问到的面试题,11月11号去一家公司做了一套笔试题,里面就有让你写出打印顺序的。

一.类的加载顺序

1.父类静态变量
2.父类静态代码块
3.子类静态变量
4.子类静态代码块
5.父类变量
6.父类非静态代码块
7.父类构造方法
8.子类变量
9.子类非静态代码块
10.子类构造方法

/**
 * @Author wangbiao
 * @Date 2019-11-23 22:12
 * @Decripition TODO
 **/
public class ClassLoadTest {
    public static void main(String[] args) {
        Son son = new Son();
    }
}


class Parent{
    private static final String first="first";
    private static String second = "second";
    private String third = "third";

    static {
        System.out.println("【父类】静态代码块:first:"+first+";second:"+second);
    }
    {
        System.out.println("【父类】非静态代码块:first:"+first+";second:"+second+"third:"+third);
    }

    public Parent(){
        System.out.println("【父类】构造方法:first:"+first+";second:"+second+"third:"+third);

    }

}


class Son extends Parent{
    private static final String fourth="fourth";
    private static String fifth = "fifth";
    private String sixth = "sixth";

    static {
        System.out.println("【子类】静态代码块:fourth:"+fourth+";fifth:"+fifth);
    }
    {
        System.out.println("【子类】非静态代码块:fourth:"+fourth+";fifth:"+fifth+"sixth:"+sixth);
    }

    public Son(){
        System.out.println("【子类】构造方法:fourth:"+fourth+";fifth:"+fifth+"sixth:"+sixth);

    }

}

运行结果:

父类】静态代码块:first:first;second:second
【子类】静态代码块:fourth:fourth;fifth:fifth
【父类】非静态代码块:first:first;second:secondthird:third
【父类】构造方法:first:first;second:secondthird:third
【子类】非静态代码块:fourth:fourth;fifth:fifthsixth:sixth
【子类】构造方法:fourth:fourth;fifth:fifthsixth:sixth

二.类的加载过程

类的加载全过程:1.加载  2.准备 3.验证 4.解析 5.初始化

1.加载:通过类的全限定名称找到要加载的二进制字节流文件加载到内存中,将静态存储结构转变成方法区的运行时数据结构,并在堆内存里面创建一个class对象作为访问方法区类信息的入口。


2.验证:验证加载进来的数据是否符合虚拟机的规范,并且不会危害j虚拟机的安全。验证包括文件格式验证,元数据验证,字节码验证码,符号引用验证。

文件格式验证:验证加载进来的数据是否符合class文件的规范,只有符合规范了,数据才能被存储到方法区。
在这个阶段做的比如final方法是否被重写,final类是否被继承。

3.准备:给静态变量分配内存空间,这个内存空间是在方法区中的,并且会给默认值,默认值一般是0,如果类有静态常量的话就会直接赋初值。例如下面的代码,其中first是静态常量,就会直接将123赋值给它,而second是静态变量,就会赋值0。比如8种基本类型的初值,默认为0;引用类型的初值则为null。

private static final int first= 123;
private static int second = 456;

4.解析:将常量池里面的符号引用转变成直接引用。

5.初始化:执行类构造器,类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。即执行给类变量赋值和执行静态代码块里面的内容,如果这个类继承了父类,那么先执行父类的类变量赋值和静态代码块的内容。所以这里就和前面代码执行的输出过程相验证。先是执行类静态变量赋值,再执行静态代码块,最后执行构造方法。

三.对象的创建过程

 Student student = new Student()

当new一个对象并付值的过程

1.判断这个类是否被加载到虚拟机中,首先回去检查这个指令是在常量池中能否定位到一个类的符号引用(即类的全名),并且会检查这个类是否被加载解析和初始化,如果没有就会去走类的加载过程。

2.给这个对象分配内存空间,类在被加载的时候就知道创建这个类的对象要分配的内存大小了。分配内存有两种方式,一种是指针碰撞,另一种是空闲列表。

指针碰撞就是在内存空间中有一个已经被使用了的空间和空闲空间的临界点,指针就指向这个临界点,当要分配内存空间的时候,指针需要向空闲区域移动要分配内存大小的位置。
空闲列表就是在虚拟机中维护着一个堆内存空闲区域的列表,列表中记录着空闲区域的位置和大小,当需要分配内存的时候,找出一个大小合适的空间给对象。空闲列表的方式分配会产生内存碎片。
这两种分配方式和垃圾回收算法有关,标记整理垃圾回收算法就会用到指针碰撞的分配方式,标记清除就会用到空闲列表的方式。

3.给对象的实例变量赋初值,基本类型赋的初值是0,引用类型是null

4.设置对象头,给对象头赋hashcode值,GC年龄等信息。

5.给对象变量赋设定的初始值,执行非静态代码块和构造方法,如果有父类的话,先会给父类的对象变量赋初始值,执行父类的代码块和构造方法。

6.在栈中创建对象引用变量,指向刚才创建的实例对象。

到此类就被完全加载进来了。Student student = new Student(),这里还涉及到一个知识点,就是单例的双重检测,为什么在写单例的双重检测的时候,申明的类变量要用volatitle修饰,可以看到Student student = new Student()是经过三步走的:

A.在堆内存开启存储空间   B.初始化对象    C.赋值给引用变量
这里如果不用volatitle修饰的话,由于指令会重排序,可能会先执行A,再执行C,最后执行B,这样就会在多线程下,线程一执行到了C,然后线程二去获取对象,发现对象非空,就拿到了单例,其实线程一还没有执行B,线程二拿到的是一个不完整的对象,这就有可能出问题,volatitle修饰的话就不会出现指令重排了。

三.类加载机制

1.类加载器分类
     

根类加载器 bootstrap class loader 
它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar,sun.boot.class.path路径下的内容),是用原生代码(C语言)来实现的,并不继承自 java.lang.ClassLoader。

加载扩展类和应用程序类加载器。并指定他们的父类加载器。

扩展类加载器 extensions class loader

用来加载 Java 的扩展库(JAVA_HOME/jre/ext/*.jar,或java.ext.dirs路径下的内容) 。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类。

应用类加载  application class loader

它根据 Java 应用的类路径(classpath,java.class.path 路径下的内容)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。

自定义类加载器

可以自己写一个类加载器,不过这个类加载器要继承ClassLoader这个类

2.双亲委派机制

双亲委派机制是当一个类加载器接收到类加载的请求的时候自己首先不去加载,而是交给父类去加载,父类也不去加载,最后都会交给根加载器加载,如果根加载器在发现扫描的范围内没有这个类的时候,就交给父类加载,父类也没有加载成功的话,就交给子类加载,如果子类还是加载不成功,那就会报ClassNotFoundException。

双亲委派机制的好处就是在全局能确定一个唯一类,在虚拟机中类的确定不仅和类的全限定名称有关,还和类加载器有关,如果不用双亲委派机制的话,一个类有可能被多个类加载器加载,然后出现赋值的时候问题,不能确定是哪一个类了,双亲委派模型可以确定一个类最终都是同一个类加载器加载,确保加载结果相同。这也是保证java核心库的安全,如果用户写了一个java.lang.Object类的话,这个类也会最终交由根加载器加载,但是在根加载器加载的时候会先去加载核心类库里面的类,发现核心类库里面已经有了就不会再加载这个了。

四.tomcat的类加载机制

tomcat的类加载机制首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的

CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*/server/*/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。

commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。

WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。

而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能

tomcat类加载的设计模式没有使用双亲委派模型,而是采用了隔离的方式,每一个webappClassLoader都会去加载自己目录下面的class,不传递给父类去加载,那为什么要这么设计呢?

这是由于一个tomcat里面可以加载多个工程,每一个工程都需要加载自己的类库和自己下面的类,如果两个工程里面引用的类库一样,但版本不一样,由于都交给顶层类加载器加载的话,这样就会很容易出现问题。

web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。

五.上下文类加载器

这个太难理解,放到后面去理解

https://blog.csdn.net/yangcheng33/article/details/52631940

六.classLoader

Class.forName加载类时将类进了初始化,而ClassLoader的loadClass并没有对类进行初始化,只是把类加载到了虚拟机中。参考

在我们熟悉的Spring框架中的IOC的实现就是使用的ClassLoader。

而在我们使用JDBC时通常是使用Class.forName()方法来加载数据库连接驱动。这是因为在JDBC规范中明确要求Driver(数据库驱动)类必须向DriverManager注册自己。

文章:https://www.jianshu.com/p/dd39654231e0

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值