类的加载和实例化

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013617791/article/details/81809250


以下陈述的内容都已HotSpot为基准。

唯一的JVM进程

当启动Java虚拟机运行某个Java程序时,不管Java程序有多慢复杂,都只占用1个JVM进程,都使用JVM进程的内存区。
JVM进程结束后,进程在内存中的状态会丢失,即再次开启JVM进程,内存不会保留上次内存中的信息。

JVM进程终止的情况:

  • 进程运行到最后正常结束
  • 程序运行到使用System.exit() 或 Runtime.getRuntime().exit()代码初结束
  • 程序执行过程中遇到未捕获的异常或错误而结束
  • 程序所在平台强制结束JVM进程

类加载机制

类加载机制是什么

一个.java文件在编译后会形成相应的一个或多个Class文件(若一个类中含有内部类,则编译后会产生多个Class文件),但这些Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程就是虚拟机的 类加载机制

类加载机制的特点

与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类型的加载和连接都是在程序运行期间完成,这样会在类加载时稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性,Java中天生可以动态扩展的语言特性多态就是依赖运行期动态加载和动态链接这个特点实现的。例如,如果编写一个使用接口的应用程序,可以等到运行时再指定其实际的实现。这种组装应用程序的方式广泛应用于Java程序之中。

类加载机制的过程

程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤对类进行加载也叫初始化(这里的叫法不太严谨,但是一般都这么说,注意和每个子步骤中的加载和初始化的定义做区别)。如果没有意外,JVM会连续完成这三个步骤,所以一般把这三个步骤统称为类的加载或类初始化。另外,如果该类的直接父类还没有被加载,则先加载该类的父类。
加载、验证、准备、初始化和卸载这5个阶段的开始顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

特别需要注意的是,类的加载过程必须按照这种顺序按部就班地“开始”,而不是按部就班的“进行”或“完成”,因为这些阶段通常都是相互交叉地混合式进行的,也就是说通常会在一个阶段执行的过程中调用或激活另外一个阶段。

下图:类的整个生命周期
在这里插入图片描述

加载

类的加载,指类加载器根据查找路径找到并类的.class文件读入内存,并为之创建一个java.lang.Class对象

类也是一种对象,他没都是java.lang.Class的实例

关于类加载器:

JVM的类加载器通常被称为系统加载器。除了根类加载器之外,其他类加载器都是通过Java语言编写的,开发者可以通过集成ClassLoader基类来创建自己的类加载器。

类加载的来源

  • 本地文件系统
  • JAR包
  • 网络
  • 动态编译Java源文件后加载

类加载的时机
什么情况下虚拟机需要开始加载一个类呢?虚拟机规范中并没有对此进行强制约束,这点可以交给虚拟机的具体实现来自由把握。

  • 使用前预先加载
  • 首次使用时加载
  • 类加载的原则:延迟加载,能不加载就不加载。

连接

类的连接,即把类的.class文件中的内容合并到JRE中,具体可分为三个阶段:

  • 验证:检查.class文件内部结构的正确性
  • 准备:为类的静态变量分配内存并设置默认初始值,这些变量所使用的内存都将在方法区中进行分配。

注意:

  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化的时候随对象一起分配到堆中。
  • 这些内存都将在方法区中分配
  • 非常量(final)的静态数据类型被默认赋对应类型的零值(如0、0L、null、false等),而不是程序中显示的赋值。
  • 解析:将类的符号引用替换为直接引用

符号引用和直接引用的区别与关联:

  • 符号引用:
    符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
  • 直接引用:
    直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

初始化

定义
对静态变量和静态代码块执行初始化。

注意:

  1. 此时才是开始使用程序代码去初始化类变量和其他资源,之前除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。
  2. 初始化的顺序为代码排列顺序。如静态代码块在静态变量声明前,静态变量a在静态代码块和静态变量中分别做了赋值,则a先被静态代码块赋值,后被静态变量赋值语句赋值。并且静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

类构造器<clinit>()

  • 定义: 初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作静态语句块static{}中的语句合并产生的

  • 特点: 类构造器&lt;clinit>()实例构造器&lt;init>()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器<clinit>()执行之前,父类的类构造&lt;clinit>()先执行完毕。由于父类的构造器<clinit>()先执行,也就意味着父类中定义的静态语句块/静态变量的初始化要优先于子类的静态语句块/静态变量的初始化执行。特别地,类构造器<clinit>()对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器<clinit>()。

  • 多线程时: 虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为 在同一个类加载器下,一个类型只会被初始化一次。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。

    //静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
    public class Test{
        static{
            i=0;
            System.out.println(i);//Error:Cannot reference a field before it is defined(非法向前应用)
        }
        static int i=1;
    } 
    

类初始化的时机
更详细讲述

  • 创建类的实例
  • 调用某个类的静态变量或者静态方法
  • 初始化某个类的子类
  • 使用反射机制强制创建某个类或接口对应的java.lang.Class对象
  • 直接使用java.exe命令来运行某个主类

对于常量(final)静态(static)类变量,在程序编译时期就可以确定该值(是个常量),所以不会加载对应的类。

类实例化

类实例化的时机

类实例化的过程

  1. 前提: .class文件已经被加载完毕(经过加载、解析和初始化)
  2. 系统赋初值:在堆内存中为Xxx类以及其父类(包括间接父类)中的成员变量开辟空间,并系统默认初值(0,null,false),因为成员变量有初值,函数才能使用。

    即使子类覆盖了父类成员变量,仍会给父类所有成员变量分配空间,此时子类中多了一个属性,分态性,分空间存储,分别存储父类和子类的成员变量

  3. 父类构造函数执行
    Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有类的构造函数的第一条语句必须是超类构造函数的调用语句,以保证所创建实例的完整性
  4. 成员变量的显示初始化
  5. 子类构造代码块初始化

    注意:
    1、编译器会将4和5中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前
    2、Java是按照编程顺序来执行实例变量初始化器和实例初始化器中的代码的,并且不允许顺序靠前的实例代码块初始化在其后面定义的实例变量

    //下面的这些代码都是无法通过编译的,编译器会抱怨说我们使用了一个未经定义的变量。
    //之所以要这么做是为了保证一个变量在被使用之前已经被正确地初始化。
    public class InstanceInitializer {  
        {  
            j = i;  
        }  
        private int i = 1;  
        private int j;  
    }  
    public class InstanceInitializer {  
        private int j = i;  
        private int i = 1;  
    }  
    
  6. 构造函数初始化
    每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成<init>()方法,参数列表与Java语言书写的构造函数的参数列表相同。

类加载器

功能

类加载器负责将未被加载的.class文件加载到内存,并为之生成对应的java.lang.Class对象。这意味着被加载的类具有唯一性

  • 类的唯一性:一个类,由其权限定类名和类加载器作为唯一标识。也就是说,不同加载器加载的同一个类被认为是完全不同、互不兼容的。

分类

  • 启动类加载器(根类加载器)
    使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机),是虚拟机自身的一部分。它负责加载存放在JDK\jre\lib下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库。无法被Java程序直接引用
  • 扩展类加载器:
    使用java实现,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用
  • 应用程序类加载器:
    使用java实现,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

除了根类加载器器之外,所有类加载器都是ClassLoader子类的实例。开发者可以通过扩展ClassLoader子类并重写其方法来实现自定义的类加载器。

类加载器的层次关系

这几种类加载器的层次关系如下图所示:
这里写图片描述

  • 这种层次关系称为类加载器的双亲委派模型。该模型在JDK1.2期间被引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式。
  • 在双亲委派模型中,除了引导类加载器之外,所有的类加载器都有一个父类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。也就是说,加载之间的父子关系并不是继承关系来实现的,而是类加载器和其加载的实例的关系。一般来说,开发人员编写的类加载器的父类加载器是应用程序类加载器。
  • 双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

常问问题

一个实例变量在对象初始化的过程中会被赋值几次?

我们知道,JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。如果我们在声明实例变量x的同时对其进行了赋值操作,那么这个时候,这个实例变量就被第二次赋值了。如果我们在实例代码块中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。如果我们在构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。也就是说,在Java的对象初始化过程中,一个实例变量最多可以被初始化4次。

类的加载机制和实例化的区别

类的加载机制 类的实例化
是什么 把类从.class文件加载到虚拟机,为实例化做准备 创建一个类的实例(对象)的过程
实例化的对象 只有类变量按照程序代码被赋值(<clinit>()方法的执行) 非静态变量的初始化,以及构造方法的执行(<init()方法的执行)
执行次数 一个类只会执行1次 一个类可执行多次

参考文献

https://blog.csdn.net/ns_code/article/details/17881581 类加载机制
https://blog.csdn.net/ns_code/article/details/17675609 符号引用和直接引用
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html 深入探讨Java类加载器
https://blog.csdn.net/justloveyou_/article/details/72217806 深入理解Java类加载器
https://blog.csdn.net/justloveyou_/article/details/72466105 深入理解类的加载时机和加载过程
https://blog.csdn.net/justloveyou_/article/details/72466416 深入理解Java对象的创建过程:类的初始化与实例化

猜你喜欢

转载自blog.csdn.net/u013617791/article/details/81809250