参考文章

JVM 通过 ClassLaoder 将 主机硬盘里的 Class文件(8字节为单位的二进制流,任何编程语言通过各自的编译器都能编译成对应的Class文件) 加载到 JVM的方法区,
并对方法区中 Class字节码进行 校验、解析、和初始化、最终形成可以被JVM直接使用的 数据类型

类加载的过程

  1. 类文件的加载
    • 通过一个类的名字==获取此类的二进制字节流==(PS:不限于从文件中读取)
      • 从EAR包、WAR包、JAR包中获取
      • 从网络中获取,这种场景最典型的应用就是Applet
      • 运行时计算生成,这种场景使用得最多的就是动态代理技术,在java.lang.reflect.Proxy 中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类 的二进制字节流。
      • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。(运行时数据结构是什么,由具体的虚拟机自己定义)
      • 在内存中==生成一个代表这个类的java.lang.Class对象==,作为方法区这个类的各种数据的访问入口。

当类加载完成后,系统会给为之生成一个对象 ==(对象的实例化,还没有被栈里的指针引用)==
defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class(只要二进制字节流的内容符合Class文件规范)。==这个对象作为程序访问方法区中的这些类型数据的外部接口==

==随后进入链接阶段,链接阶段负责把类的二进制数据添加到JRE中。==


类的链接

  1. Class文件的==字节流验证==:
    • 文件格式验证
    • 元数据验证(语义分析,类与类的继承关系等)
    • 字节码验证(数据流和控制流分析)
    • 符号引用验证(对类自身以外的信息进行匹配校验)
  2. 准备
    正式为类变量分配内存并设置默认初始值,这里类变量指的是被static修饰的变量。例外:如果类字段是常量 (被final修饰),则在这里会被初始化为表达式指定的值
  3. 解析:
    将常量池内的符号引用替换为直接引用。
    符号引用:类似于OS中的逻辑地址;
    直接引用:类似于OS中的物理地址,直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。

类的初始化

  1. 初始化:

真正开始执行类中定义的Java程序代码;初始化用于执行Java类的构造方法。
初始化阶段是执行类构造器()方法的过程。

在以下四种情况下初始化过程会被触发执行:

  • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。
    生成这4条指令的最常见的java代码场景是:
    • 使用new关键字实例化对象、
    • 读取或设置一个类的静态字段的时候 (被final修饰、已在编译器把结果放入常量池的静态字段除外[intern()方法]),
    • 调用类的静态方法的时候。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先出发其父类的初始化
  • jvm启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类

举个栗子:
在上面准备阶段 public static int value = 12; 在准备阶段完成后 value的值为0,
而在初始化阶段调用了类构造器()方法,这个阶段完成后value的值为12。

类初始化的过程是不可逆的,如果中间一步出错,则无法执行下一步


类加载器与双亲委派模型

深入探讨 Java 类加载器

  1. Bootstrap ClassLoader :
    将存放于\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用

  2. Extension ClassLoader :
    \lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库加载。开发者可以直接使用扩展类加载器。

  3. Application ClassLoader :
    负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用。

类加载器双亲委派模型
工作过程:
如果一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,
每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中
只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

好处:

  1. 安全性
    避免用户编写的类动态替换Java的一些核心类

java类随着它的类加载器一起具备了一种带有优先级的层次关系
例如类java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类
2. 避免类的重复加载
JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。

相反,如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。

缓存机制

所有加载过的类都会缓存在内存中,
如果程序中尝试使用某个class时,先从缓存中查找这个类;
如果不存在,则读取该类对应的二进制文件并将其转换为class对象并存入缓存区。
这就是为什么类修改后需要重启的原因。

自定义类加载器

java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例

  1. 从上面源码看出,调用loadClass时会先根据委派模型在父加载器中加载,如果加载失败,则会调用当前加载器的findClass来完成加载。

  2. 因此我们自定义的类加载器只需要继承ClassLoader并覆盖findClass方法,下面是一个实际例子,在该例中我们用自定义的类加载器去加载我们事先准备好的class文件。

ClassLoader.defineClass()方法可以把二进制流字节组成的文件转换为一个java.lang.Class

优秀文章 - 类加载分析,自定义类加载器

为什么要自定义类加载器

  • 对代码进行加密
  • 从指定来源加载类
  • 综合前两种情况

类的实例化

  1. Class.forName(“包名.类名”).newInstance(); // 类加载机制
  2. new 类名(); // 类加载的初始化阶段

newInstance()实际上是把new这个方式分解为两步,即首先调用Class加载方法加载某个类,然后实例化。
这样分步的好处是显而易见的。类的加载 和 类实例化 分开处理。
我们可以在调用class的静态加载方法forName时获得更好的灵活性,提供给了一种解耦的手段。

区别:

  1. Class下的newInstance()的使用有局限,因为它生成对象只能调用无参的构造函数;使用 new关键字生成对象没有这个限制。
  2. newInstance( )是一个方法,而new是一个关键字;

参考文章:

  1. 垃圾回收机制 && 弱引用

    GC


参考文章

  1. Java虚拟机学习 - 体系结构 内存模型

    JVM内存模型

image

JDK1.7以前 常量池是放在Java JVM中的方法区中的,许多人也叫它“永久代”

JDK1.7中,这个常量池是移动到了java堆中去了

JDK1.8中,移除整个永久代(常量池),取而代之的是一个叫元空间(Metaspace)的区域,(使用本地内存来存储类元数据信息)

内存分为哪几部分

1.