07.Java类加载问题

1,270 阅读20分钟

目录介绍

  • 7.0.0.1 Java内存模型里包含什么?程序计数器的作用是什么?常量池的作用是什么?
  • 7.0.0.2 什么是类加载器?类加载器工作机制是什么?类加载器种类?什么是双亲委派机制?
  • 7.0.0.3 什么时候发生类初始化?类初始化后对类的做了什么,加载变量,常量,方法都内存那个位置?
  • 7.0.0.4 通过下面一个代码案例理解类加载顺序?当遇到 类名.变量 加载时,只加载变量所在类吗?
  • 7.0.0.5 看下面这段代码,说一下准备阶段和初始化阶段常量变化的原理?变量初始化过程?
  • 7.0.0.7 说收垃圾回收机制?为什么引用计数器判定对象是否回收不可行?有哪些引用类型?
  • 7.0.0.8 谈谈Java的类加载过程?加载做了什么?验证做了什么?准备做了什么?解析做了什么?初始化做了什么?

好消息

  • 博客笔记大汇总【15年10月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计500篇[近100万字],将会陆续发表到网上,转载请注明出处,谢谢!
  • 链接地址:github.com/yangchong21…
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

7.0.0.1 Java内存模型里包含什么?程序计数器的作用是什么?常量池的作用是什么?

  • Java内存模型里包含什么?
    • JVM会用一段空间来存储执行程序期间需要用到的数据和相关信息,这段空间就是运行时数据区(Runtime Data Area),也就是常说的JVM内存。JVM会将它所管理的内存划分为线程私有数据区和线程共享数据区两大类。
    • 线程私有数据区包含:
      • 1.程序计数器:是一个数据结构,用于保存当前正常执行的程序的内存地址。Java虚拟机的多线程就是通过线程轮流切换并分配处理器时间来实现的,为了线程切换后能恢复到正确的位置,每条线程都需要一个独立的程序计数器,互不影响,该区域为“线程私有”。
      • 2.Java虚拟机栈:线程私有的,与线程生命周期相同,用于存储局部变量表,操作栈,方法返回值。局部变量表放着基本数据类型,还有对象的引用。
      • 3.本地方法栈:跟虚拟机栈很像,不过它是为虚拟机使用到的Native方法服务。
    • 线程共享数据区包含:
    • 技术博客大总结
      • 4.Java堆:所有线程共享的一块内存区域,用于存放几乎所有的对象实例和数组;是垃圾收集器管理的主要区域,也被称做“GC堆”;是Java虚拟机所管理的内存中最大的一块。
      • 5.方法区:各个线程共享的区域,储存虚拟机加载的类信息,常量,静态变量,编译后的代码。
      • 6.运行时常量池:代表运行时每个class文件中的常量表。包括几种常量:编译时的数字常量、方法或者域的引用。
  • 程序计数器的作用是什么?
  • 常量池的作用是什么?

7.0.0.2 什么是类加载器?类加载器工作机制是什么?类加载器种类?什么是双亲委派机制?

  • 什么是类加载器?
    • 负责读取 Java 字节代码,并转换成java.lang.Class类的一个实例;
  • 类加载器工作机制是什么
    • 是虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可被虚拟机直接使用的Java类型的过程。另外,类型的加载、连接和初始化过程都是在程序运行期完成的,从而通过牺牲一些性能开销来换取Java程序的高度灵活性。下面介绍类加载每个阶段的任务:
      • 加载(Loading):通过类的全限定名来获取定义此类的二进制字节流;将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义;在内存中生成一个代表这个类的java.lang.Class对象,它将作为程序访问方法区中的这些类型数据的外部接口
      • 验证(Verification):确保Class文件的字节流中包含的信息符合当前虚拟机的要求,包括文件格式验证、元数据验证、字节码验证和符号引用验证
      • 准备(Preparation):为类变量分配内存,因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在Java堆中;设置类变量初始值,通常情况下零值
      • 解析(Resolution):虚拟机将常量池内的符号引用替换为直接引用的过程
      • 初始化(Initialization):是类加载过程的最后一步,会开始真正执行类中定义的Java字节码。而之前的类加载过程中,除了在『加载』阶段用户应用程序可通过自定义类加载器参与之外,其余阶段均由虚拟机主导和控制
  • 类加载器种类?
    • 启动类加载器,Bootstrap ClassLoader,加载JACA_HOME\lib,或者被-Xbootclasspath参数限定的类
    • 扩展类加载器,Extension ClassLoader,加载\lib\ext,或者被java.ext.dirs系统变量指定的类
    • 应用程序类加载器,Application ClassLoader,加载ClassPath中的类库
    • 自定义类加载器,通过继承ClassLoader实现,一般是加载我们的自定义类
    • 技术博客大总结
  • 什么是双亲委派机制?
    • 主要是表示类加载器之间的层次关系
      • 前提:除了顶层启动类加载器外,其余类加载器都应当有自己的父类加载器,且它们之间关系一般不会以继承(Inheritance)关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。
      • 工作过程:若一个类加载器收到了类加载的请求,它先会把这个请求委派给父类加载器,并向上传递,最终请求都传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

7.0.0.3 什么时候发生类初始化?类初始化后对类的做了什么,加载变量,常量,方法都内存那个位置?

  • 什么时候发生类初始化
    • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
      • 调用一个类型的静态方法时(即在字节码中执行invokestatic指令)
      • 调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式
    • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
    • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
    • 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例左后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄锁对应的类没有进行过初始化时。
  • 类初始化后对类的做了什么技术博客大总结
    • 这个阶段主要是对类变量初始化,是执行类构造器的过程。
    • 换句话说,只对static修饰的变量或语句进行初始化。
    • 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
    • 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

7.0.0.4 通过下面一个代码案例理解类加载顺序?当遇到 类名.变量 加载时,只加载变量所在类吗?

  • 代码案例如下所示
    class A{
        public static int value = 134;
        static{
            System.out.println("A");
        }
    }
    
    class B extends  A{
        static{
            System.out.println("B");
        }
    }
    
    
    public class Demo {
       public static void main(String args[]){
           int s = B.value;
           System.out.println(s);
       }
    }
    
  • a.打印错误结果
    A 
    B
    134 
    
  • b.打印正确结果
    A
    134 
    
    • 观察代码,发现B.value中的value变量是A类的。所以,帮主在这里大胆的猜测一下,当遇到 类名.变量 加载时,只加载变量所在类。
  • 如何做才能打印a这种结果呢?
    class A{
        public static int valueA = 134;
        static{
            System.out.println("A");
        }
    }
    
    class B extends  A{
        public static int valueB = 245;
        static{
            System.out.println("B");
        }
    }
    
    public class Demo {
       public static void main(String args[]){
           int s = B.valueB;
           System.out.println(s);
       }
    }
    
    A
    B
    245 
    

7.0.0.5 看下面这段代码,说一下准备阶段和初始化阶段常量变化的原理?

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

7.0.0.7 说收垃圾回收机制?为什么引用计数器判定对象是否回收不可行?

  • 判定对象可回收有两种方法:
    • 引用计数算法:
      • 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。然而在主流的Java虚拟机里未选用引用计数算法来管理内存,主要原因是它难以解决对象之间相互循环引用的问题,所以出现了另一种对象存活判定算法。
    • 可达性分析法:
      • 通过一系列被称为『GC Roots』的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。其中可作为GC Roots的对象:虚拟机栈中引用的对象,主要是指栈帧中的本地变量、本地方法栈中Native方法引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象
  • 回收算法有以下四种:
    • 分代收集算法:是当前商业虚拟机都采用的一种算法,根据对象存活周期的不同,将Java堆划分为新生代和老年代,并根据各个年代的特点采用最适当的收集算法。技术博客大总结
      • 新生代:大批对象死去,只有少量存活。使用『复制算法』,只需复制少量存活对象即可。
      • 老年代:对象存活率高。使用『标记—清理算法』或者『标记—整理算法』,只需标记较少的回收对象即可。
    • 复制算法:把可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用尽后,把还存活着的对象『复制』到另外一块上面,再将这一块内存空间一次清理掉。
    • 标记-清除算法:首先『标记』出所有需要回收的对象,然后统一『清除』所有被标记的对象。
    • 标记-整理算法:首先『标记』出所有需要回收的对象,然后进行『整理』,使得存活的对象都向一端移动,最后直接清理掉端边界以外的内存。
  • 垃圾收集算法分类
    • 标记-清楚算法(Mark-Sweep)
      • 在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。标记—清除算法是基础的收集算法,有两个不足:1)标记和清除阶段的效率不高;2)清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。
    • 复制算法(Copying)
      • 复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的JVM 用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以会分成1块大内存Eden和两块小内存Survivor(大概是8:1:1),每次使用1块大内存和1块小内存,当回收时将2块内存中存活的对象赋值到另一块小内存中,然后清理剩下的。
    • 标记—整理算法(Mark-Compact)
      • 标记—整理算法和复制算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代。
    • 分代收集(Generational Collection)
      • 分代收集是根据对象的存活时间把内存分为新生代和老年代,根据各代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用复制算法,老年代采用标记—整理算法。
  • 为什么引用计数器判定对象是否回收不可行?
    • 实现简单,判定效率高,但不能解决循环引用问题,同时计数器的增加和减少带来额外开销。
  • 引用类型有哪些种
    • 强引用:默认的引用方式,不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
    • 软引用(SoftReference):如果一个对象只被软引用指向,只有内存空间不足够时,垃圾回收器才会回收它;
    • 弱引用(WeakReference):如果一个对象只被弱引用指向,当JVM进行垃圾回收时,无论内存是否充足,都会回收该对象。
    • 虚引用(PhantomReference):虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用通常和ReferenceQueue配合使用。

7.0.0.8 谈谈Java的类加载过程?加载做了什么?验证做了什么?准备做了什么?解析做了什么?初始化做了什么?

  • Java文件从编码完成到最终执行过程
    • 编译:编译,即把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件。
    • 运行:运行,则是把编译声称的.class文件交给Java虚拟机(JVM)执行。
    • 举个通俗点的例子来说,JVM在执行某段代码时,遇到了classA,然而此时内存中并没有classA的相关信息,于是JVM就会到相应的class文件中去寻找classA的类信息,并加载进内存中,这就是我们所说的类加载过程。
  • 谈谈Java的类加载过程?
    • 类加载的过程主要分为三个部分:
    • 加载
    • 链接
      • 而链接又可以细分为三个小部分:
      • 验证
      • 准备
      • 解析
    • 初始化
  • 加载做了什么?
    • 加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。
      • 这里有两个重点:
      • 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
      • 类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
    • 在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下3件事情:
      • 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
      • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
      • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
    • 加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
  • 验证做了什么?技术博客大总结
    • 主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
    • 包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
    • 对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
    • 对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
    • 对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
  • 准备做了什么?
    • 主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
    • 特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。
    • 比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static a = 123, 那么该阶段a的初值就是123
  • 解析做了什么?
    • 将常量池内的符号引用替换为直接引用的过程。
    • 两个重点:
      • 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
      • 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
    • 举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
    • 在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
  • 初始化做了什么?
    • 这个阶段主要是对类变量初始化,是执行类构造器的过程。
    • 换句话说,只对static修饰的变量或语句进行初始化。
    • 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
    • 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

其他介绍

01.关于博客汇总链接

02.关于我的博客