类文件的结构
一、魔数(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
方法的那个类),虚拟机会先初始化这个类。 -
MethodHandle
和VarHandle
可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用findStaticVarHandle
来初始化要调用的类。 - 当一个接口中定义了 JDK8 新加入的默认方法时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
六、卸载
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用(不被反射调用,class.forname(""))
- 该类的类加载器的实例已被 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: