JVM 类加载

类文件的结构

一、魔数(Magic Number)

每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。

二、Class 文件版本号(Minor&Major Version)

紧接着魔数的四个字节存储的是 Class 文件的版本号:前两个字节是次版本号,后两个字节是主版本号

每当 Java 发布大版本(比如 Java 7,Java8)的时候,主版本号都会加 1。

高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。

三、常量池(Constant Pool)

紧接着主次版本号之后的是常量池,常量池的数量是两个字节的 constant_pool_count-1(常量池计数器是从 1 开始计数的,第 0 项常量有特殊考虑,当值为 0 代表“不引用任何一个常量池项”)。

常量池主要存放:字面量符号引用,字面量是比如字符串、final常量。

符号引用包含三类:

  • 类和接口的全限定名。
  • 字段的名称和描述。
  • 方法的名称和描述。

常量池中的每一项都是一个表,这个表的第一位都是用来标识常量类型的。

四、访问标识(Access Flags)

标识类或接口的层次的访问信息,包括这个Class是类还是接口,是否为public或者abstract类型,如果是类的话,是否被申明为final等。

五、当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合

u2             this_class;//当前类全限定名
u2             super_class;//父类全限定名
u2             interfaces_count;//实现的接口数量
u2             interfaces[interfaces_count];//一个类可以实现多个接口

六、字段表集合(Fields)

字段数量:两个字节描述字段数量。

字段表:字段表中的项包括访问标识、名称、对常量池的引用、一些额外的属性。

字段表用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。

七、方法集合(Methods)

方法数量:两个字节标识方法数量

方法表:方法表与字段表类似,表中项包括访问标识、名称、对常量池的引用、一些额外的属性。

八、属性表(Attributes)

与字段和方法类似,Class文件也有自己的属性表集合。

类加载

类的生命周期?

  • 加载 -> 连接 -> 初始化 -> 使用 -> 卸载

  • 连接又分为三步:验证 -> 准备 -> 解析

一、类加载

类加载主要完成三件事:

  • 通过全类名获取定义此类的二进制字节流
  • 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  • 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

二、验证

  • 文件格式验证:如魔数是否正确,主版本号是否能被当前虚拟机处理,常量池的类型是否被支持。
  • 元数据验证:对字节码描述的信息进行语义分析,例如:类是否有父类,是否继承了不该被继承的类等
  • 字节码验证:通过数据流和控制流分析,确定程序语义是否合法、符合逻辑。比如保证任意时刻操作数栈和指令代码序列都能配合工作。
  • 符号引用验证:确保解析动作能正确执行。

三、准备

准备阶段正式为类变量分配内存并设置类变量初始值。注意点:

  • 这时候进行内存分配的仅包括类变量,即静态变量,而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  • 从概念上讲,类变量所使用的内存都应当在 方法区 中进行分配。JDK 7 之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。
  • 里所设置的初始值"通常情况"下是数据类型默认的零值(如 0、0L、null、false 等),只有final才在准备阶段赋值。

四、解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。

五、初始化

初始化阶段是执行初始化方法方法的过程,是类加载的最后一步,同时也是对象创建的第一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

对于初始化方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个进程阻塞,并且这种阻塞很难被发现。

虚拟机对类进行初始化的五种情况?
  • 当遇到 new 新对象,访问静态变量(不是静态常量,静态常量在常量池中),给静态变量赋值、调用类的静态方法,会初始化类。
  • 使用 java 反射对类进行反射调用,比如 newInstance(), Class.forname("...")等,如果类没有初始化,会触发类初始化。
  • 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  • 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  • MethodHandleVarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。
  • 当一个接口中定义了 JDK8 新加入的默认方法时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

六、卸载

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用(不被反射调用,class.forname(""))
  3. 该类的类加载器的实例已被 GC

所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

类加载器总结?

JVM内置了三个重要的类加载器。

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

类加载的双亲委派模式?

  • 自底向上检查类是否被加载,如果加载过,直接返回。
  • 自顶向下尝试加载类,如果顶层类加载器加载失败,才会由底层类加载器加载。
  • 首先检查用户自定义类是否加载了此类,然后检查应用程序类加载器,扩展类加载器,启动类加载器。最后依次由上到下尝试加载。

双亲委派模式的好处?

  • 双亲委派模型可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)。

  • 也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

如果我们不想用双亲委派模型怎么办?

继承ClassLoader类,重写 loadClass() 方法。

如何自定义类加载器?

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

jvm常见参数总结?

// 堆内存 -Xms<heap size>[unit] 
-Xms2G                              堆初始化内存2G
-Xmx5G                              最大内存5G

// 新生代内存  -XX:NewSize=<young size>[unit] 
-XX:NewSize=256m        新生代最小内存256m
-XX:MaxNewSize=1024m  新生代最大内存1024m

// 通过-Xmn<young size>[unit]指定新生代内存
-Xmn256m                        新生代最大内存和最小内存都是256m
  
// 通过 XX:NewRatio=<int> 设置新生代与老年代的比值
-XX:NewRatio=1              新生代:老年代 的比值为1:1

// 显示的指定元空间的大小
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存

 //显示指定选择垃圾收集器
 -XX:+UseSerialGC
 -XX:+UseParallelGC
 -XX:+UseParNewGC
 -XX:+UseG1GC
  

JVM 调优

JVM 调优常见工具?

JDK命令行工具:
  • jps:
  • jstat:
  • jmap:
  • jhat:
  • jhat:
JDK 可视化分析工具:
  • JConsole:

  • Visual VM:

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容