通过源代码分析 Java 中的资源加载
前提
最近在做一个基础组件项目刚好需要用到JDK中的资源加载,这里说到的资源包括类文件和其他静态资源,刚好需要重新补充一下类加载器和资源加载的相关知识,整理成一篇文章。
理解类的工作原理
这一节主要分析类加载器和双亲委派模型。
什么是类加载器
虚拟机设计团队把类加载阶段中的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到了Java虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类,而实现这个动作的代码模块称为"类加载器(ClassLoader)"。
类加载器虽然只用于实现类加载的功能,但是它在Java程序中起到的作用不局限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立类在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。上面这句话直观来说就是:比较两个类是否"相等",只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这个两个类是来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类必然"不相等"。这里说到的"相等"包括代表类的Class对象的equals()
方法、isAssignableFrom()
方法、isInstance()
方法的返回结果,也包括使用instanceOf
关键字做对象所属关系判定等情况。
类和加载它的类加载器确定类在Java虚拟机中的唯一性这个特点为后来出现的热更新类、热部署等技术提供了基础。
双亲委派模型
从Java虚拟机的角度来看,只有两种不同的类加载器:
- 1、第一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++编程语言实现,是虚拟机的一部分。
- 2、另一种是其他的类加载器,这些类加载器都是由Java语言实现,独立于虚拟机之外,一般就是内部于JDK中,它们都继承自抽象类加载器java.lang.ClassLoader。
JDK中提供几个系统级别的类加载器:
- 1、启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在${JAVA_HONE}\lib目录中,或者被XbootstrapPath参数所指定的目录中,并且是虚拟机基于一定规则(如文件名称规则,如rt.jar)标识的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,开发者在编写自定义类加载器如果想委派到启动类加载器只需直接使用null替代即可。
- 2、扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher的静态内部类ExtClassLoader实现,它负责加载${JAVA_HONE}\lib\ext目录中,或者通过java.ext.dirs系统变量指定的路径中的所有类库,开发者可以直接使用此类加载器。
- 3、应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher的静态内部类AppClassLoader实现,但是由于这个类加载器的实例是ClassLoader中静态方法
getSystemClassLoader()
中的返回值,一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自实现的类加载器,一般情况下这个系统类加载器就是应用程序中默认使用的类加载器。 - 4、线程上下文类加载器(Thread Context ClassLoader):这个在下一小节"破坏双亲委派模型"再分析。
Java开发者开发出来的Java应用程序都是由上面四种类加载器相互配合进行类加载的,如果有必要还可以加入自定义的类加载器。其中,启动类加载器、扩展类加载器、应用程序类加载器和自定义类加载器之间存在着一定的关系:
上图展示的类加载器之间的层次关系称为双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的类加载器(Java中顶层的类加载器一般是Bootstrap ClassLoader),其他的类加载器都应当有自己的父类加载器。这些类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是通过组合(Composition)的关系实现。类加载器层次关系这一点可以通过下面的代码验证一下:
public class Main {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = Main.class.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
System.out.println(classLoader.getParent().getParent());
}
}
//输出结果,最后的null说明是Bootstrap ClassLoader
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4629104a
null
双亲委派模型的工作机制:如果一个类加载器收到了类加载的请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该传送到顶层的类加载器中,只有当父类加载器反馈自己无法完成当前的类加载请求的时候(也就是在它的搜索范围中没有找到所需要的类),子类加载器才会尝试自己去加载类。不过这里有一点需要注意,每一个类加载器都会缓存已经加载过的类,也就是重复加载一个已经存在的类,那么就会从已经加载的缓存中加载,如果从当前类加载的缓存中判断类已经加载过,那么直接返回,否则会委派类加载请求到父类加载器。这个缓存机制在AppClassLoader和ExtensionClassLoader中都存在,至于BootstrapClassLoader未知。
双亲委派模型的优势:使用双亲委派模型来组织类加载器之间的关系,一个比较显著的优点是Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang
包中的类库,它存放在rt.jar
中,无论使用哪一个类加载加载java.lang
包中的类,最终都是委派给处于模型顶层的启动类加载器进行加载,因此java.lang
包中的类如java.lang.Object
类在应用程序中的各类加载器环境中加载的都是同一个类。试想,如果可以使用用户自定义的ClassLoader去加载java.lang.Object
,那么用户应用程序中就会出现多个java.lang.Object
类,Java类型体系中最基础的类型也有多个,类型体系的基础行为无法保证,应用程序也会趋于混乱。如果尝试编写rt.jar
中已经存在的同类名的类通过自定义的类加载进行加载,将会接收到虚拟机抛出的异常。
双亲委派模型的实现:类加载器双亲委派模型的实现提现在ClassLoader的源码中,主要是ClassLoader#loadClass()
中。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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 {
//父加载器不为null,说明父加载器不是BootstrapClassLoader
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//父加载器为null,说明父加载器是BootstrapClassLoader
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;
}
}
破坏双亲委派模型
双亲委派模型在Java发展历史上出现了三次比较大"被破坏"的情况:
-
1、ClassLoader在JDK1.0已经存在,JDK1.2为了引入双亲委派模型并且需要向前兼容,java.lang.ClassLoader类添加了一个新的protected的
findClass()
方法,在这之前,用户去继承java.lang.ClassLoader只能重写其loadClass()
方法才能实现自己的目标。 -
2、双亲委派模型自身存在缺陷:双亲委派很好地解决了各个类加载器的基础类的加载的统一问题(越基础的类由越上层的类加载器加载),这些所谓的基础类就是大多数情况下作为用户调用的基础类库和基础API,但是无法解决这些基础类需要回调用户的代码这一个问题,典型的例子就是JNDI。JNDI的类库代码是启动类加载器加载的,但是它需要调用独立厂商实现并且部署在应用的ClassPath的JNDI的服务接口提供者(SPI,即是Service Provider Interface)的代码,但是启动类加载器无法加载ClassPath下的类库。为了解决这个问题,Java设计团队引入了不优雅的设计:线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过java.lang.Thread类的
setContextClassLoader()
设置,这样子,JNDI服务就可以使用线程上下文类加载器去加载所需的SPI类库,但是父类加载器中请求子类加载器去加载类这一点已经打破了双亲委派模型。目前,JNDI、JDBC、JCE、JAXB和JBI等模块都是通过此方式实现。 -
3、基于用户对应用程序动态性的热切追求:如代码热替换(HotSwap)、热模块部署等,说白了就是希望应用程序能像我们的计算机外设那样可以热插拔,因此催生出
JSR-291
以及它的业界实现OSGi,而OSGi定制了自己的类加载规则,不再遵循双亲委派模型,因此它可以通过自定义的类加载器机制轻易实现模块的热部署。
JDK中提供的资源加载API
前边花大量的篇幅去分析类加载器的预热知识,是因为JDK中的资源加载依赖于类加载器(其实类文件本来就是资源文件的一种,类加载的过程也是资源加载的过程)。这里先列举出JDK中目前常用的资源(Resource)加载的API,先看ClassLoader中提供的方法。
ClassLoader提供的资源加载API
//1.实例方法
public URL getResource(String name)
//这个方法仅仅是调用getResource(String name)返回URL实例直接调用URL实例的openStream()方法
public InputStream getResourceAsStream(String name)
//这个方法是getResource(String name)方法的复数版本
public Enumeration<URL> getResources(String name) throws IOException
//2.静态方法
public static URL getSystemResource(String name)
//这个方法仅仅是调用getSystemResource(String name)返回URL实例直接调用URL实例的openStream()方法
public static InputStream getSystemResourceAsStream(String name)
//这个方法是getSystemResources(String name)方法的复数版本
public static Enumeration<URL> getSystemResources(String name)
总的来看,只有两个方法需要分析:getResource(String name)
和getSystemResource(String name)
。查看getResource(String name)
的源码:
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);
}
return url;
}
是否似曾相识?这里明显就是使用了类加载过程中类似的双亲委派模型进行资源加载,这个方法在API注释中描述通常用于加载数据资源如images、audio、text等等,资源名称需要使用路径分隔符'/'。getResource(String name)
方法中查找的根路径我们可以通过下面方法验证:
public class ResourceLoader {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = ResourceLoader.class.getClassLoader();
URL resource = classLoader.getResource("");
System.out.println(resource);
}
}
//输出:file:/D:/Projects/rxjava-seed/target/classes/
很明显输出的结果就是当前应用的ClassPath,总结来说:ClassLoader#getResource(String name)
是基于用户应用程序的ClassPath搜索资源,资源名称必须使用路径分隔符'/'去分隔目录,但是不能以'/'作为资源名的起始,也就是不能这样使用:classLoader.getResource("/img/doge.jpg")
。接着我们再看一下ClassLoader#getSystemResource(String name)
的源码:
public static URL getSystemResource(String name) {
//实际上Application ClassLoader一般不会为null
ClassLoader system = getSystemClassLoader();
if (system == null) {
return getBootstrapResource(name);
}
return system.getResource(name);
}
此方法优先使用应用程序类加载器进行资源加载,如果应用程序类加载器为null(其实这种情况很少见),则使用启动类加载器进行资源加载。如果应用程序类加载器不为null的情况下,它实际上退化为ClassLoader#getResource(String name)
方法。
总结一下:ClassLoader提供的资源加载的方法中的核心方法是ClassLoader#getResource(String name)
,它是基于用户应用程序的ClassPath搜索资源,遵循"资源加载的双亲委派模型",资源名称必须使用路径分隔符'/'去分隔目录,但是不能以'/'作为资源名的起始字符,其他几个方法都是基于此方法进行衍生,添加复数操作等其他操作。getResource(String name)
方法不会显示抛出异常,当资源搜索失败的时候,会返回null。
Class提供的资源加载API
java.lang.Class中也提供了资源加载的方法,如下:
public java.net.URL getResource(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResource(name);
}
return cl.getResource(name);
}
public InputStream getResourceAsStream(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResourceAsStream(name);
}
return cl.getResourceAsStream(name);
}
从上面的源码来看,Class#getResource(String name)
和Class#getResourceAsStream(String name)
分别比ClassLoader#getResource(String name)
和ClassLoader#getResourceAsStream(String name)
只多了一步,就是搜索之前先进行资源名称的预处理resolveName(name)
,我们重点看这个方法做了什么:
private String resolveName(String name) {
if (name == null) {
return name;
}
if (!name.startsWith("/")) {
Class<?> c = this;
while (c.isArray()) {
c = c.getComponentType();
}
String baseName = c.getName();
int index = baseName.lastIndexOf('.');
if (index != -1) {
name = baseName.substring(0, index).replace('.', '/')
+"/"+name;
}
} else {
name = name.substring(1);
}
return name;
}
逻辑相对比较简单:
- 1、如果资源名称以'/'开头,那么直接去掉'/',这个时候的资源查找实际上退化为ClassPath中的资源查找。
- 2、如果资源名称不以'/'开头,那么解析出当前类的实际类型(因为当前类有可能是数组),取出类型的包路径,替换包路径中的'.'为'/',再拼接原来的资源名称。举个例子:"club.throwable.Main.class"中调用了
Main.class.getResource("doge.jpg")
,那么这个调用的处理资源名称的结果就是club/throwable/doge.jpg
。
小结:如果看过我之前写过的一篇URL和URI相关的文章就清楚,实际上Class#getResource(String name)
和Class#getResourceAsStream(String name)
的资源名称处理类似于相对URL的处理,而"相对URL的处理"的根路径就是应用程序的ClassPath。如果资源名称以'/'开头,那么相当于从ClassPath中加载资源,如果资源名称不以'/'开头,那么相当于基于当前类的实际类型的包目录下加载资源。
实际上类似这样的资源加载方式在File类中也存在,这里就不再展开。
小结
理解JDK中的资源加载方式有助于编写一些通用的基础组件,像Spring里面的ResourceLoader、ClassPathResource这里比较实用的工具也是基于JDK资源加载的方式编写出来。下一篇博文《浅析JDK中ServiceLoader的源码》中的主角ServiceLoader就是基于类加载器的功能实现,它也是SPI中的服务类加载的核心类。
说实话,类加载器的"双亲委派模型"和"破坏双亲委派模型"是常见的面试题相关内容,这里可以简单列举两个面试题:
- 1、谈谈对类加载器的"双亲委派模型"的理解。
- 2、为什么要引入线程上下文类加载器(或者是对于问题1有打破这个模型的案例吗)?
希望这篇文章能帮助你理解和解决这两个问题。
参考资料:
- 《深入理解Java虚拟机第二版》
- JavaSE-8源码
(本文完 c-1-d e-20181014)
推荐阅读
-
数据封套分析软件_数据封套分析_数据封套 java - 如何将 NSDate 的状态保存和加载到文件中?
-
小红书大产品部架构 小红书产品概览--经过性能、稳定性、成本等多个维度的详细评估,小红书最终决定选择基于腾讯云星海自研硬件的SA2云服务器作为主力机型使用。结合其秒级的快速扩缩、超强兼容和平滑迁移能力,小红书在抵御上亿次用户访问、保证系统稳定运行的同时,也实现了成本的大幅降低。 星海SA2云服务器是基于腾讯云星海的首款自研服务器。腾讯云星海作为自研硬件品牌,通过创新的高兼容性架构、简洁可靠的自主设计,结合腾讯自身业务以及百万客户上云需求的特点,致力于为云计算时代提供安全、稳定、性能领先的基础架构产品和服务。如今,星海SA2云服务器也正在为越来越多的企业提供低成本、高效率、更安全的弹性计算服务。 以下是与小红书SRE总监陈敖翔的对话实录。 问:请您介绍一下小红书及其主要商业模式? 小红书是一个面向年轻人的生活方式平台,在这里,他们发现了向上、多元的真实世界。小红书日活超过 3500 万,月活跃用户超过 1 亿,日均笔记曝光量达 80 亿。小红书由社交平台和在线购物两大部分组成。与其他线上平台相比,小红书的内容基于真实的口碑分享,播种不止于线上,还为线下实体店赋能。 问:围绕业务发展,小红书的系统架构经历了怎样的变革和演进? 系统架构变化不大,影响最深的是资源开销。过去三年,资源开销大幅增加,同比增长约 10 倍。在此背景下,我们努力进行优化,包括很早就开始使用 K8S 进行资源调度。到 18 年年中,绝大多数服务已经完全实现了容器化。 问:目前小红书系统架构中的计算基础设施建设和布局是怎样的? 我们目前的建设方式可以简单描述为星型结构。腾讯云在上海的一个区是我们的计算中心,承载着我们的核心数据和在线业务。在外围,我们还有两个数据中心进行计算分流,同时承担灾备和线上业务双活的角色。 与其他新兴电子商务互联网公司类似,小红书的大部分计算能力主要用于线下数据分析、模型训练和在线推荐等平台。随着业务的发展,对算力的需求也在加速增长。
-
弄清资源、资源加载器和容器之间的微妙关系 - 在 Java 中,资源会被抽象成 url,通过 url 前面的协议(如 file:、classpath:)来处理不同的操作逻辑,资源是一个接口
-
Java 类加载器的作用 - 简介:类加载器是 Java™ 中一个非常重要的概念。类加载器负责将 Java 类的字节码加载到 Java 虚拟机中。本文首先详细介绍了 Java 类加载器的基本概念,包括代理模型、加载类的具体过程和线程上下文类加载器等。然后介绍了如何开发自己的类加载器,最后介绍了类加载器在 Web 容器和 OSGi™ 中的应用。 类加载器是 Java 语言的一项创新,也是 Java 语言广受欢迎的重要原因之一。它允许将 Java 类动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 开始出现,最初是为了满足 Java Applets 的需求而开发的,Java Applets 需要从远程位置下载 Java 类文件并在浏览器中执行。现在,类加载器已广泛应用于网络容器和 OSGi。一般来说,Java 应用程序的开发人员不需要直接与类加载器交互;Java 虚拟机的默认行为足以应对大多数情况。但是,如果遇到需要与类加载器交互的情况,而您又不太了解类加载器的机制,就很容易花费大量时间调试异常,如 ClassNotFoundException 和 NoClassDefFoundError。本文将详细介绍 Java 的类加载器,帮助读者深入理解 Java 语言中的这一重要概念。下面先介绍一些基本概念。 类加载器的基本概念 顾名思义,类加载器用于将 Java 类加载到 Java 虚拟机中。一般来说,Java 虚拟机以如下方式使用 Java 类:Java 源程序(.java 文件)经 Java 编译器编译后转换为 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码并将其转换为 java.lang 实例。每个实例都用来表示一个 Java 类。通过该实例的 newInstance 方法创建该类的对象。实际情况可能更加复杂,例如,Java 字节代码可能是由工具动态生成或通过网络下载的。 基本上,所有类加载器都是 java.lang.ClassLoader 类的实例。下面将详细介绍这个 Java 类。 java.lang.ClassLoader 类简介 java.lang.ClassLoader 类的基本职责是根据给定类的名称为其查找或生成相应的字节码,然后根据这些字节码定义一个 Java 类,即 java.lang.Class 类的实例。除此之外,ClassLoader 还负责加载 Java 应用程序所需的资源,如图像文件和配置文件。不过,本文只讨论它加载类的功能。为了履行加载类的职责,ClassLoader 提供了许多方法,其中比较重要的方法如表 1 所示。下文将详细介绍这些方法。 表 1.与加载类相关的 ClassLoader 方法
-
通过源代码分析 Java 中的资源加载
-
windows下进程间通信的(13种方法)-摘 要 本文讨论了进程间通信与应用程序间通信的含义及相应的实现技术,并对这些技术的原理、特性等进行了深入的分析和比较。 ---- 关键词 信号 管道 消息队列 共享存储段 信号灯 远程过程调用 Socket套接字 MQSeries 1 引言 ---- 进程间通信的主要目的是实现同一计算机系统内部的相互协作的进程之间的数据共享与信息交换,由于这些进程处于同一软件和硬件环境下,利用操作系统提供的的编程接口,用户可以方便地在程序中实现这种通信;应用程序间通信的主要目的是实现不同计算机系统中的相互协作的应用程序之间的数据共享与信息交换,由于应用程序分别运行在不同计算机系统中,它们之间要通过网络之间的协议才能实现数据共享与信息交换。进程间通信和应用程序间通信及相应的实现技术有许多相同之处,也各有自己的特色。即使是同一类型的通信也有多种的实现方法,以适应不同情况的需要。 ---- 为了充分认识和掌握这两种通信及相应的实现技术,本文将就以下几个方面对这两种通信进行深入的讨论:问题的由来、解决问题的策略和方法、每种方法的工作原理和实现、每种实现方法的特点和适用的范围等。 2 进程间的通信及其实现技术 ---- 用户提交给计算机的任务最终都是通过一个个的进程来完成的。在一组并发进程中的任何两个进程之间,如果都不存在公共变量,则称该组进程为不相交的。在不相交的进程组中,每个进程都独立于其它进程,它的运行环境与顺序程序一样,而且它的运行环境也不为别的进程所改变。运行的结果是确定的,不会发生与时间相关的错误。 ---- 但是,在实际中,并发进程的各个进程之间并不是完全互相独立的,它们之间往往存在着相互制约的关系。进程之间的相互制约关系表现为两种方式: ---- (1) 间接相互制约:共享CPU ---- (2) 直接相互制约:竞争和协作 ---- 竞争——进程对共享资源的竞争。为保证进程互斥地访问共享资源,各进程必须互斥地进入各自的临界段。 ---- 协作——进程之间交换数据。为完成一个共同任务而同时运行的一组进程称为同组进程,它们之间必须交换数据,以达到协作完成任务的目的,交换数据可以通知对方可以做某事或者委托对方做某事。 ---- 共享CPU问题由操作系统的进程调度来实现,进程间的竞争和协作由进程间的通信来完成。进程间的通信一般由操作系统提供编程接口,由程序员在程序中实现。UNIX在这个方面可以说最具特色,它提供了一整套进程间的数据共享与信息交换的处理方法——进程通信机制(IPC)。因此,我们就以UNIX为例来分析进程间通信的各种实现技术。 ---- 在UNIX中,文件(File)、信号(Signal)、无名管道(Unnamed Pipes)、有名管道(FIFOs)是传统IPC功能;新的IPC功能包括消息队列(Message queues)、共享存储段(Shared memory segment)和信号灯(Semapores)。 ---- (1) 信号 ---- 信号机制是UNIX为进程中断处理而设置的。它只是一组预定义的值,因此不能用于信息交换,仅用于进程中断控制。例如在发生浮点错、非法内存访问、执行无效指令、某些按键(如ctrl-c、del等)等都会产生一个信号,操作系统就会调用有关的系统调用或用户定义的处理过程来处理。 ---- 信号处理的系统调用是signal,调用形式是: ---- signal(signalno,action) ---- 其中,signalno是规定信号编号的值,action指明当特定的信号发生时所执行的动作。 ---- (2) 无名管道和有名管道 ---- 无名管道实际上是内存中的一个临时存储区,它由系统安全控制,并且独立于创建它的进程的内存区。管道对数据采用先进先出方式管理,并严格按顺序操作,例如不能对管道进行搜索,管道中的信息只能读一次。 ---- 无名管道只能用于两个相互协作的进程之间的通信,并且访问无名管道的进程必须有共同的祖先。 ---- 系统提供了许多标准管道库函数,如: pipe——打开一个可以读写的管道; close——关闭相应的管道; read——从管道中读取字符; write——向管道中写入字符; ---- 有名管道的操作和无名管道类似,不同的地方在于使用有名管道的进程不需要具有共同的祖先,其它进程,只要知道该管道的名字,就可以访问它。管道非常适合进程之间快速交换信息。 ---- (3) 消息队列(MQ) ---- 消息队列是内存中独立于生成它的进程的一段存储区,一旦创建消息队列,任何进程,只要具有正确的的访问权限,都可以访问消息队列,消息队列非常适合于在进程间交换短信息。 ---- 消息队列的每条消息由类型编号来分类,这样接收进程可以选择读取特定的消息类型——这一点与管道不同。消息队列在创建后将一直存在,直到使用msgctl系统调用或iqcrm -q命令删除它为止。 ---- 系统提供了许多有关创建、使用和管理消息队列的系统调用,如: ---- int msgget(key,flag)——创建一个具有flag权限的MQ及其相应的结构,并返回一个唯一的正整数msqid(MQ的标识符); ---- int msgsnd(msqid,msgp,msgsz,msgtyp,flag)——向队列中发送信息; ---- int msgrcv(msqid,cmd,buf)——从队列中接收信息; ---- int msgctl(msqid,cmd,buf)——对MQ的控制操作; ---- (4) 共享存储段(SM) ---- 共享存储段是主存的一部分,它由一个或多个独立的进程共享。各进程的数据段与共享存储段相关联,对每个进程来说,共享存储段有不同的虚拟地址。系统提供的有关SM的系统调用有: ---- int shmget(key,size,flag)——创建大小为size的SM段,其相应的数据结构名为key,并返回共享内存区的标识符shmid; ---- char shmat(shmid,address,flag)——将当前进程数据段的地址赋给shmget所返回的名为shmid的SM段; ---- int shmdr(address)——从进程地址空间删除SM段; ---- int shmctl (shmid,cmd,buf)——对SM的控制操作; ---- SM的大小只受主存限制,SM段的访问及进程间的信息交换可以通过同步读写来完成。同步通常由信号灯来实现。SM非常适合进程之间大量数据的共享。 ---- (5) 信号灯 ---- 在UNIX中,信号灯是一组进程共享的数据结构,当几个进程竞争同一资源时(文件、共享内存或消息队列等),它们的操作便由信号灯来同步,以防止互相干扰。 ---- 信号灯保证了某一时刻只有一个进程访问某一临界资源,所有请求该资源的其它进程都将被挂起,一旦该资源得到释放,系统才允许其它进程访问该资源。信号灯通常配对使用,以便实现资源的加锁和解锁。 ---- 进程间通信的实现技术的特点是:操作系统提供实现机制和编程接口,由用户在程序中实现,保证进程间可以进行快速的信息交换和大量数据的共享。但是,上述方式主要适合在同一台计算机系统内部的进程之间的通信。 3 应用程序间的通信及其实现技术 ---- 同进程之间的相互制约一样,不同的应用程序之间也存在竞争和协作的关系。UNIX操作系统也提供一些可用于应用程序之间实现数据共享与信息交换的编程接口,程序员可以通过自己编程来实现。如远程过程调用和基于TCP/IP协议的套接字(Socket)编程。但是,相对普通程序员来说,它们涉及的技术比较深,编程也比较复杂,实现起来困难较大。 ---- 于是,一种新的技术应运而生——通过将有关通信的细节完全掩盖在某个独立软件内部,即底层的通讯工作和相应的维护管理工作由该软件内部来实现,用户只需要将通信任务提交给该软件去完成,而不必理会它的具体工作过程——这就是所谓的中间件技术。 ---- 我们在这里分别讨论这三种常用的应用程序间通信的实现技术——远程过程调用、会话编程技术和MQSeries消息队列技术。其中远程过程调用和会话编程属于比较低级的方式,程序员参与的程度较深,而MQSeries消息队列则属于比较高级的方式,即中间件方式,程序员参与的程度较浅。 ---- 4.1 远程过程调用(RPC)
-
玩转Java底层:JMX详解 - jconsole与自定义MBean监控工具的实际应用与区别" 在日常JVM调优中,我们熟知的jconsole工具通过JMX包装的bean以图形化形式展示管理数据,而像jstat和jmap这类内建监控工具则由JVM直接支持。本文将以jconsole为例,深入讲解其实质——基于JMX的MBean功能,包括可视化界面上的bean属性查看和操作调用。 MBeans在jconsole中的体现是那些可观察的组件属性和方法,如上图所示,通过名为"Verbose"的属性能看到其值为false,同时还能直接操作该bean的方法,例如"closeJerryMBean"。 尽管jconsole给我们提供了直观的可视化界面,但请注意,这里的MBean并非固定不变,开发者可根据JMX提供的接口将自己的自定义bean展示到jconsole。以下步骤展示了如何创建并注册一个名为"StudyJavaMBean"的自定义MBean: 1. 首先定义接口`StudyJavaMBean`,接口需遵循MBean规范,即后缀为"MBean"且包含getter方法代表属性,如`getApplicationName`,和无返回值的setter方法代表操作,如`closeJerryMBean`。 ```java public interface StudyJavaMBean { String getApplicationName(); void closeJerryMBean(); } ``` 2. 编写接口的实现类`StudyJavaMBeanImpl`,实现接口中的方法: ```java public class StudyJavaMBeanImpl implements StudyJavaMBean { @Override public String getApplicationName() { return "每天学Java"; } @Override public void closeJerryMBean() { System.out.println("关闭Jerry应用"); } } ``` 3. 在代码中注册自定义MBean,涉及的关键步骤包括: - 获取平台MBeanServer - 定义ObjectName,指定唯一的MBean标识符 - 注册MBean到服务器 - 启动RMI连接器服务,以便jconsole能够访问 ```java public void registerMBean() throws Exception { // ... 具体实现省略 ... } ``` 实际运行注册后的MBean,您将在jconsole中发现并查看自定义bean的属性和调用相关方法。然而,这种方式相较于传统的属性/日志查看和HTTP接口,实用性相对有限,可能存在潜在的安全风险。但不可否认的是,JMX及其MBean机制对于获取操作系统信息、内存状态等关键性能指标仍然具有重要价值。例如: 1. **获取操作系统信息**:通过JMX MBean,可以直接获取到诸如CPU使用率、操作系统版本等系统级信息,这对于资源管理和优化工作具有显著帮助。
-
【Netty】「萌新入门」(七)ByteBuf 的性能优化-堆内存的分配和释放都是由 Java 虚拟机自动管理的,这意味着它们可以快速地被分配和释放,但是也会产生一些开销。 直接内存需要手动分配和释放,因为它由操作系统管理,这使得分配和释放的速度更快,但是也需要更多的系统资源。 另外,直接内存可以映射到本地文件中,这对于需要频繁读写文件的应用程序非常有用。 此外,直接内存还可以避免在使用 NIO 进行网络传输时发生数据拷贝的情况。在使用传统的 I/O 时,数据必须先从文件或网络中读取到堆内存中,然后再从堆内存中复制到直接缓冲区中,最后再通过 SocketChannel 发送到网络中。而使用直接缓冲区时,数据可以直接从文件或网络中读取到直接缓冲区中,并且可以直接从直接缓冲区中发送到网络中,避免了不必要的数据拷贝和内存分配。 通过 ByteBufAllocator.DEFAULT.directBuffer 方法来创建基于直接内存的 ByteBuf: ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); 通过 ByteBufAllocator.DEFAULT.heapBuffer 方法来创建基于堆内存的 ByteBuf: ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); 注意: 直接内存是一种特殊的内存分配方式,可以通过在堆外申请内存来避免 JVM 堆内存的限制,从而提高读写性能和降低 GC 压力。但是,直接内存的创建和销毁代价昂贵,因此需要慎重使用。 此外,由于直接内存不受 JVM 垃圾回收的管理,我们需要主动释放这部分内存,否则会造成内存泄漏。通常情况下,可以使用 ByteBuffer.clear 方法来释放直接内存中的数据,或者使用 ByteBuffer.cleaner 方法来手动释放直接内存空间。 测试代码: public static void testCreateByteBuf { ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16); System.out.println(buf.getClass); ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); System.out.println(heapBuf.getClass); ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); System.out.println(directBuf.getClass); } 运行结果: class io.netty.buffer.PooledUnsafeDirectByteBuf class io.netty.buffer.PooledUnsafeHeapByteBuf class io.netty.buffer.PooledUnsafeDirectByteBuf 池化技术 在 Netty 中,池化技术指的是通过对象池来重用已经创建的对象,从而避免了频繁地创建和销毁对象,这种技术可以提高系统的性能和可伸缩性。 通过设置 VM options,来决定池化功能是否开启: -Dio.netty.allocator.type={unpooled|pooled} 在 Netty 4.1 版本以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现; 这里我们使用非池化功能进行测试,依旧使用的是上面的测试代码 testCreateByteBuf,运行结果如下所示: class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf 可以看到,ByteBuf 类由 PooledUnsafeDirectByteBuf 变成了 UnpooledUnsafeDirectByteBuf; 在没有池化的情况下,每次使用都需要创建新的 ByteBuf 实例,这个操作会涉及到内存的分配和初始化,如果是直接内存则代价更为昂贵,而且频繁的内存分配也可能导致内存碎片问题,增加 GC 压力。 使用池化技术可以避免频繁内存分配带来的开销,并且重用池中的 ByteBuf 实例,减少了内存占用和内存碎片问题。另外,池化技术还可以采用类似 jemalloc 的内存分配算法,进一步提升分配效率。 在高并发环境下,池化技术的优点更加明显,因为内存的分配和释放都是比较耗时的操作,频繁的内存分配和释放会导致系统性能下降,甚至可能出现内存溢出的风险。使用池化技术可以将内存分配和释放的操作集中到预先分配的池中,从而有效地降低系统的内存开销和风险。 内存释放 当在 Netty 中使用 ByteBuf 来处理数据时,需要特别注意内存回收问题。 Netty 提供了不同类型的 ByteBuf 实现,包括堆内存(JVM 内存)实现 UnpooledHeapByteBuf 和堆外内存(直接内存)实现 UnpooledDirectByteBuf,以及池化技术实现的 PooledByteBuf 及其子类。 UnpooledHeapByteBuf:通过 Java 的垃圾回收机制来自动回收内存; UnpooledDirectByteBuf:由于 JVM 的垃圾回收机制无法管理这些内存,因此需要手动调用 release 方法来释放内存; PooledByteBuf:使用了池化机制,需要更复杂的规则来回收内存; 由于池化技术的特殊性质,释放 PooledByteBuf 对象所使用的内存并不是立即被回收的,而是被放入一个内存池中,待下次分配内存时再次使用。因此,释放 PooledByteBuf 对象的内存可能会延迟到后续的某个时间点。为了避免内存泄漏和占用过多内存,我们需要根据实际情况来设置池化技术的相关参数,以便及时回收内存; Netty 采用了引用计数法来控制 ByteBuf 对象的内存回收,在博文 「源码解析」ByteBuf 的引用计数机制 中将会通过解读源码的形式对 ByteBuf 的引用计数法进行深入理解; 每个 ByteBuf 对象被创建时,都会初始化为1,表示该对象的初始计数为1。 在使用 ByteBuf 对象过程中,如果当前 handler 已经使用完该对象,需要通过调用 release 方法将计数减1,当计数为0时,底层内存会被回收,该对象也就被销毁了。此时即使 ByteBuf 对象还在,其各个方法均无法正常使用。 但是,如果当前 handler 还需要继续使用该对象,可以通过调用 retain 方法将计数加1,这样即使其他 handler 已经调用了 release 方法,该对象的内存仍然不会被回收。这种机制可以有效地避免了内存泄漏和意外访问已经释放的内存的情况。 一般来说,应该尽可能地保证 retain 和 release 方法成对出现,以确保计数正确。