JVM类加载

#1. Java对象的创建过程

类加载检查 ===> 分配内存 ===> 初始化零值 ===> 设置对象头 ===> 执行 init 方法

#1.1 类加载检查

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

#1.2 分配内存

在类加载检查通过后,虚拟机将为新生对象分配内存。

对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式包括指针碰撞空闲列表选择哪种方式取决于 Java 堆是否规整,Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

  • 指针碰撞:适用于堆内存规整(没有内存碎片)的情形。原理:用过的内存整理到一边,未使用的内存放在另一边,中间有一个分界值指针,只需要向着未使用过的内存方向将该指针移动对象内存大小位置即可。采用的 GC 收集器有 Serial 和 ParNew(因为使用标记-整理,不存在内存碎片)。
  • 空闲列表:适用于堆内存不规整的情形。原理:虚拟机维护一个列表,该列表中记录哪些内存块可用,在分配的时候,找一块足够大的内存块来划分给对象实例,最后更新列表记录。采用 GC 收集器为 CMS(因为采用标记-清除算法,堆内存不规整)。

内存分配的并发问题,虚拟机采用两种方式来保证线程安全。

  • CAS + 失败重试:CAS 是乐观锁的一种实现方式。所谓乐观锁就是每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB:为每一个线程先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中剩余内存或 TLAB 内存用尽时,再采用 CAS 进行内存分配。
#1.3 初始化零值

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

#1.4 设置对象头

初始化零值之后需要对对象头进行必要的设置。

#1.5 执行init方法

上面工作完成后,在虚拟机的视角,新的对象已经产生。从 Java 程序的视角,执行new指令后就会执行 init 方法,把对象按照程序员的医院进行初始化,从而得到一个真正可用的对象。

#2. 对象访问定位的方式

① 使用句柄:在 Java 堆中创建句柄池,句柄包含对象实例数据和对象类型数据,本地变量表中的 reference 存储对象的句柄地址。

② 直接指针:本地变量表中存储的直接就是对象的地址。

直接指针速度快,而使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。

#3. Java中Class文件字节码结构

class-file-structure

#4. 类加载过程和类的生命周期

类加载过程:加载、连接、初始化

类的生命周期:加载、连接、初始化、使用、卸载

  • 加载:通过全类名获取定义此类的字节流;将字节流所代表的静态存储结构转换为方法去的运行时数据结构;在堆内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
  • 连接:包括三步:验证、准备、初始化。① 验证:验证文件格式、元数据、字节码符号引用;② 准备:为类的静态变量分配内存,并将其初始化默认值;③ 解析:把类中的符号引用转换为直接引用。
  • 初始化:维蕾德静态变量赋予正确的初始值。
  • 使用:new 出对象在程序中使用。
  • 卸载:执行垃圾回收。

#5. 类加载器

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。

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

  • BootstrapClassLoader(启动类加载器):最顶层的加载器,由 C++ 实现,负责加载Java核心类库,即%JAVA_HOME%/lib目录下的 jar 包和类。
  • ExtensionClassLoader(扩展类加载器):负载加载 Java 扩展库,即%JRE_HOME%/lib/ext目录下的 Jar 包和类。
  • AppClassLoader(应用程序加载器):负责加载当前拥有classpath下的所有 jar 包和类。

#6. 双亲委派

#6.1 双亲委派机制

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

在类加载的时候,首先判断该类是否被加载,已经加载过的类无需加载会直接返回,负责会自己尝试加载。加载的时候,首先会把该请求委派给父类加载器进行处理,因此所有的请求最终都会传送到顶层的启动类加载 BootstrapClassLoader 加载器中。当父类加载加载器无法处理时,才会自己进行处理。当父类加载器为 null 时,会使用 BootstrapClassLoader 作为父类加载器。

parents-delegate-1.png

parents-delegate-2.png

#6.2 双亲委派机制的作用
  • 避免类的重复加载。即使是相同的类文件,被不同的类加载器加载后产生的也是不同的两个类。
  • 保证了核心 API 不被篡改。
  • 保证了 Java 程序的稳定运行。
#6.3 打破双亲委派机制的方法
  • 自定义一个类加载器,重写 loadClass() 方法。
  • 引入线程上下文类加载器
#6.4 打破双亲委派机制的场景
#6.4.1 JDBC

JDBC:使用线程上下文类加载器(Thread Context ClassLoader)

JDBC 的 Driver 接口定义在 JDK 中,其实现由数据库的服务商来提供,比如 MySQL 驱动包。DriverManager 类中要加载各个实现了 Driver 接口的类,然后进行管理,其由 BootstrapClassLoader 加载,而其 Driver 接口的实现类是位于服务商提供的 Jar 包,根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。也就是说 BootstrapClassLoader 还要去加载 jar 包中的 Driver 接口的实现类。然而 BootstrapClassLoader 默认只负责加载$JAVA_HOME中jre/lib/rt.jar里所有的类,所以需要由子类加载器去加载 Driver 实现,这就破坏了双亲委派模型。

#6.4.2 Tomcat

Tomcat:自定义类加载器,Tomcat 的类加载器如下图。

tomcat-classloader.png

每个 Tomcat 的 WebappClassLoader 加载自己目录下的 class 文件,不会传递给父类加载器,破坏了双亲委派机制。

Tomcat 自定义了很多来加载器,可能处于以下目的:

  • 对于各个webapp中的classlib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的 lib 以便不浪费资源;
  • JVM一样的安全性问题。使用单独的ClassLoader去装载Tomcat自身的类库,以免其他恶意或无意的破坏;
  • 热部署。Tomcat修改文件不用重启就会自动重新装载类库。