美文网首页
java类加载机制

java类加载机制

作者: 有章 | 来源:发表于2018-08-13 21:28 被阅读0次
    类加载流程

    双亲委派模型:
    Bootsrap类加载器,负责加载JAVA_HOME/lib下的类或者被-Xbootclasspath 参数所指定的路径种的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录下也不会重载)
    ExtentClassLoad扩展类加载器,负责加载JAVA_HOME/lib/ext下的类或者由系统路径获取java.ext.dirs系统变量直接指定的路径下的类库,开发者可以直接使用
    Application ClassLoad应用类加载器,负责加载Class path下的类。
    ClassLoader. getSystemClassLoader返回值即为该类加载器

    加载
    根据一个类的全限定名(如cn.edu.hdu.test.HelloWorld.class)来读取此类的二进制字节流到JVM内部;
    将字节流所代表的静态存储结构转换为方法区的运行时数据结构(hotspot选择将Class对象存储在方法区中,Java虚拟机规范并没有明确要求一定要存储在方法区或堆区中)
    转换为一个与目标类型对应的java.lang.Class对象;

    连接又分为验证、准备、解析阶段:
    验证
    验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证;

    准备
    为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量将不再此操作范围内);

    解析
    将常量池中所有的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法)。这个阶段可以在初始化之后再执行。

    初始化
    在连接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员自己写的逻辑去初始化类变量和其他资源,举个例子如下:

    public static int value1  = 5;
    public static int value2  = 6;
    static{
        value2 = 66;
    }
    

    在准备阶段value1和value2都等于0;

    在初始化阶段value1和value2分别等于5和66;

    所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用;
    编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量;
    如果超类还没有被初始化,那么优先对超类初始化,但在<clinit>方法内部不会显示调用超类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的超类<clinit>方法已经被执行。
    JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。(所以可以利用静态内部类实现线程安全的单例模式)
    如果一个类没有声明任何的类变量,也没有静态代码块,那么可以没有类<clinit>方法;

    1.准备阶段为类变量(static修饰的变量,非类实例变量)分配内存并设置初始值,这些变量使用的内存都在方法区中进行分配(实例变量将会在对象初始化时分配在堆中)
    public static value=123,在准备阶段会设置为0,在初始化阶段再设置为123
    public static final value=123为特殊情况,在准备阶段就会初始化为123
    2.静态代码块和实例变量都是在初始化阶段赋值

    public static int value=5;
    public static int value2=6;
    static {
    value2=66;
    }
    
    public class SSClass {
        static {
            System.out.println("SSClass init...");
        }
    }
    public class SuperClass extends SSClass {
        static {
            System.out.println("SuperClass init...");
        }
        public static int value=123;
        public SuperClass(){
            System.out.println("init SuperClass");
        }
    }
    public class SubClass extends SuperClass {
        static {
            System.out.println("SubClass init...");
        }
        static int a;
        public SubClass(){
            System.out.println("init SubClass..");
        }
    }
    public class NotInitialization {
        public static void main(String[] args) {
            System.out.println(SuperClass.value);
        }
    }
    ------------------------------------
    结果:
    SSClass init...
    SuperClass init...
    123
    

    1.只会初始化定义字段的类
    2.对final static修饰的字段引用,不会初始化所在类(在准备阶段已经赋值,并放在了常量池)

    public class ConstantClass {
        static {
            System.out.println("ConstantClass init");
        }
        public final static String HELLOWORLD="hello world";
    }
    public class NotInitialization {
        public static void main(String[] args) {
            System.out.println(ConstantClass.HELLOWORLD);
        }
    }
    ----------------------
    结果
    hello world
    

    3.通过数组定义来引用类,不会触发此类的初始化

    public class NotInitialization
    {
        public static void main(String[] args)
        {
            SuperClass[] sca = new SuperClass[10];
        }
    }
    

    jvm有严格的规定(五种情况):
    1.遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,假如类还没进行初始化,则马上对其进行初始化工作。
    其实就是3种情况:用new实例化一个类时、读取或者设置类的静态字段时(不包括被final修饰的静态字段,因为他们已经被塞进常量池了)、以及执行静态方法的时候。
    2.使用java.lang.reflect.*的方法对类进行反射调用的时候,
    如果类还没有进行过初始化,马上对其进行。
    3.初始化一个类的时候,如果他的父亲还没有被初始化,则先去初始化其父亲。
    4.当jvm启动时,用户需要指定一个要执行的主类(包含static void main(String[] args)的那个类),则jvm会先去初始化这个类。
    5.用Class.forName(String className);来加载类的时候,也会执行初始化动作。
    注意:ClassLoader的loadClass(String className);方法只会加载并编译某类,并不会对其执行初始化。


    利用类的实例化构建一个单例模式:

    public class Singleton{
    private static Singleton singleton=new Singleton();
    Singleton(){
    }
    //静态方法获取私有属性,如果类没有初始化,则去初始化,初始化时,jvm会保证只有一个线程可以去执行初始化操作
    public static Singleton getInstance(){
       return singleton;
    }
    

    double check的单例模式:
    类属性(static修饰)在准备阶段会赋值为null (int 型等会分配内存,赋初值0),在初始化阶段进行初始化,分配内存进行初始化,并将singleton指针指向,不具有原子性,所以需要加锁.
    1.volatile的作用:防止内存重排序
    new对象的过程:
    a.分配内存
    b.赋初值
    c.指针singleton指向内存
    指令重排序后导致可能执行下列的步骤:
    a.分配内存
    b.指针singleton指向内存
    c.内存初始化
    synchronized加锁虽然能保证互斥,但是不保证程序在一个时间片内将所有代码执行完毕,会释放时间片,等待在分配时间片再执行。如果b和c执行顺序被调换,线程1分配了内存,并将singleton指向了内存,但是却没有初始化,此时正好写会了主存。此时线程2执行,在第一个if(singleton==null) (此处并没有加锁)进行判断,返现singleton并不为null,则返回了没有初始化的singleton,导致引用到的地方发生奔溃。

    public class Singleton{
      private static volatile Singleton singleton=null;
      public  static Singleton getInstance(){
        if(singleton==null){
          synchronized(this){ 
     //加锁后,线程执行完会将singleton刷新到主存中,但是!!
    //如果此时线程时间片用完了,但是只执行了分配内存、指针singleton指向
    //内存,却没有执行初始化!!下一次线程判断不为null之后,不会执行新建,会
    //用到没有初始化的内存块。程序可能会奔溃
               if(singleton==null){
                  singleton=new Singleton();
              }         
            }
         }
      }
      return singleton;
    }
    

    【参考博客】
    https://www.cnblogs.com/aspirant/p/7200523.html
    https://blog.csdn.net/noaman_wgs/article/details/74489549
    https://blog.csdn.net/anjxue/article/details/51038466
    https://www.cnblogs.com/aspirant/p/7200523.html

    相关文章

      网友评论

          本文标题:java类加载机制

          本文链接:https://www.haomeiwen.com/subject/nvhpbftx.html