Java类的加载、链接和初始化
一、Java的类加载机制回顾与总结:
我们知道一个Java类要想运行,必须由jvm将其装载到内存中才能运行,装载的目的就是把Java
字节代码转换成JVM
中的java.lang.Class
类的对象。这样Java就可以对该对象进行一系列操作,装载过程有两个比较重要的特征:层次组织结构和代理模式。层次组织结构指的是每个类加载器都有一个父类加载器,通过getParent()方法可以获取到。类加载器通过这种父亲-后代的方式组织在一起,形成树状层次结构。代理模式则指的是一个类加载器既可以自己完成Java类的定义工作,也可以代理给其它的类加载器来完成。由于代理模式的存在,启动一个类的加载过程的类加载器和最终定义这个类的类加载器可能并不是一个。ClassLoader的加载类过程主要使用loadClass方法,该方法中封装了中加载机制:双亲委派模式。
一般来说,父类优先的策略就足够好了。在某些情况下,可能需要采取相反的策略,即先尝试自己加载,找不到的时候再代理给父类加载器。这种做法在Java的Web容器中比较常见,也是
Servlet
规范推荐的做法。比如,Apache Tomcat
为每个Web
应用都提供一个独立的类加载器,使用的就是自己优先加载的策略。IBM WebSphere Application Server
则允许Web应用选择类加载器使用的策略。
类加载器的一个重要用途是在JVM
中为相同名称的Java类创建隔离空间。在JVM
中,判断两个类是否相同,不仅是根据该类的二进制名称,还需要根据两个类的定义类加载器。只有两者完全一样,才认为两个类的是相同的。因此,即便是同样的Java
字节代码,被两个不同的类加载器定义之后,所得到的Java
类也是不同的。如果试图在两个类的对象之间进行赋值操作,会抛出java.lang.ClassCastException
。这个特性为同样名称的Java
类在JVM
中共存创造了条件。在实际的应用中,可能会要求同一名称的Java
类的不同版本在JVM
中可以同时存在。通过类加载器就可以满足这种需求。这种技术在OSGi
中得到了广泛的应用
Java类的加载过程:
1.通过类的全名产生对应类的二进制数据流。(如果没找到对应类文件,只有在类实际使用时才抛出错误。)
2.分析并将这些二进制数据流转换为方法区(JVM 的架构:方法区、堆,栈,本地方法栈,pc 寄存器)特定的数据结构(这些数据结构是实现有关的,不同 JVM 有不同实现)。这里处理了部分检验,比如类文件的魔数的验证,检查文件是否过长或者过短,确定是否有父类(除了 Obecjt 类)。
3.创建对应类的 java.lang.Class 实例(注意,有了对应的 Class 实例,并不意味着这个类已经完成了加载链链接!)。
Java类的链接
Java类的链接指的是将Java类的二进制代码合并到JVM的运行状态之中的过程。在链接之前,这个类必须被成功加载。
链接的过程比加载过程要复杂很多,这是实现java的动态性的重要一步!分为三部分:verification
(检测),preparation
(准备) 和 resolution
(解析)
1.verification
(检测):
验证是用来确保Java类的二进制表示在结构上是完全正确的。如果验证过程出现错误的话,会抛出java.lang.VerifyError
错误。
linking的resolve会把类中成员方法、成员变量、类和接口的符号引用替换为直接引用,而在这之前,需要检测被引用的类型正确性和接入属性是否正确(就是public ,private的的问题)诸如,检查final class 没有被继承,检查静态变量的正确性等等。
验证是用来确保Java类的二进制表示在结构上是完全正确的。如果验证过程出现错误的话,会抛出java.lang.VerifyError
错误。
2.preparation
(准备):
准备过程则是创建Java
类中的静态域,并将这些域的值设为默认值。准备过程并不会执行代码。在一个Java
类中会包含对其它类或接口的形式引用,包括它的父类、所实现的接口、方法的形式参数和返回值的Java
类等。
对类的成员变量分配空间。虽然有初始值,但这个时候不会对他们进行初始化(因为这里不会执行任何 Java 代码)。具体如下:
所有原始类型的值都为 0。如 float: 0f, int: 0, boolean: 0(注意 boolean 底层实现大多使用 int),引用类型则为 null。值得注意的是,JVM 可能会在这个时期给一些有助于程序运行效率提高的数据结构分配空间。
3.resolution
(解析):
解析的过程就是确保这些被引用的类能被正确的找到。解析的过程可能会导致其它的Java类被加载。
为类、接口、方法、成员变量的符号引用定位直接引用(如果符号引用先到常量池中寻找符号,再找先应的类型,无疑会耗费更多时间),完成内存结构的布局。
这一步是可选的。可以在符号引用第一次被使用时完成,即所谓的延迟解析(late resolution)。但对用户而言,这一步永远是延迟解析的,即使运行时会执行 early resolution,但程序不会显示的在第一次判断出错误时抛出错误,而会在对应的类第一次主动使用的时候抛出错误!
另外,这一步与之后的类初始化是不冲突的,并非一定要所有的解析结束以后才执行类的初始化。不同的 JVM 实现不同。
看下面一段代码:
public class LinkTest { public static void main(String[] args) { ToBeLinked toBeLinked = null; System.out.println("Test link."); } }
类 LinkTest
引用了类ToBeLinked
,但是并没有真正使用它,只是声明了一个变量,并没有创建该类的实例或是访问其中的静态域。如果把编译好的ToBeLinked
的Java
字节代码删除之后,再运行LinkTest
,程序不会抛出错误。这是因为ToBeLinked
类没有被真正用到。链接策略使得ToBeLinked
类不会被加载,因此也不会发现ToBeLinked
的Java字节代码实际上是不存在的。如果把代码改成ToBeLinked toBeLinked = new ToBeLinked();
之后,再按照相同的方法运行,就会抛出异常了。因为这个时候ToBeLinked
这个类被真正使用到了,会需要加载这个类。
三、Java类的初始化
开发 Java 时,接触最多的是对象的初始化。实际上类也是有初始化的。相比对象初始化,类的初始化机制要简单不少。
类的初始化也是延迟的,直到类第一次被主动使用(active use),JVM 才会初始化类。
当一个Java类第一次被真正使用到的时候,JVM会进行该类的初始化操作。初始化过程的主要操作是执行静态代码块和初始化静态域。在一个类被初始化之前,它的直接父类也需要被初始化。但是,一个接口的初始化,不会引起其父接口的初始化。在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代码块和初始化静态域。
public class StaticTest { public static int X = 10; public static void main(String[] args) { System.out.println(Y); //输出60 } static { X = 30; } public static int Y = X * 2; }
在上面的代码中,在初始化的时候,静态域的初始化和静态代码块的执行会从上到下依次执行。因此变量X的值首先初始化成10,后来又被赋值成30;而变量Y的值则被初始化成60。
类的初始化分两步:
1.如果基类没有被初始化,初始化基类。
2.有类构造函数,则执行类构造函数。
类构造函数是由 Java 编译器完成的。它把类成员变量的初始化和 static 区间的代码提取出,放到一个<clinit>
方法中。这个方法不能被一般的方法访问(注意,static final 成员变量不会在此执行初始化,它一般被编译器生成 constant 值)。同时,<clinit>
中是不会显示的调用基类的<clinit>
的,因为 1 中已经执行了基类的初始化。该初始化过程是由 Jvm 保证线程安全的。。
Java类和接口的初始化只有在特定的时机才会发生,这些时机包括:
创建一个Java类的实例。如 MyClass obj = new MyClass() 调用一个Java类中的静态方法。如 MyClass.sayHello() 给Java类或接口中声明的静态域赋值。如 MyClass.value = 10 访问Java类或接口中声明的静态域,并且该域不是常值变量。如 int value = MyClass.value 在顶层Java类中执行assert语句。
通过Java反射API也可能造成类和接口的初始化。需要注意的是,当访问一个Java
类或接口中的静态域的时候,只有真正声明这个域的类或接口才会被初始化。考虑下面的代码:
class B { static int value = 100; static { System.out.println("Class B is initialized."); //输出 } } class A extends B { static { System.out.println("Class A is initialized."); //不会输出 } } public class InitTest { public static void main(String[] args) { System.out.println(A.value); //输出100 } }
在上述代码中,类InitTest
通过A.value
引用了类B中声明的静态域value
。由于value
是在类B
中声明的,只有类B
会被初始化,而类A则不会被初始化。
Java类的加载、链接和初始化的更多相关文章
- Java类的加载 链接 初始化
原文地址 Java类的加载.链接和初始化.Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是java.lang.Class类的对象.一个Java类从字节代码到能够 ...
- JVM详解之:类的加载链接和初始化
目录 简介 加载 运行时常量池 类加载器 链接 验证 准备 解析 初始化 总结 简介 有了java class文件之后,为了让class文件转换成为JVM可以真正运行的结构,需要经历加载,链接和初始化 ...
- java 类的加载,链接,初始化
本篇的话题,讨论Java类的加载.链接和初始化.Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是java.lang.Class类的对象.一个Java类从字节代码 ...
- java类从加载、连接到初始化过程
类加载器 在了解Java的机制之前,需要先了解类在JVM(Java虚拟机)中是如何加载的,这对后面理解java其它机制将有重要作用. 每个类编译后产生一个Class对象,存储在.class文件中,JV ...
- JAVA类的加载、连接与初始化
JAVA类的加载.连接与初始化 类的声明周期总共分为5个步骤1.加载2.连接3.初始化4.使用5.卸载 当java程序需要某个类的时候,java虚拟机会确保这个类已经被加载.连接和初始化,而连接这个类 ...
- 别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析【JVM篇二】
目录 1.什么是类的加载(类初始化) 2.类的生命周期 3.接口的加载过程 4.解开开篇的面试题 5.理解首次主动使用 6.类加载器 7.关于命名空间 8.JVM类加载机制 9.双亲委派模型 10.C ...
- jvm系列(一):java类的加载机制
java类的加载机制 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装 ...
- 【Java基础】Java类的加载和对象创建流程的详细分析
相信我们在面试Java的时候总会有一些公司要做笔试题目的,而Java类的加载和对象创建流程的知识点也是常见的题目之一.接下来通过实例详细的分析一下. 实例问题 实例代码 Parent类 package ...
- Java类的加载的一个小问题
前言 之前写了一篇文章专门介绍了一下类的加载和对象的创建流程,然后收到了一个博友的疑问,觉得蛮好的,在这里和大家分享下. 博文地址:[Java基础]Java类的加载和对象创建流程的分析 疑问 类在加载 ...
随机推荐
- jquery实现ajax,返回json数据
jquery实现ajax可以调用几种方法 我经常用的是$get(url,data,callback,type)方法 其中url是异步请求的页面(可以是.ashx文件),data是参数,callback ...
- 优化C++程序编译效率的一些方法
优化是一件非常重要的事情.作为一个程序设计者,你肯定希望自己的程序既小又快.DOS时代的许多书中都提到,“某某编译器能够生成非常紧凑的代码”,换言之,编译器会为你把代码尽可能地缩减,如果你能够正确地使 ...
- GITHUB 提交错误 Error: Permission denied (publickey) 解决
1. 在开发机上生成自己的密钥 ssh-keygen -b 1024 -t rsa -b 指密钥对长度 -t 指加密方式 Enter file in which to save the key ( ...
- 表达式语言之java对正则表达式的处理
正则表达式用于字符串匹配,字符串查找,字符串替换等.例如注册email格式的验证等.java中处理正则表达式相关的类主要有java.lang.String,java.util.regex.Patter ...
- LINQ.CS
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Zdso ...
- usb wifi driver run in ubuntu support 360/xiaodu and with 3.13.0-32-generic
(为了实现usb-wifi用在linux系统上,需求解决方案,过程记录和如何实现) 重点解决3.13.0-32-generic内核编译 mt7601 usb wifi 驱动问题. 1:首先下载MT76 ...
- 使用libuv实现生产者和消费者模式
生产者和消费者模式(Consumer + Producer model) 用于把耗时操作(生产线程),分配给一个或者多个额外线程执行(消费线程),从而提高生产线程的响应速度(并发能力) 定义 type ...
- android studio 不能在线更新android SDK Manager问题解决办法
Failed to fetch URL https://dl-ssl.google.com/android/repository/addons_list-2.xml, reason: Connecti ...
- BZOJ3550: [ONTAK2010]Vacation
3550: [ONTAK2010]Vacation Time Limit: 10 Sec Memory Limit: 96 MBSubmit: 91 Solved: 71[Submit][Stat ...
- watch your tone
老板要求邮件注意语气... 木想到混了这么久这种事情还要老板提醒