[JVM]虚拟机类加载机制

目录

概述

加载

连接阶段

验证

准备

解析

初始化

类加载器

双亲委派模型

类加载器种类

什么是双亲委派模型

双亲委派机制的优点?

双亲委派模型的实现

破环双亲委派模型

 

类加载及执行子系统

Tomcat

关于OSGI

OSGI 实战

一些问题

java类的初始化顺序是怎样的?

运行时可以加载类吗?如果可以如何加载?

运行时可以创建一个全新的类,并编译加载到内存中吗?


概述

定义如下: 虚拟机把描述类的数据从Class文件(已经编译成Class文件了)加载到内存并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。类从java文件变成class文件的过程是java的编译过程,是由java编译器javac实现的。

关于class类文件结构如下,参考:https://blog.csdn.net/zhoufenqin/article/details/51045890

类从被加载到到虚拟机内存开始 ->到卸载出内存为止,生命周期如下:

其中验证,准备,解析三个部分统一称为连接。

加载,验证,准备,初始化,和卸载这5个阶段的顺序时确定的。而解析则不一定。在某些情况下可以在初始化之后再开始解析,目的就是为了支持java语言的动态绑定(即运行时绑定或晚期绑定)。

虚拟机规定了5种情况必须立即对类进行初始化:

(1)遇到new,getstatic,putstatic,invokestatic这四条指令时。

new->实例化对象

getstatic,putstatic->读取,设置一个类的静态字段(final修饰,已在编译期把结果放入常量池的静态字段除外)

invokestatic->调用一个类的静态方法

(2)使用java.lang.reflect包的方法对类进行反射调用的时候

(3)初始化一个类时其父类还没有初始化,显初始化父类

(4)虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类)

(5)动态语言,一个java.lang.invoke.MethodHandle实例最后的极细结果有REF_getStatic,REF_putStatic,REF_involeStatic的方法句柄时。

加载

加载是类加载(Class Loading)过程的一个阶段。在加载阶段,虚拟机需要完成以下3件事(非常重要):

1)通过一个类的全限定名来获取定义此类的二进制字节流(并未指定从哪儿获取,怎样获取,也可以从jar读取,也可以从网络中获取,也可运行时计算生成,如动态代理,甚至类加载器也可以自己写)

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

3)在内存中生产一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口(p.s. 这里和反射是一样的)

数组类的加载本身不通过类加载器创建,由java虚拟机直接创建。

(1)若数组类的组件类型时引用类型,就遍历按照类的加载方法去加载

(2)若不是引用类型,如int[]数组,虚拟机会把数组与引导类加载器关联

(3)数组类的可见性与它的组件类型的可见性保持一致

连接阶段

验证

目的是确保class文件的字节流中的信息符合当前虚拟机的要求,并且不会危害虚拟机。包括

 (1)文件格式验证。如是否以魔数0xCAFEBABE开头等

 (2)元数据验证。对字节码描述的信息进行语义分析,保证其描述的信息符合规范。是否有父类,是否继承了不允许被继承的类

 (3)字节码验证。确保程序语义是合法的,符合逻辑的

 (4)符号引用验证。符号引用中通过字符串描述的全限定名是否能找到对应的类

准备

为类变量(static变量)分配内存并设置类变量的初始值。这些类变量所使用的内存将在方法区中进行分配。注意仅包括类变量不包括实例变量(非static变量)

两个关键字:类变量+初始值

初始值是数据类型的零值。例如

public static int value = 123;

变量value在准备阶段过后的初始值为0,而不是123。而把value赋值为123的putstatic指令是在程序编译之后,存放于类构造器<clinit>()方法之中。所以把value赋值为123的动作在初始化阶段才会执行。

但是如果类字段为常量,即final修饰的成员变量,则在类的表的字段属性表中存在对应的ConstantValue属性,并且在准备阶段value就会被初始化为ConstantValue属性所指定的值。如:

public static final int value = 123;

编译时javac将会为 value生成ConstantValue属性,并在准备阶段就根据ConstantValue的设置将value赋值为123.

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

  •     符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,与虚拟机实现的内存布局无关
  •     直接引用(Direct References):直接指向目标的指针,相对偏移量或一个能间接定位到目标的句柄

详细解析步骤参见深入理解java虚拟机。

 

初始化

类加载过程的最后一步。在前面的过程中除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作全由虚拟机主导和控制。

初始化阶段,才真正开始指向类中定义的java程序代码(字节码。初始化阶段是执行类构造器<clinit>()方法的过程

  • <clinit>()方法是有编译器自动收集类中的所有类变量(即static变量)的赋值动作和静态语句块(注意静态语句块仅仅只包括static{}块,不包含任何static方法)中的语句合并产生。编译器收集的顺序按照代码源文件顺序,静态语句块只能访问到定义在静态语句块之前的变量
  • <clinit>()方法与类的构造函数,或者说是实例构造器<init>()方法不同,它不需要显示的调用父类构造器。虚拟机会保证在子类的<clinit>()执行之前,父类的<clinit>()方法已经执行完毕。可以想到,虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object
  • 由于父类的<clinit>()方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。例如
public class TestClinit{

    static class Parent {
        public static int A = 1;
        static {
            A = 2;
        }
    }

    static class Sub extends Parent {
        public static int B = A;

    }
    public static void main(String[] args){
        System.out.println(Sub.B);
    }
}

 打印的结果字段B的值将会是2而不是1

  • <clinit>()方法对于类或接口不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,则编译器可以不生成<clinit>()方法
  • 接口仍然有变量初始化的赋值操作,接口与类一样都会生成<clinit>()方法。但接口的<clinit>()方法不需要先执行父接口的<clinit>()方法方法。只有当父接口中的变量使用时,父接口才会初始化。接口的实现类在初始化时一样不会执行接口的<clinit>()方法。
  • 虚拟机会保证<clinit>()方法在多线程环境下的正确性

类加载器

定义:通过一个类的全限定名来获取描述此类的二进制字节流, 并且这个动作是放到java虚拟机外部去实现的,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为-类加载器。

(1)对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟中的唯一性,每一个类加载器,都拥有一个独立的名称空间。

(2)带来的影响:比较两个类是否"相等", 只有在这两个类是由同一个类加载器加载的前提下才有意义。即使两个类来源于同一个Class文件,被同一个虚拟机加载,但是他们的类加载不同,那这两个类就必定不相等。

(3)这里所指的"相等",包括代表类的Class对象的equals()方法,isAssignableForm()方法,isInstance()方法返回的结果相同

延伸问题:运行时可以加载类吗?如果可以如何加载?

 

双亲委派模型

也有人叫它双亲委派机制,其实更准确的名字应该是双亲委派模型。这是一个虚拟机的类加载器的概念。

所谓类加载器,它的作用就是实现累的加载。值得注意的是,在 java的体系里面,比较两个类是否相等,只有在两个类是由同一个类加载起加载的前提下才有意义。否则,即便两个类来源于同一个Class文件,被同一个虚拟机加载,但是加载他们的类加载器不一样,那这两个类就不相等。简而言之,这是为了能够确立类的唯一性。

因此,我们必须确保在java里,同一个类始终是由同一个类加载器加载的,这是java体系运行的一个关键。

来自<<深入理解java虚拟机>>一书的原话:对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间.

即类的唯一性确定 = 类加载器 + 类本身

类加载器种类

有两种分法,从虚拟机的角度来说,只存在两种不同的类加载器:

  1. 启动类加载器,即Bootstrap ClassLoader. 在HotSpot虚拟机中由C++语言实现,是虚拟机自身一部分。
  2. 其他的类加载器。他们都由java语言实现,独立于虚拟机,并且都是java.lang.ClassLoader的子类。

但是对大多数人更有意义的应该是从开发者角度来看。从开发者角度来看类加载器可以细分为三类:

  1. 启动(Bootstrap)类加载器:<JAVA_HOME>\lib. 负责将 Java_Home/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

  2. 标准扩展(Extension)类加器:<JAVA_HOME>\lib\ext是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

  3. 应用程序(Application)类加载器:用户类路径上所指定的类库.是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般称为系统(System)加载器

什么是双亲委派模型

其实这个中文的名字个人觉得起的非常不好,起名字讲求一个顾名思义,双亲委派模型来源于英文:Parents Delegation Model。

第二个单词Delegation即为委派的意思,中文将Parents翻译为了双亲,实在感觉很别扭。

除了上面提到的类加载器,程序员还可以定义自己的类加载器。有这么多的类加载器,那么当我要加载一个类的时候,到底应该用哪个类加载器呢?双亲委派模型就是这个问题的答案。它的处理如下:

当某一个类加载器在接到加载类的请求时,首先不是自己去尝试加载而是将加载任务交给父类加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回。只有当父加载器无法完成加载任务时,才自己去加载。

注意这些类加载器的关系并非通过继承实现,而是通过组合(Composition)来实现的。P.S. 组合优先于继承是efficitive java里的建议,也是面向对象的理论支持的。除此处之外,java中的HashSet的功能也是通过和HashMap组合关系实现的,HashSet借用了HashMap中所有关于key的方法。

回到正题:上面的机制就叫做双亲委派机制

双亲委派机制的优点?

使用双亲委派模型的好处在于它带来了一种系统安全性,即: java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。

但是,用户编写一个java.lang.Object的同名类但不放在ClassPath中,而是放在自己的开发路径下,那么,由于类加载器的不一样,他们就不是同一个类,因为前面第一节讲的类的唯一性告诉了我们,他们对于虚拟机来说是不同的两个类。同时同理,对于用户自己编写的Object类,无论哪一个类加载器要加载这个类,都会交给应用程序类加载器去加载,这也确定了这个类的唯一性。

因此,双亲委派机制的优点就是它能确定类的唯一性,并由此带来了一种系统的安全性。个人认为这是java生态系统的基石之一。

双亲委派模型的实现

实现方法就在java.lang.ClassLoader的loadClass()方法中,方法如下:

  protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
 
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
 
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

逻辑和注释都非常清楚,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

注意这里直接调用了parent的loadClass方法,parent的定义如下:

 // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;

破环双亲委派模型

首先注意,双亲委派模型只是推荐的加载器模式,并不是准则。在java中存在着一部分破环双亲委派模型的地方,最典型的一个例子是线程上下文类加载器(Thread Context ClassLoader)。

 

类加载及执行子系统

Tomcat

主流的web服务器如tomcat,jettty等都实现了自己定义的类加载器皿。一个功能健全的web服务器,要解决如下几个问题:

  1. 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。
  2. 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以相互共享。
  3. 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。
  4. 支持JSP应用的Web服务器,大多数都需要支持HotSwap功能。

由于存在上述问题,在部署web应用时,单独的一个ClassPath,即类路径,就无法满足要求了。因此,各种web服务器都提供了多个ClassPath供用户存放第三方的类库。以Tomcat为例:

在Tomcat目录结构中,有3组目录(“/common/*”,“/server/*”,“/shared/*”)可以存放Java类库,另外还可以加上Web应用程序自身的目录“/WEB-INF/*”:

  • 放置在/common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
  • 放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。
  • 放置在/shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
  • 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其它Web应用程序都不可见。

为了支持这套目录结构,并对目录力的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,因此我们会发现它与双亲委派模型图十分类似,如下:

问题:Tomcat类加载器是否破坏了双亲委派机制?

显然违背了。 

双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载

然而tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。

关于OSGI

OSGi全称Open Service Gateway Initiative,直译过来就是:开放的服务入口(网关)的初始化。

我们来回到我们以前的某些开发场景中去,假设我们使用SSH(struts+spring+hibernate)框架来开发我们的Web项目,我们做产品设计和开发的时候都是分模块的,我们分模块的目的就是实现模块之间的“解耦”,更进一步的目的是方便对一个项目的控制和管理。
我们对一个项目进行模块化分解之后,我们就可以把不同模块交给不同的开发人员来完成开发,然后项目经理把大家完成的模块集中在一起,然后拼装成一个最终的产品。一般我们开发都是这样的基本情况。

那么我们开发的时候预计的是系统的功能,根据系统的功能来进行模块的划分,也就是说,这个产品的功能或客户的需求是划分的重要依据。

但是我们在开发过程中,我们模块之间还要彼此保持联系,比如A模块要从B模块拿到一些数据,而B模块可能要调用C模块中的一些方法(除了公共底层的工具类之外)。所以这些模块只是一种逻辑意义上的划分。

最重要的一点是,我们把最终的项目要去部署到tomcat或者jBoss的服务器中去部署。那么我们启动服务器的时候,能不能关闭项目的某个模块或功能呢?很明显是做不到的,一旦服务器启动,所有模块就要一起启动,都要占用服务器资源,所以关闭不了模块,假设能强制拿掉,就会影响其它的功能。

以上就是我们传统模块式开发的一些局限性。

我们做软件开发一直在追求一个境界,就是模块之间的真正“解耦”、“分离”,这样我们在软件的管理和开发上面就会更加的灵活,甚至包括给客户部署项目的时候都可以做到更加的灵活可控。但是我们以前使用SSH框架等架构模式进行产品开发的时候我们是达不到这种要求的

现在我们的OSGI技术就可以满足我们之前所说的境界:在不同的模块中做到彻底的分离,而不是逻辑意义上的分离,是物理上的分离,也就是说在运行部署之后都可以在不停止服务器

由此,OSGI技术将来会变得非常的重要,因为它在实现模块化解耦的路上,走得比现在大家经常所用的SSH框架走的更远。这个技术在未来大规模、高访问、高并发的Java模块化开发领域,或者是项目规范化管理中,会大大超过SSH等框架的地位。

现在主流的一些应用服务器,Oracle的weblogic服务器,IBM的WebSphere,JBoss,还有Sun公司的glassfish服务器,都对OSGI提供了强大的支持,都是在OSGI的技术基础上实现的。有那么多的大型厂商支持OSGI这门技术,我们既可以看到OSGI技术的重要性。所以将来OSGI是将来非常重要的技术。

但是OSGI仍然脱离不了框架的支持,因为OSGI本身也使用了很多spring等框架的基本控件(因为要实现AOP依赖注入等功能),但是哪个项目又不去依赖第三方jar呢?

的时候直接把某些模块拿下来,其他模块的功能也不受影响。

在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索:

  1. 将以java.*开头的类委派给父类加载器加载。
  2. 否则,将委派列表名单内的类委派给父类加载器加载。
  3. 否则,将Import列表中的类委派给 Export这个类的Bundle的类加载器加载。
  4. 否则,查找当前Bundle的 Class Path,使用自己的类加载器加载。
  5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给 Fragment Bundle的类加载器加载。
  6. 否则,查找Dynamic Import列表的 Bundle,委派给对应Bundle的类加载器加载。
  7. 否则,类查找失败。

上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类加载器中进行的。

只要有足够意义和理由,突破已有的原则就可认为是一种创新。正如OSGi中的类加载器并不符合传统的双亲委派的类加载器,并且业界对其为了实现热部署而带来的额外的高复杂度还存在不少争议,但在Java 程序员中基本有一个共识:OSGi中对类加载器的使用是很值得学习的,弄懂了OSGi的实现,就可以算是掌握了类加载器的精髓。

OSGI 实战

为了实现代码热替换,模块化和动态化,就像鼠标一样即插即用,双亲委派这种树状的加载器就难以胜任,于是出现了 OSGI 加载模型,OSGI 里每个程序模块(Bundle,就是普通的 jar 包, 只是加入了特殊的头信息,是最小的部署模块)都会有自己的类加载器,当需要更换程序时,就连同 Bundle 和类加载器一起替换,是一种网状的加载模型,Bundle 间互相委托加载,并不是层次化的。

Java 类加载机制的隔离是通过不同类加载器加载指定目录来实现的,类加载的共享机制是通过双亲委派模型来实现,而 OSGI 实现隔离靠的是每个 Bundle 都自带一个独立的类加载器 ClassLoader

OSGI 的应用是一个模块化的系统,避免了部署时 jar 或 classpath 错综复杂依赖管理,发布应用和更新应用都很强大,可以热替换特定的 Bundle 模块,提高部署可靠性。

OSGI总结

  1. 定义一种模块化开发的架构
  2. 经典应用:eclipse
  3. 特点:热插拔,模块化功能
  4. 缺点:复杂度大

一些问题

java类的初始化顺序是怎样的?

答:事实上这个问题的答案已经蕴藏在前面的内容当中,这里无非是提炼一下。简要的说是:父类静态代码块,父类静态成员变量(这两者顺序是按照代码先后)->子类静态代码块,子类静态成员变量->父类普通代码块,普通成员变量->父类构造函数->子类普通代码块,普通成员变量->子类构造函数

运行时可以加载类吗?如果可以如何加载?

目前了解到的知识,在java目前体现当中,我们可以实现运行时加载,即多态,那么我们可以实现动态的直接创建出一个类来,并把它编译加载到内存当中吗?

答案是可以的,使用反射我们就可以动态的根据类名等信息,把类加载到内存当中。可参考博客 [java] 反射和多态实现原理详解以及对比

运行时可以创建一个全新的类,并编译加载到内存中吗?

可以,大致的思路是,首先把这个class类建立起来,无论是通过文件还是字符串的方式。然后在使用java编译器编译此class。最后在调用类加载器加载此类即可。

可参考博客:java动态创建class-两种方式(写入文件和非文件)

 

 

 

 

 

  • 3
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值