-
星空体育入口:JVM 的类加载机制是在运行时第一次使用时动态加载类,而不是一次性加载所有类。
- 时间:2024-06-26 来源:zoc7RcITctunhMtq7EzA 人气:
由于一次性加载会占用大量内存。
類的生命週期
包括以下七個階段:
「加載(Loading)」 「驗證(Verification)」 「準備(Preparation)」 「解析(Resolution)」 「初始化(Initialization)」 使用(Using) 卸載(Unloading)
類加載過程 --- 創建一個對象的過程包含加載、驗證、準備、解析和初始化這五個階段。
1. 在完成加载
的过程中,会执行以下三个步骤:
首先,可以通过以下方式获取二进制字节流:
从ZIP包中读取,作为JAR、EAR、WAR格式的基础。从网络中下载信息的最典型应用是Applet。在运行时生成,比如使用动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成代理类的二进制字节流。 是由其他文件生成的,比如说通过 JSP 文件生成相应的 Class 类。使用类的完整限定名来检索定义该类的字节流。将基于该字节流表示的静态存储结构,转化为方法区中的运行时存储结构。在内存中创建一个代表该类的 Class 对象,用作方法区中该类各种数据的访问入口。
2.验证格式验证:验证是否符合class文件规范 语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同) 操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否可以通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
3.准备
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,这里使用的是方法区的存储空间。
public static int value = 123;
如果一个类变量被定义为常量,它将被初始化为表达式所给定的值,而非默认的0。若类变量为常量,则会被初始化为表达式所指定的值,而非零。例如,常量 value 的初始值为123,而非0。
公有 静态 最终 整数 值 = 123;
实例变量不会在这个阶段分配内存,而是在对象实例化时一同分配在堆中。解析器(resolver)将常量池中的符号引用转换为直接引用(即获取类、字段、方法在内存中的指针或偏移量,以便直接调用该方法),这一过程可以在初始化后进行,以支持 Java 的动态绑定。在JVM中,
以上的三个阶段合称为链接阶段,链接阶段的主要任务是将加载到JVM中的二进制字节流类的数据信息合并到JVM的运行时状态中。
5.开始初始化
开始初始化的阶段是虚拟机执行类构造器 () 方法的过程,也是真正开始执行类中定义的 Java 程序代码的时候。在准备阶段,类变量已经被赋予系统要求的初始值,而在初始化阶段,主要是根据程序员制定的计划来进行类变量和其他资源的初始化。
() 是通过编译器自动将类中的所有类变量的赋值操作和静态代码块中的语句合并生成的,其收集顺序取决于这些语句在源文件中出现的顺序。需要特别注意的是,静态代码块只能访问它之前定义的类变量,而它之后定义的类变量只能赋值,不能访问。
虚拟机确保一个类的 () 方法在多线程环境中正确加锁和同步。若多个线程同时初始化一个类,仅有一个线程执行该类的\ () 方法,其他线程将被阻塞直至活跃线程执行完毕。当一个类的 () 方法中存在耗时操作时,可能会导致多个线程被阻塞,在实际应用中这种阻塞看起来是很难发现的。星空体育入口
以上所述的过程可以简单概括为以下两步:
给类变量赋值,假设是首次使用该类,就需要先经历类加载的过程,然后才能创建对象。「1、分配对象在堆区所需的内存」时,分配的内存包含该类和其父类的所有实例变量,但不包括任何静态变量。 \n「2、为所有实例变量赋予默认值」的步骤是将方法区内有关实例变量的定义拷贝至堆区,然后赋以默认值。 \n「3、执行实例化代码」的过程中,初始化顺序是先初始化父类,再初始化子类;在初始化时,首先执行实例代码块,然后再执行构造方法。在第一个执行类中,包括静态成员变量的初始化和静态代码块的执行;而在第二个执行类中,则包括非静态成员变量的初始化和非静态代码块的执行,最后执行构造函数。在继承的情况下,程序会先执行父类的静态代码,然后执行子类的静态代码;随后执行父类的非静态代码和构造函数;最后执行子类的非静态代码和构造函数。如果有类似于Child c = new Child()这样的引用形式,在栈区定义Child类型的引用变量c,然后将堆区对象的地址赋值给它。需要注意的是,“每个子类对象持有父类对象的引用”,可以在内部通过super关键字来调用父类对象,但在外部不可访问。在继承存在的情况下,初始化顺序为:父类(静态变量、静态代码块) -> 子类(静态变量、静态代码块) -> 父类(实例变量、普通代码块) -> 父类(构造函数) -> 子类(实例变量、普通代码块) -> 子类(构造函数)。类的初始化情况由虚拟机规范并没有强制规定何时进行加载。规范明确规定了只有以下五种情况下类必须被初始化(加载、验证、准备会发生):
,即在遇到 new、getstatic、putstatic、invokestatic 这四种字节码指令时,如果类尚未被初始化,则必须立即执行初始化操作。触发生成这 4 条指令的常见场景包括:在使用 new 关键字实例化对象时;读取或设置类的静态字段(final 修饰的静态字段以及已在编译期将结果放入常量池的字段除外)时;以及调用类的静态方法时。生成这 4 条指令最常见的情况是:当使用 new 关键字实例化对象时;当读取或设置类的静态字段(除了被 final 修饰并在编译时结果被放入常量池的静态字段)时;以及调用类的静态方法时。当使用 java.lang.reflect 包中的方法进行类反射调用时,若该类尚未初始化,则需要先触发其初始化。在初始化一个类时,如果发现其父类尚未被初始化,则需要先初始化其父类。在启动虚拟机时,用户需指定一个主类来执行(即包含 main() 方法的类),虚拟机会首先对该主类进行初始化。对一个类进行主动引用被称为引用。除此之外,所有涉及类的引用方式都不会引发初始化,这种情况被称为被动引用星空体育手机版。被动引用的常见例子包括:
当子类引用父类的静态字段时,并不会触发子类的初始化过程。System.out.println(SubClass.value); // value字段在SuperClass中定义。通过数组定义来引用类,不会触发该类的初始化。这一步骤将对数组类进行初始化,数组类是虚拟机自动生成的、直接继承自Object的子类,内部包含了数组的属性和方法。在编译时,常量会存储在调用类的常量池中,而不是直接引用定义常量的类,因此不会导致定义常量的类进行初始化。同时,可以通过{"x"}{"n"}{"x"}SuperClass[] scc={"p"}={"p"}new{"p"}SuperClass[10];声明和实例化一个大小为10的数组。在Java中,若两个类是通过相同的类加载器加载的,若且仅若这两个类的全限定名相同,那么它们是相等的。这是因为每个类加载器都有自己独立的类命名空间。最终的相等条件包括了类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法返回为 true,还包括使用 instanceof 关键字对对象所属关系的判断结果为 true。Java虚拟机使用两种不同的类加载器:启动类加载器(Bootstrap ClassLoader)是虚拟机的一部分,由C++实现; 其他类的加载器由Java实现,独立于虚拟机,继承自java.lang.ClassLoader抽象类。
以上情况可以被划分为以下三种类加载器:BootstrapClassLoader、ExtensionClassLoader、AppClassLoader启动类加载器(BootstrapClassLoader)是嵌入JVM内核中的加载器,由C++语言编写,主要负责加载JAVA_HOME/lib目录下的类库,或者被-Xbootclasspath参数指定的路径中的类,虚拟机只识别文件名(例如rt.jar,即使放在lib目录中但文件名不符合规范的类库也不会被加载)。Java程序无法直接引用启动类加载器,当用户编写自定义类加载器时,如果需要将加载请求委托给启动类加载器,可以直接使用null代替。扩展类加载器(ExtensionClassLoader)是用Java编写的,由ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现。它的工作是将所有位于 /lib/ext 目录或者被 java.ext.dir 系统变量指定路径中的类库加载到内存中,开发者可以直接使用扩展类加载器加载这些类库。它的父类加载器是引导类加载器。
应用程序类加载器是由 sun.misc.Launcher$AppClassLoader 实现的。通常负责在应用程序的classpath目录中加载所有的jar和class文件。当开发者使用这个类加载器时,如果应用程序没有定义自己的类加载器,通常情况下这就是程序中的默认类加载器。它的父亲已经去世。载入器是ExteClassLoader。双亲委派模型
应用程序的实现涉及三种类加载器协同工作,以实现类加载功能,同时也支持自定义类加载器的添加。图示了类加载器之间的分层结构,被称之为双亲委派模型。在这里,通常是通过委托而不是继承关系来实现父子关系。在工作流程
中,当一个类加载器收到一个加载类的请求时,它不会尝试自己加载该类,而是将请求传递给其父类加载器来处理。每个级别的类加载器都是一样的。应该将所有的类加载请求传递给顶层的启动类加载器,只有当父类加载器无法完成此加载请求(在其搜索范围内找不到该类)时,子类加载器才会尝试进行加载。
Java 中的好处在于其类与类加载器形成一种带有优先级的层次关系,从而实现基础类的统一。在 rt.jar 中存在 java.lang.Object 这样的类,如果你创建了另一个 java.lang.Object 并将其放进 ClassPath 中,程序将能够成功编译。因为存在双亲委派模型,所以在 rt.jar 中的 Object 拥有比位于 ClassPath 中的 Object 更高的优先级星空体育网站。这是因为 rt.jar 中的 Object 是由启动类加载器加载的,而 ClassPath 中的 Object 是由应用程序类加载器加载的。rt.在jar包中的Object拥有更高的优先级,因此程序中所有的Object都是指向这个Object。以下是 java.lang.ClassLoader 抽象类的一个代码片段,其中 loadClass() 方法的执行过程如下:首先检查类是否已经被加载,如果没有,则委托父类加载器进行加载。当父类加载器加载失败,会抛出ClassNotFoundException异常,此时尝试自己去加载类星空体育官方。public abstract class ClassLoader { // 这个 是 用 來 做 代理 的 父 类 装 载 器 private final ClassLoader parent; public Class<?覆盖loadClass(String name)方法时,如果加载失败,则抛出ClassNotFoundException异常。在方法中调用loadClass(name, false)方法,并返回其结果。这个方法被标记为受保护的。加载类(String 名称, boolean 解析) 抛出 ClassNotFoundException { synchronized (getClassLoadingLock(名称)) { // 首先,请检查该类是否已经被加载了Class> 获取LoadedClass(name);,如果(c == null) { 尝试{ 如果(parent!如果(c != null){c = parent.loadClass(name,抛出了ClassNotFoundException异常,表示找不到类,这可能是来自非空父类加载器的异常情况。若c仍然为空,则应继续查找。接着调用findClass方法来寻找这个类。实例c等于findClass(name)。若(resolve)为真,则解析这个类(resolveClass(c)),最后返回c。这是protected的类<?找到类(String name)方法会抛出ClassNotFoundException异常。在以下示例中,FileSystemClassLoader是个自定义类加载器,继承自java.lang.ClassLoader,主要用于加载文件系统上的类。首先它会根据类的完整名称在文件系统中寻找类的字节码文件(.class文件),然后读取文件内容,最后通过 defineClass() 方法将这些字节码转换为java.lang.Class类的实例。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。public class FileSystemClassLoader extends ClassLoader {\n private String rootDir;\n \n public FileSystemClassLoader(String rootDir) {\n this.rootDir = rootDir;\n }\n \n protected Class>找类(String名字)抛出ClassNotFoundException异常{{{{{{{{{{byte[]类数据 = getClassData(name);{{{{{{{{{如果(类数据 == null){{{{{{{{{{{{{ ew ClassNotFoundException();{{{{{{{{{{{{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}否则{{{{{{{{{{{{{{返回defineClass(name,
类数据,
0,(p)classData.length);(p)(p)(p)(p)(p)(p)(p)(p)}(p)(p)(p)(p)( )(p)(p)(p)(p)(p)(p)私有(p)byte[] getClassData(String className) { (p)(p)(p)(p)(p)(p)(p)(p)String 路径 = classNameToPath(className); (p)(p)(p)(p)(p)(p)(p)(p)试 { (p)(p)(p)(p)(p)(p)(p)(p)(p)(p)(p)( 输入流 ins = 新 FileInputStream(path); (p)(p)(p)(p)(p)(p)(p)(p)(p)(p)(p)( ByteArrayOutputStream baos =创建一个新的ByteArrayOutputStream实例;设置缓冲区大小为4096;创建一个大小为bufferSize的byte数组;初始化一个变量bytesNumRead;循环读取数据到buffer,直到读取到的字节数不为零。 等待输入并写入缓冲区,直到缓冲区到达自定义大小或输入达到指定长度。读取字节); 返回 baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.',); File.separatorChar) + "/.class"; } }
巨人的背后
程序炉的春招笔记摘要
星空体育官方版
https://github.com/CyC2018/CS-Notes本文转载自微信公众号「多选参数」,可扫描以下二维码关注。请联系多选参数公众号获取转载授权。请联系多选参数公众号进行转载。
星空体育登录入口
星空体育网站
星空体育官方版 星空体育手机版 星空体育登录入口