文章目录
1.概述
-
在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。
-
按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:
其中,验证、准备、解析3个部分统称为链接(Linking) -
从程序中类的使用过程看
2.过程一:Loading(加载)阶段
2.1 加载完成的操作
2.2 二进制流的获取方式
2.3 类模型与Class实例的位置
类模型的位置
加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDK1.8之前:永久代;J0K1.8及之后:元空间)。
Class实例的位置
类将.class文件加载至元空间后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象。
图示
- 外部可以通过访问代表Order类的Class对象来获取Order的类数据结构
再说明
Class类的构造方法是私有的,只有JVM能创建。
java.lang.Class 实例是访问类型元数据的接口,也是实现反射的关键数据、入口。通过Class类提供的接口,可以获得目标类所关联的 .class 文件中具体的数据结构:方法、字段等信息。
举例
/**
* @ClassName LoadingTest
* @author: shouanzh
* @Description 通过Class类得了 java.lang.String 类所以方法信息
* 并打印方法访问标识符、描述符
* @date 2022/2/21 17:02
*/
public class LoadingTest {
public static void main(String[] args) {
try {
Class clazz = Class.forName("java.lang.String");
// 获取当前运行时类声明的所有方法
Method[] ms = clazz.getDeclaredMethods();
for (Method m : ms) {
// 获取方法的修饰符
String mod = Modifier.toString(m.getModifiers());
System.out.print(mod + " ");
// 获取方法的返回值类型
String returnType = (m.getReturnType()).getSimpleName();
System.out.print(returnType + " ");
// 获取方法名
System.out.print(m.getName() + "(");
// 获取方法的参数列表
Class<?>[] ps = m.getParameterTypes();
if (ps.length == 0) {
System.out.print(')');
}
for (int i = 0; i < ps.length; i++) {
char end = (i == ps.length - 1) ? ')' : ',';
// 获取参数的类型
System.out.print(ps[i].getSimpleName() + end);
}
System.out.println();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
public boolean equals(Object)
public String toString()
public int hashCode()
public volatile int compareTo(Object)
public int compareTo(String)
public int indexOf(String,int)
static int indexOf(char[],int,int,String,int)
static int indexOf(char[],int,int,char[],int,int,int)
public int indexOf(int)
public int indexOf(String)
public int indexOf(int,int)
public static String valueOf(char)
public static String valueOf(Object)
public static String valueOf(boolean)
public static String valueOf(char[],int,int)
public static String valueOf(char[])
public static String valueOf(double)
public static String valueOf(float)
public static String valueOf(long)
public static String valueOf(int)
private static void checkBounds(byte[],int,int)
public int length()
public boolean isEmpty()
public char charAt(int)
public int codePointAt(int)
public int codePointBefore(int)
public int codePointCount(int,int)
public int offsetByCodePoints(int,int)
public void getChars(int,int,char[],int)
void getChars(char[],int)
public byte[] getBytes()
public byte[] getBytes(String)
public void getBytes(int,int,byte[],int)
public byte[] getBytes(Charset)
public boolean contentEquals(StringBuffer)
public boolean contentEquals(CharSequence)
private boolean nonSyncContentEquals(AbstractStringBuilder)
public boolean equalsIgnoreCase(String)
public int compareToIgnoreCase(String)
public boolean regionMatches(int,String,int,int)
public boolean regionMatches(boolean,int,String,int,int)
public boolean startsWith(String)
public boolean startsWith(String,int)
public boolean endsWith(String)
private int indexOfSupplementary(int,int)
public int lastIndexOf(int,int)
static int lastIndexOf(char[],int,int,char[],int,int,int)
static int lastIndexOf(char[],int,int,String,int)
public int lastIndexOf(String,int)
public int lastIndexOf(int)
public int lastIndexOf(String)
private int lastIndexOfSupplementary(int,int)
public String substring(int)
public String substring(int,int)
public CharSequence subSequence(int,int)
public String concat(String)
public String replace(char,char)
public String replace(CharSequence,CharSequence)
public boolean matches(String)
public boolean contains(CharSequence)
public String replaceFirst(String,String)
public String replaceAll(String,String)
public String[] split(String,int)
public String[] split(String)
public static transient String join(CharSequence,CharSequence[])
public static String join(CharSequence,Iterable)
public String toLowerCase(Locale)
public String toLowerCase()
public String toUpperCase()
public String toUpperCase(Locale)
public String trim()
public char[] toCharArray()
public static transient String format(Locale,String,Object[])
public static transient String format(String,Object[])
public static String copyValueOf(char[],int,int)
public static String copyValueOf(char[])
public native String intern()
2.4 数组类的加载
3.过程二:Linking(链接)阶段
3.1 环节1:链接阶段之Verification(验证)
- 当类加载到系统后,就开始链接操作,验证是链接操作的第一步。
- 它的目的是保证加载的字节码是合法、合理并且符合规范的
- 验证的步骤比较复杂,实际要验证的项目也很繁多,大体上Java虚拟机需要做以下检查,如图所示。
具体说明
3.2 环节2:链接阶段之Preparation(准备)
准备阶段(Preparation),简言之,为类的静态变量分配内存,并将其初始化为默认值。
// 链接: 验证 准备(static的变量赋初始值 如果是static final修饰的话就赋实际值) 解析
/**
* 基本数据类型:非 final 修饰的变量,在准备环节进行默认初始化赋值
* final 修饰以后,在准备环节直接进行显式赋值
*
* 拓展:如果使用字面量的方式定义一个字符串的常量的话,也是在准备环节直接进行显式赋值
*/
public class LinkingTest {
private static long id;
private static final int num = 1;
public static final String constStr = "CONST";
public static final String constStr1 = new String("CONST");
}
3.3 环节3:链接阶段之Resolution(解析)
解析阶段(Resolution),简言之,将类、接口、字段和方法的符号引用转为直接引用。
举例
小结
- 所谓解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。因此,可以说,如果直接引用存在,那么可以肯定系统中存在该类、方法或者字段。但只存在符号引用,不能确定系统中一定存在该结构。
- 不过Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在Hotspot VM 中,加载、验证、准备和初始化会按照顺序有条不紊的执行,但是链接阶段中的解析操作往往会伴随着JVM在执行完初始化操作之后再执行。
4.过程三:Initialization(初始化)阶段
初始化阶段,简言之,为类的静态变量赋予正确的初始值。
Java编译器并不会为所有的类都产生<clinit>()初始化方法。哪些类在编译为字节码后,字节码文件中将不会包含<clinit>()方法?
- 一个类中并没有声明任何的类变量,也没有静态代码块时
- 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
- 一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式 (如果这个static final 不是通过方法或者构造器,则在链接阶段)
/**
*
* 哪些场景下,Java 编译器就不会生成<clinit>()方法
*/
public class InitializationTest1 {
// 场景1:对应非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
public int num = 1;
// 场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法
public static int num1;
// 场景3:比如对于声明为 static final 的基本数据类型的字段,
// 不管是否进行了显式赋值,都不会生成<clinit>()方法
public static final int num2 = 1;
}
4.1 初始化阶段赋值与准备阶段赋值对比
使用 static + final 修饰,且显式赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行
/**
*
* 说明:使用 static + final 修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值?
* 情况1:在链接阶段的准备环节赋值
* 情况2:在初始化阶段<clinit>()中赋值
*
* 结论:
* 在链接阶段的准备环节赋值的情况:
* 1. 对于基本数据类型的字段来说,如果使用 static final 修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行
* 2. 对于 String 来说,如果使用字面量的方式赋值,使用 static final 修饰的话,则显式赋值通常是在链接阶段的准备环节进行
*
* 在初始化阶段<clinit>()中赋值的情况
* 排除上述的在准备环节赋值的情况之外的情况
*
* 最终结论:使用 static + final 修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行
*/
public class InitializationTest2 {
public static int a = 1; //在初始化阶段<clinit>()中赋值
public static final int INT_CONSTANT = 10; //在链接阶段的准备环节赋值
public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100); //在初始化阶段<clinit>()中赋值
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000); //在初始化阶段<clinit>()中赋值
public static final String s0 = "helloworld0"; //在链接阶段的准备环节赋值
public static final String s1 = new String("helloworld1"); //在初始化阶段<clinit>()中赋值
}
4.2 < clinit>()的线程安全性
class StaticA {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.jvm.StaticB");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticA init OK");
}
}
class StaticB {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.jvm.StaticA");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticB init OK");
}
}
public class StaticDeadLockMain extends Thread {
private char flag;
public StaticDeadLockMain(char flag) {
this.flag = flag;
this.setName("Thread" + flag);
}
@Override
public void run() {
try {
Class.forName("com.jvm.Static" + flag);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(getName() + " over");
}
public static void main(String[] args) throws InterruptedException {
StaticDeadLockMain loadA = new StaticDeadLockMain('A');
loadA.start();
StaticDeadLockMain loadB = new StaticDeadLockMain('B');
loadB.start();
}
}
4.3 类的初始化情况:主动使用vs被动使用
主动使用< clinit>() 会被调用。被动使用不会调用
-XX:+TraceClassLoading:追踪打印类的加载信息
主动使用1: 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化
/**
* 反序列化
*/
Class Order implements Serializable {
static {
System.out.println("Order类的初始化");
}
}
public void test() {
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
// 序列化
oos = new ObjectOutputStream(new FileOutputStream("order.dat"));
oos.writeObject(new Order());
// 反序列化
ois = new ObjectInputStream(new FileOutputStream("order.dat"));
Order order = ois.readObject();
}
catch (IOException e){
e.printStackTrace();
}
catch (ClassNotFoundException e){
e.printStackTrace();
}
finally {
try {
if (oos != null) {
oos.close();
}
if (ois != null) {
ois.close();
}
}
catch (IOException e){
e.printStackTrace();
}
}
}
主动使用2: 当调用类的静态方法时,即当使用了字节码invokestatic指令。
public class ActiveUse {
public static void main(String[] args) {
Order.method(); // Order类的初始化 Order method()...
}
}
class Order {
static {
System.out.println("Order类的初始化");
}
public static void method() {
System.out.println("Order method()...");
}
}
主动使用3: 当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用getstatic或者putstatic指令。(对应访问变量、赋值变量操作)
public class ActiveUse {
@Test
public void test() {
System.out.println(User.num); // User类的初始化 1
}
}
class User {
static {
System.out.println("User类的初始化");
}
public static int num = 1;
public static final int num1 = 1; // 使用这个不会初始化
}
public class ActiveUse {
public static void main(String[] args) {
System.out.println(Compare.num); // Compare接口的初始化 1
}
}
interface Compare {
public static final Thread t = new Thread() {
{
System.out.println("Compare接口的初始化");
}
};
public static final int num = Integer.valueOf(1);
}
主动使用4: 当使用java.lang.reflect包中的方法反射类的方法时
比如:Class.forName(“com.jvm.lecture.classdemo.Order”)
public class ActiveUse {
public static void main(String[] args) {
try {
// 主动加载一个类
Class.forName("com.jvm.lecture.classdemo.Order"); // Order类的初始化
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Order {
static {
System.out.println("Order类的初始化");
}
public static void method() {
System.out.println("Order method()...");
}
}
主动使用5: 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
public class ActiveUse {
public static void main(String[] args) {
SonTest1 sonTest1 = new SonTest1();
// FatherTest类的初始化...
// SonTest1类的初始化...
}
}
class FatherTest {
static {
System.out.println("FatherTest类的初始化...");
}
}
class SonTest1 extends FatherTest{
static {
System.out.println("SonTest1类的初始化...");
}
}
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
在初始化一个类时,并不会先初始化它所实现的接口
在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。
在初始化一个类时,并不会先初始化它所实现的接口:
public class ActiveUse {
public static void main(String[] args) {
// interface Compare 不会被初始化
SonTest1 sonTest1 = new SonTest1();
// FatherTest类的初始化...
// SonTest1类的初始化...
}
}
class FatherTest {
static {
System.out.println("FatherTest类的初始化...");
}
}
class SonTest1 extends FatherTest implements Compare{
static {
System.out.println("SonTest1类的初始化...");
}
}
interface Compare {
public static final Thread t = new Thread() {
{
System.out.println("Compare接口的初始化");
}
};
public static final int num = Integer.valueOf(1);
}
在初始化一个接口时,并不会先初始化它的父接口:
public class ActiveUse {
public static void main(String[] args) {
System.out.println(Compare.num);
// Compare接口的初始化
// 1
}
}
interface Compare extends CompareA{
public static final Thread t = new Thread() {
{
System.out.println("Compare接口的初始化");
}
};
public static final int num = Integer.valueOf(1);
}
interface CompareA {
public static final Thread t = new Thread() {
{
System.out.println("CompareA接口的初始化");
}
};
public static final int num = Integer.valueOf(1);
}
主动使用6: 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。
public class ActiveUse {
public static void main(String[] args) {
SonTest1 sonTest = new SonTest1();
// Compare接口的初始化
// SonTest1类的初始化...
}
}
class SonTest1 implements Compare{
static {
System.out.println("SonTest1类的初始化...");
}
}
interface Compare{
public static final Thread t = new Thread() {
{
System.out.println("Compare接口的初始化");
}
};
public default void method() {
System.out.println("hello word");
}
}
主动使用7: 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
JVM启动的时候通过引导类加载器加载一个初始类。这个类在调用public static void main(String[])方法之前被链接和初始化。这个方法的执行将依次导致所需的类的加载,链接和初始化。
主动使用8: 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)
被动使用举例
当通过子类引用父类的静态变量,不会导致子类初始化,只有真正声明这个字段的类才会被初始化。
/**
* @ClassName PassiveUse
* @author: shouanzh
* @Description 类的被动使用,即不会进行类的初始化操作,即不会调用类的<clinit>() 方法
* @date 2022/2/22 20:30
*/
public class PassiveUse {
public static void main(String[] args) {
// 当通过子类引用父类的静态变量,不会导致子类初始化,只有真正声明这个字段的类才会被初始化。
System.out.println(Child.num);
// Parent类的初始化
// 1
}
}
class Child extends Parent {
static {
System.out.println("Child类的初始化");
}
}
class Parent {
static {
System.out.println("Parent类的初始化");
}
public static int num = 1;
}
没有初始化的类,不意为着没有加载。
通过数组定义类引用,不会触发此类的初始化
Parent[] parents= new Parent[10];
System.out.println(parents.getClass());
// new的话才会初始化
parents[0] = new Parent();
引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。
public class PassiveUse {
public static void main(String[] args) {
// 不会初始化
System.out.println(Serival.num);
// 但引用其他类的话还是会初始化
System.out.println(Serival.num2);
}
}
interface Serival {
public static final Thread t = new Thread() {
{
System.out.println("Serival初始化");
}
};
public static final int num = 10; // 链接过程的准备环节就被赋值了
public static final int num2 = new Random().nextInt(10);
}
调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.test.java.Person");
5.过程四:类的Using(使用)
任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,便“万事俱备只欠东风”,就等着开发者使用了。
开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。
6.过程五:类的Unloading(卸载)
6.1 类、类的加载器、类的实例之间的引用关系
在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,代表某个类的Class实例与其类的加载器之间为双向关联关系。
一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的java类都有一个静态属性class,它引用代表这个类的Class对象。
6.2 类的生命周期
当Sample类被加载、链接和初始化后,它的生命周期就开始了。当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
6.3 具体例子
loader1变量和obj变量间接应用代表Sample类的Class对象,而objClass变量则直接引用它。
如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。
当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不再重新加载;如果不存在Sample类会被重新加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例(可以通过哈希码查看是否是同一个实例)
6.4 回顾方法区的垃圾回收
6.5 类的卸载
昨天是历史,明天是谜团,只有今天是天赐的礼物。