JVM笔记11-类加载器和OSGI

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 一.JVM 类加载器: 一个类在使用前,如何通过类调用静态字段,静态方法,或者new一个实例对象,第一步就是需要类加载,然后是连接和初始化,最后才能使用。   类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialzation)、使用(Using)和卸载(Unloading)7 个阶段。

一.JVM 类加载器:

一个类在使用前,如何通过类调用静态字段,静态方法,或者new一个实例对象,第一步就是需要类加载,然后是连接和初始化,最后才能使用。

  类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialzation)、使用(Using)和卸载(Unloading)7 个阶段。其中验证、准备、解析 3 个部分统称为连接(Linking),这 7 个阶段的发生顺序如下图所示:

加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定)。注意,这里笔者写的是按部就班地 “开始”,而不是按部就班地 “进行” 或 “完成”,强调这点是因为这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。

        什么情况下需要开始类加载过程的第一个阶段:加载?Java 虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有 5 种情况必须立即对类进行 “初始化”(而加载、验证、准备自然需要在此之前开始):

  1.   遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的 Java 代码场景是:使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2.   使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3.   当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4.   当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个类。
  5.   当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结构 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化。

        对于这 5 种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。

类加载器就是将 .java 代码文件编译成 .class 字节码文件后,Java虚拟机的类加载器通过读取此类的二进制流,转换成目标类的实例。

除了Java会生成字节码外,运行在JVM上的JRuby,Scala,Groovy同样需要编译成对应的 .class 文件,这里列举了四种不同的字节码,不单是Java才生成字节码文件。

常用的类加载器有4种:

  1.Bootstrap ClassLoader:启动类加载器,加载JAVA_HOME/lib 目录下的类。如下图选中的就是

 

 

  2.ExtClassLoader:扩张类加载器,加载JAVA_HOME/lib/ext 目录下的类。

 

   3.AppClassLoader:应用程序类加载器,加载用户指定的classpath(存放 src 目录 Java 文件编译之后的 class 文件和 xml、properties 等资源配置文件的 src/main/webapp/WEB-INF/classes 目录)下的类

  4.UserClassLoader:用户自定义的类加载器(只要继承 ClassLoader并实现 findClass(String name) 方法),自定义加载路径。

 

类加载时并不需要等到某个类被首次主动使用时再加载它,JVM类加载器会在预料某个类要使用时预先加载。双亲委派模型,如下图:


Java 类加载基于双亲委派模型——当有类加载请求时,从下往上检查类是否被加载,如果没被加载,UserClassLoader 就委托父类 AppClassLoader 加载,AppClassLoader 继续委托其父类 ExtClassLoader 加载,接着分派给 Bootstrap ClasssLoader 加载;

如果无法加载就返回到发起加载请求的类加载一直到由最开始发起加载请求的 UserClassLoader 加载,所有类最终都会去到顶层。Bootstrap ClasssLoader 开始加载,无法加载就返回子加载器处理,一直到最开始的加载器。

这样子,就算用户自定义了 java.lang.Object 类和系统的 java.lang.Object 类重复,也不会被加载,下面我们就来自定义自己的类加载器。


/**
 * Created by cong on 2018/8/2.
*/
public class MyClassLoader extends ClassLoader {
    public MyClassLoader() {
        super();

    }

    public MyClassLoader(ClassLoader parent) {
        super(parent);

    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // do something
        // 自己先不加载,先让父类加载
        return super.findClass(name);
    }

    public static void main(String[] args) throws ClassNotFoundException {

        MyClassLoader myLoader = new MyClassLoader();
        // 打印当前类路径
        System.out.println(System.getProperty("java.class.path"));
        
        // ClassPath路径下并不存在Demo.class类,故抛出异常
        System.out.println(myLoader.loadClass("Demo").getClassLoader().getClass().getName());

    }
}

运行结果如下:

 

学习自定义类加载器后,我们看下源码里双亲委派模型是怎么加载类的。源码如下:


public abstract class ClassLoader {
    private final ClassLoader parent;

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 先检查类是否已经被加载
            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异常被抛出则表明父类加载器加载失败  
                }
                if (c == null) {
                    // 如果父类无法加载,就自己加载      
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    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 类里有同步代码块 synchronized (getClassLoadingLock(name)),而在 JDK1.6 之前是方法 protected synchronized Class<?> loadClass(String name, boolean resolve) 上锁,锁住方法当前对象。

这就导致一个问题,当 A 包依赖 B 包,A 在自己的类加载器的 loadClass 方法中,最终调用到 B 的类加载器的 loadClass 方法。A 先锁住自己的类加载器,然后去申请 B 的类加载器的锁,当 B 也依赖 A 包时,B 加载 A 的包时,过程相反,在多线程下,就容易产生死锁。如果类加载器是单线程运行就会安全,但效率会很低 同步代码块 synchronized (getClassLoadingLock(name)) 锁住的是一个特定对象。


 private final ConcurrentHashMap<String, Object> parallelLockMap;

   protected Object getClassLoadingLock(String className) {
        Object lock = this;
        // parallelLockMap是一个ConcurrentHashMap
        if (parallelLockMap != null) {
            // 锁对象
            Object newLock = new Object();

            // putIfAbsent(K, V)方法查看K(className)和V(newLock)是否相互对应,
            // 是的就返回V(newLock),否则返回null
            // 每个className关联一个锁,并将这个锁返回,缩小了锁定粒度了,只要类名不同,就会匹配不同的锁,
            // 就是并行加载,类似ConcurrentHashMap里面的分段锁,
            // 不锁住整个Map,而是锁住一个Segment,每次只需要对Segment上锁或解锁,以空间换时间

            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                // 创建一个新锁对象
                lock = newLock;
            }
        }
        return lock;

通过并行加载,可以提升加载效率,然后讲下类加载的面试题,在 Java 反射中 Class.forName() 加载类和使用 ClassLoader 加载类是不一样的。例子如下:


/**
 * Created by cong on 2018/8/5.
 */
public class MyCase {
    static {
        System.out.println("执行了静态代码块");
    }

    private static String field = methodCheck();

    public static String methodCheck() {

        System.out.println("执行了静态代方法");
        return "给静态变量赋值";
    }
}

----------------------------------------------------


/**
 * Created by cong on 2018/8/5.
 */
public class DemoTest {
    public static void main(String[] args) {
        try {
            System.out.println("Class.forName开始执行:");
            //hjc是包名
            Class.forName("hjc.MyCase");
            System.out.println("ClassLoader开始执行:");
            ClassLoader.getSystemClassLoader().loadClass("hjc.MyCase");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

运行结果如下:

Class.forName 是加载 MyCase 类并完成初始化,给静态代码块和静态变量赋值,而 ClassLoader 只是将类加载进 JVM 虚拟机,并没有初始化。

接下来我们进入Class.forName的源码探究,源码如下:


    @CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        return forName0(className, true,
                        ClassLoader.getClassLoader(Reflection.getCallerClass()));
    }

Class.forName 底层也是调用了 ClassLoader,只是第二个参数为 true,即加载类并初始化,默认就会初始化类,JDBC 连接就是用 Class.forName 加载驱动。所以注册连接驱动会在静态代码块执行,Sprng 里的 IOC 是通过 ClassLoader 来产生,可以控制 Bean 的延迟加载(首次使用才创建)。

 

二.OSGI 实战:

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

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


OSGI 加载 Bundle 模块的顺序

  1. 首先检查包名是否以 java.* 开头,或者是否在一个特定的配置文件(org.osgi.framework.bootdelegation)中定义。如果是,则 bundle 类加载器立即委托给父类加载器(通常是 Application 类加载器),如果不是则进入 2
  2. 检查是否在 Import-Package、Require-Bundle 委派列表里,如果是委托给对应 Bundle 类加载器,如果不是,进入 3
  3. 检查是否在当前 Bundle 的 Classpath 里,如果是使用自己的类加载器加载,如果不是,进入 4
  4. 搜索可能附加在当前 bundle 上的 fragment 中的内部类,找到则委派给 Fragment bundle 类加载器加载,如果找不到,进入 5
  5. 查找动态导入列表里的 Bundle,委派给对应的类加载器加载,否则类加载失败

如果用 Java 的结构的项目去部署,当项目复杂度提升时,每次上线,代码只是增加或者修改了部分功能,但都得关掉服务,重新部署所有的代码和配置,管理沟通成本都很高,很容产生线上事故,而 OSGI 的应用是一个模块化的系统,避免了部署时 jar 或 classpath 错综复杂依赖管理,发布应用和更新应用都很强大,可以热替换特定的 Bundle 模块,提高部署可靠性。

接下来我们用IDE创建一个OSGI应用,首先要去 http://download.eclipse.org/equinox/ 下载最新的OSGI 框架enquinox

创建一个 OSGI 应用。打开 Eclipse,File->New->Project:

选择 OSGI 框架 Equniox(Eclipse 强大的插件机制就是构建于 OSGI Bundle 之上,Eclipse 本身就包含了 Equniox) :


接下来,勾选创建 Activator 类,新建一个创Activator 类,每个 Bundle 启动时都会调用 Bundle(模块)里 Activator(类)的 start 方法,停止时调用 stop 方法,代码如下:


import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

/**
 * Created by cong on 2018/8/5.
 */
public class Activator implements BundleActivator {

    private static BundleContext context;

    static BundleContext getContext() {
        return context;
    }

    public void start(BundleContext bundleContext) throws Exception {
        Activator.context = bundleContext;
        //添加输出This is OSGI Projcect
        System.out.println("This is OSGI Projcect");
    }

    public void stop(BundleContext bundleContext) throws Exception {
        Activator.context = null;
    }

}

接下来进行一下配置,Run->Run Configuration-> 双击 OSGI Framework 生成项目配置:如下图:

 

然后点击运行按钮,可以看到控制台输出 This is OSGI Projcect。在控制台我们输入 ss ( short status) 查看服务状态:


This is OSGI Projcect
osgi> ss
"Framework is launched."


id    State       Bundle
0    ACTIVE      org.eclipse.osgi_3.12.100.v20180210-1608
1    ACTIVE      org.apache.felix.gogo.runtime_0.10.0.v201209301036
2    ACTIVE      org.apache.felix.gogo.command_0.10.0.v201209301215

// ACTIVE表明 com.osgi.bundle.demo Bundle运行中 

3    ACTIVE      com.osgi.bundle.demo_1.0.0.qualifier
4    ACTIVE      org.apache.felix.gogo.shell_0.10.0.v201212101605
5    ACTIVE     org.eclipse.equinox.console_1.1.300.v20170512-2111
// 停止 com.osgi.bundle.demo Bundle  
osgi> stop com.osgi.bundle.demo
osgi> ss
"Framework is launched."


id    State       Bundle
0    ACTIVE      org.eclipse.osgi_3.12.100.v20180210-1608
1    ACTIVE      org.apache.felix.gogo.runtime_0.10.0.v201209301036
2    ACTIVE      org.apache.felix.gogo.command_0.10.0.v201209301215

// RESOLVED 表明 Bundle com.osgi.bundle.demo 停止了 

3    RESOLVED    com.osgi.bundle.demo_1.0.0.qualifier
4    ACTIVE      org.apache.felix.gogo.shell_0.10.0.v201212101605
5    ACTIVE      org.eclipse.equinox.console_1.1.300.v20170512-2111
// 通过close关闭整个应用框架
osgi> close
Really want to stop Equinox? (y/n; default=y)  y
osgi> 

一个 Bundle 包含 MANIFEST.MF,也就是 Bundle 的头信息,Java 代码以及配置文件(XML,Properties),其中 MANIFEST.MF 包含了下面的信息。如下所示:


/*版本号*/
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
/*名字*/
Bundle-Name: Demo
Bundle-SymbolicName: com.osgi.bundle.demo
Bundle-Version: 1.0.0.qualifier
/*Bundle类*/
Bundle-Activator: com.osgi.bundle.demo.Activator
Bundle-Vendor: OSGI
/*依赖环境*/
Bundle-RequiredExecutionEnvironment: JavaSE-1.7
/*导入的包*/
Import-Package: org.osgi.framework;version="1.3.0"
Bundle-ActivationPolicy: lazy

Equinox OSGi 命令列表

  1.控制框架

  1.launch 启动框架

  2.shutdown 停止框架

  3.close 关闭、退出框架

  4.exit 立即退出,相当于 System.exit

  5.init 卸载所有 bundle(前提是已经 shutdown)

  6.setprop 设置属性,在运行时进行

  2.控制 Bundle

  1.Install 安装 uninstall 卸载

  2.Stop 停止

  3.Refresh 刷新

  4.Update 更新

  3.展示状态

  1.Status 展示安装的 bundle 和注册的服务

  2.Ss 展示所有 bundle 的简单状态

  3.Services 展示注册服务的详细信息

  4.Packages 展示导入、导出包的状态

  5.Bundles 展示所有已经安装的 bundles 的状态

  6.Headers 展示 bundles 的头信息,即 MANIFEST.MF 中的内容

  7.Log 展示 LOG 入口信息

  4.其他

Exec 在另外一个进程中执行一个命令(阻塞状态)

  1.Fork 和 EXEC 不同的是不会引起阻塞

  2.Gc 促使垃圾回收

  3.Getprop 得到属性,或者某个属性

  5.控制启动级别

  1.Sl 得到某个 bundle 或者整个框架的 start level 信息

  2.Setfwsl 设置框架的 start level

  3.Setbsl 设置 bundle 的 start level

  4.setibsl 设置初始化 bundle 的 start level

 

目录
相关文章
|
3月前
|
安全 前端开发 Java
【JVM的秘密揭秘】深入理解类加载器与双亲委派机制的奥秘!
【8月更文挑战第25天】在Java技术栈中,深入理解JVM类加载机制及其双亲委派模型是至关重要的。JVM类加载器作为运行时系统的关键组件,负责将字节码文件加载至内存并转换为可执行的数据结构。其采用层级结构,包括引导、扩展、应用及用户自定义类加载器,通过双亲委派机制协同工作,确保Java核心库的安全性与稳定性。本文通过解析类加载器的分类、双亲委派机制原理及示例代码,帮助读者全面掌握这一核心概念,为开发更安全高效的Java应用程序奠定基础。
87 0
|
2月前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
26天前
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
34 3
|
2月前
|
Arthas Java 测试技术
JVM —— 类加载器的分类,双亲委派机制
类加载器的分类,双亲委派机制:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器;JDK8及之前的版本,JDK9之后的版本;什么是双亲委派模型,双亲委派模型的作用,如何打破双亲委派机制
JVM —— 类加载器的分类,双亲委派机制
|
1月前
|
前端开发 Java 应用服务中间件
JVM进阶调优系列(1)类加载器原理一文讲透
本文详细介绍了JVM类加载机制。首先解释了类加载器的概念及其工作原理,接着阐述了四种类型的类加载器:启动类加载器、扩展类加载器、应用类加载器及用户自定义类加载器。文中重点讲解了双亲委派机制,包括其优点和缺点,并探讨了打破这一机制的方法。最后,通过Tomcat的实际应用示例,展示了如何通过自定义类加载器打破双亲委派机制,实现应用间的隔离。
|
6月前
|
前端开发 安全 Java
深入浅出JVM(八)之类加载器
深入浅出JVM(八)之类加载器
|
3月前
|
数据库 C# 开发者
WPF开发者必读:揭秘ADO.NET与Entity Framework数据库交互秘籍,轻松实现企业级应用!
【8月更文挑战第31天】在现代软件开发中,WPF 与数据库的交互对于构建企业级应用至关重要。本文介绍了如何利用 ADO.NET 和 Entity Framework 在 WPF 应用中访问和操作数据库。ADO.NET 是 .NET Framework 中用于访问各类数据库(如 SQL Server、MySQL 等)的类库;Entity Framework 则是一种 ORM 框架,支持面向对象的数据操作。文章通过示例展示了如何在 WPF 应用中集成这两种技术,提高开发效率。
52 0
|
3月前
|
开发者 C# Windows
WPF布局大揭秘:掌握布局技巧,轻松创建响应式用户界面,让你的应用程序更上一层楼!
【8月更文挑战第31天】在现代软件开发中,响应式用户界面至关重要。WPF(Windows Presentation Foundation)作为.NET框架的一部分,提供了丰富的布局控件和机制,便于创建可自动调整的UI。本文介绍WPF布局的基础概念与实现方法,包括`StackPanel`、`DockPanel`、`Grid`等控件的使用,并通过示例代码展示如何构建响应式布局。了解这些技巧有助于开发者优化用户体验,适应不同设备和屏幕尺寸。
75 0
|
3月前
|
安全 前端开发 Java
【JVM 探秘】ClassLoader 类加载器:揭秘 Java 类加载机制背后的秘密武器!
【8月更文挑战第25天】本文全面介绍了Java虚拟机(JVM)中的类加载器,它是JVM的核心组件之一,负责将Java类加载到运行环境中。文章首先概述了类加载器的基本工作原理及其遵循的双亲委派模型,确保了核心类库的安全与稳定。接着详细阐述了启动、扩展和应用三种主要类加载器的层次结构。并通过一个自定义类加载器的例子展示了如何从特定目录加载类。此外,还介绍了类加载器的完整生命周期,包括加载、链接和初始化三个阶段。最后强调了类加载器在版本隔离、安全性和灵活性方面的重要作用。深入理解类加载器对于掌握JVM内部机制至关重要。
141 0
|
4月前
|
存储 前端开发 Java
(二)JVM成神路之剖析Java类加载子系统、双亲委派机制及线程上下文类加载器
上篇《初识Java虚拟机》文章中曾提及到:我们所编写的Java代码经过编译之后,会生成对应的class字节码文件,而在程序启动时会通过类加载子系统将这些字节码文件先装载进内存,然后再交由执行引擎执行。本文中则会对Java虚拟机的类加载机制以及执行引擎进行全面分析。