从类被加载到虚拟机内存中开始,到释放内存总共有7个步骤:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、 卸载(Unloading)。其中验证,准备,解析三个部分统称为连接。
相关:JVM-1-CSDN博客
加载:
通过类的完全限定名,查找此类字节码文件,利用字节码文件创建Class对象
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口
在Java中,通过类的完全限定名加载类字节码文件时,会根据类加载器的规则进行查找。类加载器负责定位和加载类的字节码文件,它可以根据特定的策略在不同的位置进行搜索。
在Java中,有以下几种类加载器:
1.启动类加载器(Bootstrap Class Loader):这是Java虚拟机的一部分,负责加载Java核心类库,如java.lang包中的类。它是用本地代码实现的,不继承自java.lang.ClassLoader
2.扩展类加载器(Extension Class Loader):它负责加载Java扩展库,位于<JAVA HOME>/lib/ext目录下的JAR文件。它是由sun.misc.Launcher$ExtClassLoader实现的,是java.lang.ClassLoader的子类。
3.应用程序类加载器(Application Class Loader):也称为系统类加载器(System ClassLoader),它负责加载应用程序的类,包括用户自定义的类和第三方类库。它是由sun.misc.Launcher$AppClassLoader实现的,同样是java.lang.ClassLoader的子类。
当使用类的完全限定类,名记载类字节码文件时,类加载器,会按照以下顺序进行查找:
1.首先,由启动类加载器尝试加载。启动类加载器只加载Java核心类库,无法加载应用程序的类。
2.如果启动类加载器无法加载该类,则由扩展类加载器尝试加载。它会搜索Java扩展库中的类。
3.如果扩展类加载器也无法加载该类,则由应用程序类加载器尝试加载。它会搜索应用程序的类路径(classpath)下的类。
4.如果应用程序类加载器仍然无法加载该类,则会抛出ClassNotFoundException异常,表示无法找到该类。
但因为存在双亲委派机制,所以在类加载器加载类时(双亲委派规则在应用程序类加载器和扩展类加载器尝试加载类时起作用):
1.当应用程序类加载器或扩展类加载器需要加载一个类时,它们首先会将加载请求委派给父类加载器。
2.父类加载器会按照相同的方式尝试加载该类。如果父加载器能够成功加载该类,那么加载过程结束,类被返回给子加载器。
3.如果父加载器无法加载该类,则子加载器会尝试自己加载。如果子加载器能够成功加载该类,加载过程结束。
4.如果子加载器仍然无法加载该类,它会将加载请求再次委派给父加载器的父加载器(即更上一级的加载器)。这个过程会一直循环进行,直到达到启动类加载器。
5.如果启动类加载器仍然无法加载该类,系统会抛出ClassNotFoundException异常,表示无法找到该类。
双亲委派机制:
即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载
优点:
1.避免类的重复加载
2. 避免Java的核心API被篡改
为什么将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构?
-
内存管理:方法区是Java虚拟机用于存储类信息、常量、静态变量等数据的区域。通过将字节码转化为方法区的运行时数据结构,可以更好地管理内存,有效地组织和存储类相关的数据。
-
动态链接:在类加载过程中,Java虚拟机会进行符号引用到直接引用的解析。这包括将类、方法、字段等符号引用转化为方法区中的直接引用,以便在运行时进行动态链接和方法调用。
-
运行时执行:将字节码转化为方法区的运行时数据结构后,可以更方便地对类进行解析、验证和初始化。这样,在程序运行时,Java虚拟机可以更高效地执行方法区中的代码,提高执行性能。
-
共享数据:方法区是所有线程共享的区域,存储着类的元数据信息和静态变量。通过将字节码转化为方法区的运行时数据结构,可以确保这些共享数据在内存中只有一份拷贝,节省内存空间。
其中,之所以要创建Class对象,是因为java.lang.Class对象是Java反射机制的核心。
这个可以看一下java中面向对象的一项重要原则:封装
封装是将数据和操作数据的方法组合在一起,形成一个称为类(Class)的单元。封装通过将数据隐藏在类的内部,只暴露必要的接口方法来实现。
就比如我们创建一个对象时:
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String newName) {
name = newName;
}
public int getAge() {
return age;
}
public void setAge(int newAge) {
if (newAge >= 0) {
age = newAge;
}
}
}
在这个示例中,name和age字段被声明为私有的,外部代码无法直接访问它们。通过公共的getName和setName方法以及getAge和setAge方法,外部代码可以间接地访问和修改这些字段的值。在setAge方法中,还添加了对年龄值的非负检查,以确保数据的完整性。