概念:

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

类加载器

JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器。

Bootstrap ClassLoader: 它是根类加载器,由c++编写,JVM启动时加载它,然后它加载另外两个类加载器,它还会加载$JAVA_HOME中jre/lib/rt.jar里所有的class。

Extension ClassLoader: 扩展类加载器,负责加载$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

App ClassLoader: 应用类加载器,一般我们用的类都是由它加载的,当然我们可以自定义类加载器,此时就是以它为父类加载器。

自定义类加载器: 可以通过继承 ClassLoader并重写findClass方法来实现。

1. 类加载过程

JVM将类加载过程分为三个阶段,装载,链接,初始化。链接又可以分为三个阶段验证准备解析。

  • 装载:根据查找路径找到响应的.class文件加载进内存,并为之创建Class对象
  • 验证:保证被加载类的正确性
  • 准备:为类的静态变量分配内存,并初始化为默认值。这一步和初始化有区别。比如static int a=1。那么到这部分配默认值是0,初始化的时候才是1.
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
  • 初始化:对静态变量和静态代码块执行初始化工作。

1.1 加载

1.1.1过程

通过类的全限定名来获取定义此类的二进制流

  • 从jar、war包中获取
  • 从网络中获取:Applet
  • 运行时计算生成:动态代理技术
  • 其他文件生成:JSP

将字节流代表的静态储存结构转化为方法区的运行时数据结构

在内存中生成一个Class对象,作为方法区该类各种数据的访问入口

1.1.2 加载与连接的部分内容是交叉进行的

1.2 验证

1.3 准备

正式为类变量分配内存并设置类变量零值的阶段(final的变量会赋实际值)

1.4 解析

1.5 初始化

通过数组定义引用类

1
2
3
4
5
public class Test {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}

类的初始化时机

当 Java 虚拟机在初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口

  • 在初始化一个类时,并不会先初始化它所实现的接口
  • 在初始化一个接口时,并不会先初始化它的父接口

因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化

类加载的时机

  1. new一个对象的时候,也就是创建类实例的时候
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(Class.forName(“xxx.xxx”))
  5. 初始化一个类的子类(会首先初始化子类的父类)
  6. JVM启动时标明的启动类,即文件名和类名相同的那个类

双亲委派模型

工作原理:
当一个类加载器要加载某一个类的时候,它会先去找它的父类加载器去执行加载,如果父类加载器还有父类,就依次向上请求,直到.Bootstrap ClassLoader,如果父类能够完成加载就返回请求,如果不行,则由子类加载。

好处

1.可以避免重复加载,如果父类加载器已经加载过此类的时候,子类加载器则不用再加载一次。则保证了java中类名的唯一性

2.保证java核心api中定义类型不会被随意替换,因为我们的根类记载器是会加载一些核心API的,如果此时传来一个与核心API类名相同的类,我们不会加载它,避免了核心API类不被修改。

2. 类文件结构

2.1 class文件是一组8位字节为基础单位的二进制流,大端

2.2 数据类型

  1. 无符号数
  • u1、u2、u4、u8表示长度为1、2、4、8个字节(8位))
  • 表示数字、索引引用、数量值、字符串值

2.3 组成

0~3字节:魔数:文件类型

4~7字节:jdk本号

常量池

字面量:常量字符串、final常量值

符号引用

  1. 类和接口的fully Qualified Name
  2. 字段的方法和描述符
  3. 方法的名称和描述符

u2访问标志:类/接口、public、final、abstract

继承关系

  • u2类索引:类的全限定名
  • u2父索引:父类的全限定名
  • nu2+1接口索引:实现接口的全新定名

字段表集合:描述接口、变量

  • u2访问标志
  • u2 name_index
  • u2 descriptor_index
  • u2 attributes_count
  • u2 attributes

方法表集合: 描述方法

属性表集合

  • code属性
  • exception属性
  • LineNumberTable属性
  • LocalVariableTable属性
  • sourceFile属性
  • constantvalue属性:通知虚拟机自动为静态变量赋值
  • innerClass属性
  • Deprecated和Synthetic属性
  • stackMapTable属性
  • Signature属性:记录泛型信息
  • BootstrapMethod属性

3. 对象的创建

Java中创建对象的方式

  1. 使用new关键字
  2. 使用Class的newInstance方法
  3. 使用Constructor类的newInstance方法
  4. 使用clone方法
  5. 使用反序列化

3.1 对象的创建过程

1.类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

举个例子,我们的string就算是对象创建,既是 String str= new String(“abc”); 首先也会在常量池中定位是否存在值。若不存在,则会在常量池也建立一个abc.

2.分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来

分配方式

  1. 指针碰撞(内存规整)
  • 如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
  1. 空闲列表(内存不规整)
  • 如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录
  1. 线程安全:本地线程分配缓冲TLAB,每个线程一个空间,不干涉

3.初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4.设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

5.执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来

3.2 对象内存布局

对象头

运行时数据(Mark Word):哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳

类型指针 :确定是哪个类的实例

实例数据

代码中定义的各种类型字段内容(包括从父类继承的)

对齐填充

占位符

3.3 对象的访问定位

通过栈中局部变量表中对象引用,方式:句柄直接指针

句柄

如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息

直接指针

如果使用直接指针访问,那么 Java 堆对像的布局中就必须考虑如何防止访问类型数据的相关信息,reference 中存储的直接就是对象的地址。

小总结

使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销

4.方法调用

含义:确定被调用的方法的版本

4.1 分类

解析

编译期可知,运行期不变:在类加载时的解析阶段,从常量池把符号引用转化为直接引用

非虚方法

  • 静态方法
  • 私有方法
  • 实例构造器
  • 父类方法

分派 dispatch

静态类型与实际类型

1
Human man = new Man();

Human是man的静态类型
Man是man的实际类型

静态分派:依赖静态类型来定位方法执行版本的分派

  • 发生在编译阶段
  • 典型是方法重载
  • 方法根据参数,自动选择最符合的版本,可能发生自动转换
    • (char->int->long->float->double)
    • 可能发生自动装箱、转换为实现的接口
    • 继承按从下到上的顺序

动态分派:依赖实际类型来确定方法执行版本的分派

  • 典型是重写
  • 发生在运行期间