在这里插入图片描述
在这里插入图片描述

一、类加载过程

一个类的完整生命周期

在这里插入图片描述

类加载过程

Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?

系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
在这里插入图片描述

1 加载阶段 - 生成Class实例

加载过程

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

补充说明

一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。

数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。

类加载器、双亲委派模型也是非常重要的知识点,这部分内容会在后面会单独介绍到。

加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。

2 连接(Linking)阶段 - 重要

链接分为三个子阶段:验证 --> 准备 --> 解析

2.1 验证
  • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
  • 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
2.2 准备

准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配(JDK 7 之前,JDK 7 及之后在堆中)。

  • 这时候进行内存分配的仅包括类变量( 即静态变量,被 static 关键字修饰的变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  • 从概念上讲,类变量所使用的内存都应当在 方法区 中进行分配。
    • 不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。
    • 而在DK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中
  • 这里所设置的初始值"通常情况"下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。
    • 特殊情况:比如给 value 变量加上了 final 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。`
2.3 解析
  • 常量池内的符号引用转换为直接引用的过程
    • 符号引用就是一组符号来描述目标,可以是任何字面量。
    • 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
  • 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等

3 初始化(Initialization)阶段

  • 初始化阶段是执行初始化方法 < clinit > ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

  • 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来

  • < clinit > ()方法中的指令按语句在源文件中出现的顺序执行

  • < clinit > ()不同于类的构造器。(关联:构造器是虚拟机视角下的< init >()

  • 若该类具有父类,JVM会保证子类的< clinit >()执行前,父类的< clinit >()已经执行完毕

  • 虚拟机必须保证一个类的< clinit >()方法在多线程下被同步加锁,所以在多线程环境下进行类初始化的话可能会引起多个进程阻塞,并且这种阻塞很难被发现。

4 卸载阶段

卸载类即该类的 Class 对象被 GC。

卸载类需要满足 3 个要求:

  • 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  • 该类没有在其他任何地方被引用
  • 该类的类加载器的实例已被 GC

在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。

但是由我们自定义的类加载器加载的类是可能被卸载的。 只要想通一点就好了,jdk 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 jdk 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

二、类加载器详解

1 类加载器总结

JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader:

  • 启动类加载器(引导类加载器,BootStrap ClassLoader):最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。Object类在标准库lib目录下,由Bootstrap Classloader加载。
  • ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
  • AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。 父类加载器为扩展类加载器。

2 自定义类加载器

除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部 继承自java.lang.ClassLoader 。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader。

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。只有Bootstrap ClassLoader是必须用到的。

那为什么还需要自定义类加载器?

  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄露

自定义类加载器实现步骤:

  • 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
    • 在JDK1.2之前 ,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类;
    • JDK1.2之后 已不再建议用户去覆盖loadClass( )方法,而是建议把 自定义的类加载逻辑写在findclass()方法中
  • 在编写自定义类加载器时,如果没有太过于复杂的需求,可以 直接继承 URIClassLoader 类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

3 双亲委派模型

3.1 什么是双亲委派模型?

每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
  4. 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常

在这里插入图片描述

3.2 双亲委派模型的好处?为什么要设计双亲委派机制?
  • 沙箱安全机制:如果有人想替换系统级别的类:String.java是不会被加载的,这样便可以防止核心API库被随意篡改。
  • 避免类的重复加载:当父加载器已经加载了该类时,就没有必要子加载器再加载一次,保证被加载类的唯一性。
3.3 如果我们不想用双亲委派模型怎么办? (自定义类加载器)

自定义加载器的话,需要继承 ClassLoader 。

  • 如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。
  • 但是,如果想打破双亲委派模型则需要重写 loadClass() 方法
  • 打破双亲委派机制后,想干啥都可以双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader 的 loadClass() 中。
3.4 Class.forName()和ClassLoader的区别

在java中Class.forName()和ClassLoader都可以对类进行加载,它们都针对的是加载过程。二者只是加载.class文件,需要进一步调用newInstance()来创建对象。

Class.forName()

Class.forName()前者除了将类的.class文件加载到JVM中之外,还会对类进行解释,执行类中的static块。注意这里的静态块指的是在类初始化时的一些数据,但是Classloader却没有。

Class.forName()方法实际上也是调用的CLassLoader来实现的。 内部实际调用的方法是Class.forName(className, true, classloader)

@CallerSensitive
public static Class<?> forName(String className)
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
  • className:表示我们要加载的类名
  • true:指Class被加载后是不是必须被初始化。 不初始化就是不执行static的代码即静态代码,在这里默认为true,也就是默认实现类的初始化。
  • ClassLoader.getClassLoader(caller):表示类加载器,到这你会发现forNanme其实也是使用的ClassLoader类加载器加载的。
  • caller:指定类加载器。

ClassLoader

ClassLoader是遵循双亲委派模型最终调用启动类加载器的类加载器,内部实际调用的方法是 ClassLoader.loadClass(className, false)。

先判断class是否已经被加载,如果被加载了那就重新加载;如果没有加载那就使用双亲委派原则加载。加载的时候并没有指定是否要进行初始化。

它不会执行static中的内容,只有在newInstance才会去执行static块

new一个对象和forName的区别
  • newInstance( )是一个方法,而new是一个关键字;
  • 从JVM的角度看:
  • 使用关键字new创建一个类的时候,这个类可以没有被加载;
  • 使用newInstance()方法的时候,就必须保证:
    • (1) 这个类已经加载
    • (2) 这个类已经连接了
    • 而完成上面两个步骤的正是Class的静态方法forName()所完成的,这个静态方法调用了启动类加载器,即加载 Java API的那个加载器。
  • Class下的newInstance()生成对象只能调用无参的构造函数,而使用 new关键字生成对象没有这个限制。

三、JVM常用调优参数

  • 堆栈内存相关
    • -Xms 设置初始堆的大小
    • -Xmx 设置最大堆的大小
    • -Xmn 设置年轻代大小,相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值
    • -Xss 每个线程的堆栈大小
    • -XX:NewSize 设置年轻代大小(for 1.3/1.4)
    • -XX:MaxNewSize 年轻代最大值(for 1.3/1.4)
    • -XX:NewRatio 年轻代与年老代的比值(除去持久代)
    • -XX:SurvivorRatio Eden区与Survivor区的的比值
    • -XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。
    • -XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代
  • 垃圾收集器相关
    • -XX:+UseParallelGC:选择垃圾收集器为并行收集器。
    • -XX:ParallelGCThreads=20:配置并行收集器的线程数
    • -XX:+UseConcMarkSweepGC:设置年老代为并发收集。
    • -XX:CMSFullGCsBeforeCompaction=5 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行5次GC以后对内存空间进行压缩、整理。
    • -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是
      可以消除碎片
  • 辅助信息相关
    • -XX:+PrintGCDetails 打印GC详细信息
    • -XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,排查问题用
    • -XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题.
    • -XX:+PrintTLAB 查看TLAB空间的使用情况
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐