「JVM 原理使用」 实际开发中的应用

news2024/11/23 23:42:22

Class 文件格式、执行引擎主要以 Class 文件描述了存储格式、类何时加载、如何连接、VM 如何执行字节码指令,这些动作基本都是 JVM 直接控制,用户代码无法干预和改变;

用户可以干预的只有字节码生成、类加载器两部分,而这两部分的应用是许多常用功能和程序实现的基础;

文章目录

      • 1. Tomcat: 正统的类加载器架构
      • 2. OSGi: 灵活的类加载器架构
      • 3. 字节码生成技术与动态代理的实现
      • 4. Backport 工具: Java 逆向移植

1. Tomcat: 正统的类加载器架构

一个功能完备的 Web 服务器(Tomcat、Jetty、WebLogic、WebSphere),都要解决如下问题:

  • 部署在同一服务器的两个 Web 应用程序所使用的 Java 类库实现相互隔离,相互独立使用;
  • 部署在同一服务器的两个 Web 应用程序所使用的 Java 类库实现相互共享,减少资源(主要是内存,JVM 方法区)浪费;
  • 服务器要尽可能保障自身安全不受部署的 Web 应用程序影响,服务器使用的类库应与应用程序的类库相互独立;
  • 支持 JSP 的 Web 服务器大多支持 HotSwap 功能,而 JSP 纯文本存储的特性导致其被篡改的几率远大于 Java 类库和自身 Class 文件,因此也需要提供 Production Mode(生成模式) 下不处理 JSP 文件的功能(如 WebLogic);

单独的一个 ClassPath 无法满足 Web 应用的部署需求,因此 Web Server 各自提供了不同含义的第三方类库 ClassPath(lib、classes,各自对应不同的访问范围和服务对象),每个路径需要相应的自定义类加载器去加载里面的 Java 类库;

Tomcat 目录

  • /common/*,Common 类加载器负责,类库可以被 Tomcat 和所有 Web 应用程序共同使用;
  • /server/*,Catalina 类加载器(也称 Server 类加载器)负责,类库可被 Tomcat 使用,对所有 Web 应用程序不可见;
  • /shared/*,Shared 类加载器负责,类库可以被所有 Web 应用程序共同使用,对 Tomcat 不可见;
  • /lib/*,Tomcat 6 之后,默认不开启 server.loader 和 share.loader 两个类加载器,而是用 Common 类加载器替代,因此 /common/*/server/*/shared/* 三个路径默认合并到了一个 /lib/* 路径,相当于 /common/* 的作用,
  • /WEB-INF/*,Webapp 类加载器负责,每个 Web 应用程序对应一个 Webapp 类加载器,类库仅可被当前 Web 应用程序使用,对 Tomcat 和其他 Web 应用程序不可见;

请添加图片描述

JSP 类加载器负责 JSP 文件的加载,每个 JSP 文件对应一个 JasperLoader 类加载器;而 JSP 加载器存在的目的就是被丢弃,一旦 JSP 文件被修改,就需要新建一个 JSP 类加载器替换旧的,以实现 JSP 文件的 HotSwap 功能;

Common 类加载器能够加载的类可以被 Catalina 类加载器和 Shared 类加载器使用;Catalina 类加载器和 Shared 类加载器加载的类相互隔离;Webapp 类加载器可以使用 Shared 类加载器加载的类,但各 Webapp 类加载器加载实例之间相互隔离;

2. OSGi: 灵活的类加载器架构

OSGi 的研究有助于学习类加载器的知识;

OSGiOpen Service Gateway Initiative,OSGi 联盟制定的一个基于 Java 语言的动态的模块系统规范(JDK 9 引入的 JPMS 是静态的模块系统);因与 JPMS 模块化特性重叠,OSGi 现在着重向动态模块化系统的方向发展,用于实现模块级热插拔等;许多大型软件平台和中间件服务器都是基于或声明将要基于 OSGi 规范来实现的(如 IBM Jazz、GlassFish、JBoss OSGi 等);

Bundle,OSGi 的模块,类似于普通 Java 类库,且都是以 JAR 格式封装,内部都是 Java 的 Package 和 Class;但 Bundle 可以额外声明它所依赖的 Package(Import Package)和它允许导出发布的 Package(Export Package,未 export 的 package 和 Class 对外将是不可见的),用于精准控制可见性和依赖关系;

OSGi 的 Bundle 类加载器之间只有规则,没有固定的委派关系;所有 Package 的类加载动作都会委派给发布它的 Bundle 类加载器来完成;不涉及某个具体 Package 时,所有 Bundle 加载器都是平级关系,只有具体到某个 Package 或 Class 时,才根据 Package 导入导出定义来构造 Bundle 间的委派和依赖;

假设存在 Bundle A、Bundle B、Bundle C,且 3 个 Bundle 的依赖关系如下:

  • Bundle A: 声明发布了 packageA,依赖了 java.* 的包;
  • Bundle B: 声明依赖了 packageA 和 packageC,同时也依赖了 java.* 的包;
  • Bundle C: 声明发布了 packageC,依赖了 packageA;

3 个 Bundle 之间的类加载器及父类加载器之间的关系如下:

请添加图片描述

  • 以 java.* 开头的类,委派给父类加载器;
  • 否则,在委派列表名单的类,委派给父类加载器;
  • 否则,Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器;
  • 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器;
  • 否则,查找是否在自己的 Fragment Bundle 中,若在,则委派给 Fragment Bundle 的类加载器;
  • 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器;
  • 否则,类查找失败;

若 Bundle A 依赖 Bundle B 的 Package B,Bundle B 依赖 Bundle A 的 Package A;在类加载时,首先加载 Bundle A 时会锁定 Bundle A 的类加载器实例,然后委派 Bundle B 的类加载器去加载 Bundle B;而此时加载 Bundle B 的线程也会锁定 Bundle B 的类加载器实例,再去请求 Bundle A 的 Package A,如此相互等待,就会形成死锁;(这个问题在 JDK 7 之前可以通过 osgi.classloader.singleTheadLoads 参数强制单线程串行进行类加载解决,但会损耗性能;在 JDK 7 之后才从 JDK 层面得到解决,将锁的级别从 ClassLoader 对象本身降到了要加载的类名级别)

3. 字节码生成技术与动态代理的实现

JDK 中的 javac 是字节码技术的老祖宗,学习字节码的最好方式是阅读 javac 的源码(jdk.compiler/xhare/classes/com/sun/tools/javac);

使用字节码的场景有 Web 服务器中的 JSP 编译器、编译时织入的 AOP、动态代理、反射也可能通过运行时生成字节码提高执行速度;

动态代理,针对使用 Java 代码实现代理类的静态代理写法,动态代理在原始类和接口未知的情况下,实现代理类的行为,让代理类和原始类脱离了直接关系,从而让代理可以灵活的重用在不同场景;Spring 通过动态代理的方式对 Bean 进行增强;

动态代理示例

public class DynamicProxyTest {
    interface IHello {
        void sayHello();
    }

    static class Hello implements IHello {
        @Override
        public void sayHello() {
            System.out.println("hello world");
        }
    }

    static class DynamicProxy implements InvocationHandler {
        Object originalObj;

        Object bind(Object originalObj) {
            this.originalObj = originalObj;
            return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this);
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("welcome");
            return method.invoke(originalObj, args);
        }
    }

    public static void main(String[] args) {
        IHello hello = (IHello) new DynamicProxy().bind(new Hello());
        hello.sayHello();
        hello.toString();
        // 输出:
        // welcome
        // hello world
        // welcome
    }
}

Proxy::newProxyInstance() 方法返回一个实现了 IHello 的接口,并代理了 new Hello() 实例化的对象;其中程序进行了验证、优化、缓存、同步、生成字节码、显示类加载等;

设置保存运行时生成的代理类字节码文件

// 在 main() 中添加如下代码
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

可以得到 #Proxy0.class 的 Class 文件,通过反编译,可以得到动态代理类;

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package edu.aurelius.jvm.clazz;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

final class $Proxy0 extends Proxy implements DynamicProxyTest.IHello {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void sayHello() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("edu.aurelius.jvm.clazz.DynamicProxyTest$IHello").getMethod("sayHello");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

这里 super.h 是父类 Proxy 中保存的 InvocationHandler 对象(DynamicProxy 对象),统一调用 super.h 对象的 invoke() 方法来实现代理方法,让 DynamicProxy 对象 的 invoke() 中插入的逻辑可以作用在所有被代理对象(new Hello() 实例化的对象)的方法上;

实际开发中以字节为单位拼接字节码的应用场景几乎是很少见的,在需要大量操作字节码时,建议使用封装好的字节码类库(Javassist、GCLib、ASM);

4. Backport 工具: Java 逆向移植

Java Backporting Tools,Java 逆向移植工具,如 Retrotranslator 和 Retrolambda;

  • Retrotranslator,将 JDK 5 编译出来的 Class 文件转变为可以在 JDK 1.4 或 1.3 上部署的版本,从而支持自动装箱、泛型、动态注解、枚举、变长参数、遍历循环、静态导入、集合改进、并发包、泛型与注解等的反射等语法特性;
  • Retrolambda,将 JDK 8 的 Lambda 表达式、try-resources、接口默认方法等语法转换为在 JDK 5、6、7 部署环境上支持的形式;

JDK 升级新增功能分类

  • 对 Java 类库 API 增强;如 JDK 1.2 引入的 java.util.Collections 集合包,JDK 5 引入的 java.util.concurrent 并发包,JDK 7 引入的 java.lang.invoke 包;
  • 对前端编译层面做改进,支持新的语法糖;如自动装箱拆箱(编译器自动对包装对象使用 Integer.valueOf() 等代码)、变成参数(编译后自动转化为数组)、泛型(编译阶段擦除泛型信息,在元数据中保存,编译器自动插入类型转换代码);
  • 在字节码中进行支持的改动;如 JDK 7 引入的动态语言支持,在 JVM 中新增了 invokedynamic 字节码指令;
  • JDK 整体结构层面的改进;如 JDK 9 引入的 Java 模块化系统,涉及 JDK 结构、Java 语法、类加载和连接过程、JVM 等各个层面;
  • JVM 内部的改进;如 JDK 5 定义的 Java 内存模型(Java Memory Model,JMM),JDK 7、JDK 11、JDK 12 中新增的 G1、ZGC、Shenandoah 收集器等;这类改动对 Java 应用程序是透明的,只会影响程序运行时;

逆向移植可以较好的模拟前 2 类升级,后 3 类则无能为力或影响运行效率;

对第 1 类升级的逆向移植可以通过独立类库代替 JDK 中部分功能的方式来实现;
对第 2 类升级需要使用 ASM 等字节码操作工具对字节码中元数据信息和一些语法支持的内容做相应修改;
对第 3 类升级则需要绕开字节码指令,牺牲了性能;如 lambda 表达式的逆向支持,其实是生成了一组匿名内部类代替 Lambda;IntelliJ IDEA 中将匿名内部类显示成 Lambda 表达式其实就是反向使用这个过程的效果;


上一篇:「JVM 执行引擎」栈架构的字节码的解释执行引擎

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/346624.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android 面试 基础知识汇总

Android常用的设计模式HTTP和HTTPS的区别TCP/UDP的区别

PendingIntent兼容安卓12 的一个问题

这是我第一次遇到这个问题,反正我也是第一次做完整的安卓app,很多个第一次。我之前做的那个老旧手机做桌面摆件的app,因为原来的“无干预”版本无法使用,所以又用回了“和风版”,但这个版本很久没用了,有些…

实用机器学习-学习笔记

文章目录9.1模型调参9.1.1思考与总结9.1.2 基线baseline9.1.3SGD ADAM9.1.4 训练代价9.1.5 AUTOML9.1.6 要多次调参管理9.1.7复现实验的困难9.1模型调参 9.1.1思考与总结 1了解了baseline和调参基本原则 2了解了adams和sgd的优劣 3了解了训练树和神经网络的基本代价 4了解了a…

【蓝桥杯集训2】差分专题(3 / 4)

目录 差分模板 1、一维差分 2、二维差分 3729. 改变数组元素 - 差分 区间修改 100. 增减序列 - 差分模板 1、一维差分 活动 - AcWing 给a数组 [l,r] 区间的每个数c,只需要给其差分数组b做如下操作即可 b[l]c; b[r1]-c; 差分数组 a[]是b[]的前缀和数组 如果…

倒计时100天 | DBF深圳国际户外运动博览会5月一起狂飙初夏

狂飙初夏的大型户外运动嘉年华即将登场!2023年5月26日至28日,第四届DBF深圳国际户外运动博览会(简称DBF深圳户外展)将于深圳国际会展中心(宝安)举办。本届展会展览面积超过7万平方米,预计将吸引…

保姆级Vue3+Vite项目实战黑白模式切换

写在前面注:本文首发掘金签约专栏,此为文章同步!本文为 Vue3Vite 项目实战系列教程文章第四篇,系列文章建议从头观看效果更佳,大家可关注专栏防走失!点个赞再看有助于全文完整阅读!此系列文章主…

C/C++【内存管理】

✨个人主页: Yohifo 🎉所属专栏: C修行之路 🎊每篇一句: 图片来源 Love is a choice. It is a conscious commitment. It is something you choose to make work every day with a person who has chosen the same thi…

商城系统必备营销工具(五)——积分商城

做商城,流量必不可少,日活跃度也很重要。现在各大APP、网站、小程序和微商城,基本都在为了巩固流量做积分商城,虽然已经随处可见,但很多企业商家却并没有将积分商城运作起来,积分商城也没有人浏览兑换商品。…

跟ChatGPT,聊聊ChatGPT

不仅“上知天文、下知地理”,似乎还能对答如流、出口成诗,甚至还能写剧本、编音乐、写代码——最近,一款名叫ChatGPT的人工智能聊天机器人火爆全球。由此,一系列关于新一代技术变革、人工智能替代人力、巨头企业扎堆入局AI的讨论在…

Multi Paxos

basic paxos 是用于确定且只能确定一个值,“只确定一个值有什么用?这可解决不了我面临的问题,例如每个用户都要多次保存数据.” 你心中可能有这样的疑问。 原simple paxos论文里有提到一连串个instance of paxos [4] 但没有提出 multi paxos的概念&…

ChatGPT国内镜像站试用,聊天、Python代码生成。

ChatGPT国内镜像站试用,聊天、Python代码生成。 (本文获得CSDN质量评分【91】)【学习的细节是欢悦的历程】Python 官网:https://www.python.org/ Free:大咖免费“圣经”教程《 python 完全自学教程》,不仅仅是基础那么简单…… …

前端开发:关于diff算法详解

前言 前端开发中,关于JS原生的内容和前端算法相关的内容一直都是前端工作中的核心,不管是在实际的前端业务开发还是前端求职面试,都是非常重要且必备的内容。那么本篇博文来分享一个关于前端开发中必备内容:diff算法,d…

ChatGPT背后的技术可以给数据治理带来哪些神奇的效果?_光点科技

最近,由美国人工智能研究室OpenAI开发的全新“聊天机器人”ChatGPT火了。作为一款人工智能语言模型,它不仅能和人展开互动,还可以写文章、制定方案、创作诗歌,甚至编写代码、检查漏洞样样精通,上线仅两个月全球活跃用户…

Python雪花代码

前言 用python画个雪花玩玩,源码在文末公众号哈。 雪花类 class Snow(): #雪花类 def __init__(self): self.r 6 #雪花的半径 self.x ra.randint(-1000,1000) #雪花的横坐标 self.y ra.randint(-500,5…

剑指 Offer 10- I. 斐波那契数列[c语言]

目录题目思路代码结果该文章只是用于记录考研复试刷题题目 力扣斐波那契数列 写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下: F(0) 0, F(1) 1 …

卷积神经网络-D2L

从全连接层到卷积 企业级理解卷积 不稳定输入 稳定输出 求系统存量 - 信号系统周围像素点如何产生影响 - 图像处理一个像素点如何试探 - 图像识别 好处: 平移不变性和局部性 图像卷积 import torch from torch import nn from d2l import torch as d2ldef try_…

Java高频面试题,ReentrantLock 是如何实现锁公平和非公平性的?

我先解释一下个公平和非公平的概念。 公平,指的是竞争锁资源的线程,严格按照请求顺序来分配锁。 非公平,表示竞争锁资源的线程,允许插队来抢占锁资源。 ReentrantLock 默认采用了非公平锁的策略来实现锁的竞争逻辑。 其次&…

SqlServer的LDF文件丢失, 如何仅用MDF文件恢复数据库呢?(已解决)

笔者的一个大小为2 TB的SQL Server的database的LDF文件在玩存储盘映射的过程中莫名其妙的丢失了. 好在MDF文件还在. 笔者慌了, Bruce Ye告诉笔者, 不用着急, 光用MDF也可以把数据库弄回来的. 笔者就问Bruce, 假设我可以容忍LDF中信息的丢失的话, 那么该如何恢复这个数据库呢?我…

快速搭建个人在线书库,随时随地畅享阅读!

前边我们利用NAS部署了个人的导航页、小说站、云笔记,今天,我们再看看怎么部署一个个人的在线书库。 相信很多朋友都在自己的电脑中收藏了大量的PDF、MOBI等格式的电子书籍,但是一旦换了一台设备,要么是无法翻阅,要么…

如何为报表开发工具 FastReport .NET 设置 Apache 2 Web 服务器?

FastReport .NET是一款全功能的Windows Forms、ASP.NET和MVC报表分析解决方案,使用FastReport .NET可以创建独立于应用程序的.NET报表,同时FastReport .Net支持中文、英语等14种语言,可以让你的产品保证真正的国际性。专业版和企业版包括Fast…