在Java虚拟机(JVM)进程之间共享已加载类的原理并不新鲜。 例如,Sun的CDS功能将系统类写入一个内存映射到JVM的只读文件中。IBMz /OS®1.4.2 JVM中的Shiraz功能使用主JVM填充了一个类缓存。然后由从属JVM共享。
通过允许将所有系统和应用程序类存储在共享内存中的持久性动态类高速缓存中,IBM 5.0 JVM的实现使这一概念更进一步。 IBM实现JVM的所有平台都支持此共享类功能。 该功能甚至还支持与运行时字节码修改集成,本文稍后将对此进行讨论。
共享类功能是从头开始设计的,您可以打开它就忘记了,但是它提供了非常强大的作用域,可以减少虚拟内存占用并缩短JVM启动时间。 因此,它最适合于多个JVM运行相似代码或定期重新启动JVM的环境。
除了JVM及其类加载器中的运行时类共享支持外,还提供了一个公共Helper API,用于将类共享支持集成到自定义类加载器中,本文将对此进行详细讨论。
这个怎么运作
让我们从探讨共享类功能的运行方式的技术细节开始。
启用课程共享
通过将-Xshareclasses[:name=<cachename>]
到现有Java命令行来启用类共享。 JVM启动时,它将查找具有给定名称的类高速缓存(如果未提供名称,则选择默认名称),并根据需要连接到现有高速缓存或创建新的高速缓存。
您可以使用参数-Xscmx<size>[k|m|g]
指定缓存大小; 仅当JVM创建新的缓存时,此参数才适用。 如果省略此选项,则选择一个平台相关的默认值(通常为16MB)。 请注意,有些操作系统设置可能会限制可以分配的共享内存量,例如,Linux上的SHMMAX
通常设置为大约20MB。 这些设置的详细信息可以在适当的用户指南的“共享类”部分中找到( 有关链接,请参见“ 相关主题”部分)。
类缓存
类高速缓存是固定大小的共享内存的一个区域,该区域在任何使用它的JVM的生存期内都将持续存在。 根据操作系统设置和限制,系统上可以存在任意数量的共享类缓存。 但是,单个JVM在其生存期内只能连接到一个缓存。
没有JVM拥有缓存,也没有主/从JVM概念。 相反,任何数量的JVM可以同时读取和写入高速缓存。 当使用JVM实用程序显式销毁高速缓存时,或者在操作系统重新启动时(高速缓存无法在操作系统重新启动后继续存在),高速缓存将被删除。 高速缓存无法增长,并且在高速缓存已满时,JVM仍可以从中加载类,但不能向其中添加任何类。 标题为“ 共享类实用程序 ”的部分讨论了许多用于管理活动高速缓存的JVM实用程序 。
如何缓存类?
当JVM加载一个类时,它首先在高速缓存中查找以查看其所需的类是否已经存在。 如果是,它将从缓存中加载类。 否则,它将从文件系统加载该类,并将其作为defineClass()
调用的一部分写入缓存。 因此,非共享的JVM具有以下类加载器查找顺序:
- 类加载器缓存
- 父母
- 文件系统
相反,与类共享一起运行的JVM使用以下顺序:
- 类加载器缓存
- 父母
- 共享缓存
- 文件系统
使用公共Helper API从缓存中读取类或将类写入缓存,该API已集成到java.net.URLClassLoader
的IBM实现中。 因此,任何扩展java.net.URLClassLoader
的类加载器都免费获得类共享支持。
该类的哪些部分被缓存?
在JVM的IBM实现中,Java类分为两部分:只读部分,称为ROMClass
,其中包含所有类的不可变数据,而RAMClass
包含不可变的数据,例如静态类变量。 RAMClass
指向其ROMClass
数据,但两者是完全分开的,这意味着在JVM之间RAMClasses
在同一JVM中的RAMClasses
之间共享ROMClass
是非常安全的。
在非共享情况下,当JVM加载一个类时,它会分别创建ROMClass
和RAMClass
并将它们都存储在本地进程内存中。 在共享的情况下,如果JVM在类高速缓存中找到ROMClass
,则仅需要在其本地内存中创建RAMClass
; RAMClass
,它会在JVM中找到。 然后, RAMClass
引用共享的ROMClass
。
由于大多数类数据存储在ROMClass
,因此可以节省虚拟内存。 (“ 虚拟内存占用量 ”部分将对此进行详细讨论。)使用填充的缓存,JVM的启动时间也得到了显着改善,因为定义每个缓存的类的一些工作已经完成,并且这些类已经从内存中加载了,而不是而不是来自文件系统。 填充新缓存的启动时间开销(我将在本文稍后讨论)并不重要,因为每个类仅需要在定义时重新定位到缓存中。
如果类在文件系统上更改,会发生什么?
因为缓存可以无限期地持久,所以可能会发生使缓存中的类无效的文件系统更新。 因此,缓存代码的责任是确保,如果类加载器请求共享类,则返回的类应始终与将从文件系统加载的类完全相同。 这在加载类时透明地发生,因此,在知道始终加载正确的类的情况下,用户可以在共享类高速缓存的生存期内修改和更新任意数量的类。
JVM通过将时间戳记值存储到高速缓存中并在每次类加载时将高速缓存的值与实际值进行比较来检测文件系统更新。 如果检测到JAR文件已更新,则不知道已更改了哪些类,因此从该JAR加载到缓存中的所有类都将立即标记为过期,因此无法从缓存中加载。 当从文件系统中加载该JAR中的类并将其重新添加到高速缓存时,仅完整地添加实际上已更改的那些类; 那些没有改变的东西实际上已经过时了。
无法从高速缓存中清除类,但是JVM会尝试最有效地利用其拥有的空间。 例如,即使从许多不同的位置加载同一类,也绝不会添加两次。 因此,如果通过三个不同的JVM从/A.jar、/B.jar和/C.jar加载相同的类C3
,则该类数据仅添加一次,但是有三个元数据段描述了三个位置从中加载。
共享类实用程序
有许多实用程序可用于管理活动缓存,它们都是-Xshareclasses
子选项。 (您可以通过键入java -Xshareclasses:help
来获得-Xshareclasses
的所有有效子选项的完整列表。)
请注意,除了expire
之外,没有一个实用程序expire
真正启动JVM,因此它们会执行所需的操作,然后在不运行类的情况下退出。 还要注意,每个启动程序都会由Java启动程序打印消息“ Could not create the Java virtual machine
因为未启动JVM。 这不是错误。
为了演示这些选项的用法,让我们来看一些示例。 首先,让我们通过运行具有不同缓存名称的HelloWorld
类来创建两个缓存,如清单1所示:
清单1.创建两个缓存
C:\j9vmwi3223\sdk\jre\bin>java -cp . -Xshareclasses:name=cache1 Hello
Hello
C:\j9vmwi3223\sdk\jre\bin>java -cp . -Xshareclasses:name=cache2 Hello
Hello
运行listAllCaches
子选项会列出系统上的所有缓存,并确定它们是否正在使用,如清单2所示:
清单2.列出所有缓存
C:\j9vmwi3223\sdk\jre\bin>java -Xshareclasses:listAllCaches
Shared Cache Last detach time
cache1 Sat Apr 15 18:47:46 2006
cache2 Sat Apr 15 18:51:15 2006
Could not create the Java virtual machine.
运行printStats
选项将在指定的高速缓存上打印摘要统计信息,如清单3所示。有关此处显示的所有字段的含义的详细说明,请查阅用户指南(请参阅参考资料中的链接)。
清单3.缓存的摘要统计
C:\j9vmwi3223\sdk\jre\bin>java -Xshareclasses:name=cache1,printStats
Current statistics for cache "cache1":
base address = 0x41D10058
end address = 0x42D0FFF8
allocation pointer = 0x41E3B948
cache size = 16777128
free bytes = 15536080
ROMClass bytes = 1226992
Metadata bytes = 14056
Metadata % used = 1%
# ROMClasses = 313
# Classpaths = 2
# URLs = 0
# Tokens = 0
# Stale classes = 0
% Stale classes = 0%
Cache is 7% full
Could not create the Java virtual machine.
在命名的高速缓存上运行printAllStats
选项会列出高速缓存的全部内容,以及printStats
摘要信息。 列出了存储在缓存中的每个类以及上下文数据,例如类路径数据。 在清单4中,您可以看到列出了JVM的引导程序类路径,然后是一些类以及它们从何处加载的详细信息:
清单4.列出缓存的全部内容
C:\j9vmwi3223\sdk\jre\bin>java -Xshareclasses:name=cache1,printAllStats
Current statistics for cache "cache1":
1: 0x42D0FAB0 CLASSPATH
C:\j9vmwi3223\sdk\jre\lib\vm.jar
C:\j9vmwi3223\sdk\jre\lib\core.jar
C:\j9vmwi3223\sdk\jre\lib\charsets.jar
C:\j9vmwi3223\sdk\jre\lib\graphics.jar
C:\j9vmwi3223\sdk\jre\lib\security.jar
C:\j9vmwi3223\sdk\jre\lib\ibmpkcs.jar
C:\j9vmwi3223\sdk\jre\lib\ibmorb.jar
C:\j9vmwi3223\sdk\jre\lib\ibmcfw.jar
C:\j9vmwi3223\sdk\jre\lib\ibmorbapi.jar
C:\j9vmwi3223\sdk\jre\lib\ibmjcefw.jar
C:\j9vmwi3223\sdk\jre\lib\ibmjgssprovider.jar
C:\j9vmwi3223\sdk\jre\lib\ibmjsseprovider2.jar
C:\j9vmwi3223\sdk\jre\lib\ibmjaaslm.jar
C:\j9vmwi3223\sdk\jre\lib\ibmjaasactivelm.jar
C:\j9vmwi3223\sdk\jre\lib\ibmcertpathprovider.jar
C:\j9vmwi3223\sdk\jre\lib\server.jar
C:\j9vmwi3223\sdk\jre\lib\xml.jar
1: 0x42D0FA78 ROMCLASS: java/lang/Object at 0x41D10058.
Index 0 in classpath 0x42D0FAB0
1: 0x42D0FA50 ROMCLASS: java/lang/J9VMInternals at 0x41D106E0.
Index 0 in classpath 0x42D0FAB0
1: 0x42D0FA28 ROMCLASS: java/lang/Class at 0x41D120A8.
Index 0 in classpath 0x42D0FAB0
...
使用destroy
选项销毁了命名的高速缓存,如清单5所示。同样, destroyAll
销毁所有未使用且用户有权销毁的高速缓存。
清单5.销毁缓存
C:\j9vmwi3223\sdk\jre\bin>java -Xshareclasses:name=cache1,destroy
JVMSHRC010I Shared Cache "cache1" is destroyed
Could not create the Java virtual machine.
C:\j9vmwi3223\sdk\jre\bin>java -Xshareclasses:listAllCaches
Shared Cache Last detach time
cache2 Sat Apr 15 18:51:15 2006
Could not create the Java virtual machine.
清单6中所示的expire
选项是一个内部管理选项,您可以将其添加到命令行以自动破坏在指定的分钟数内未附加任何内容的缓存。 这是不会导致JVM退出的唯一实用程序。 清单6查找了一周未使用的缓存,并在启动VM之前将其销毁:
清单6.销毁一周内未使用的缓存
C:\j9vmwi3223\sdk\jre\bin>java -cp . -Xshareclasses:expire=10000,name=cache1 Hello
Hello
详细选项
详细选项提供有关类共享正在做什么的有用反馈。 它们都是-Xshareclasses
子选项。 本节提供了一些有关如何使用详细输出的示例。
如清单7所示, verbose
选项提供了有关JVM启动和关闭的简要状态信息:
清单7.获取JVM状态信息
C:\j9vmwi3223\sdk\jre\bin>java -cp . -Xshareclasses:name=cache1,verbose Hello
[-Xshareclasses verbose output enabled]
JVMSHRC158I Successfully created shared class cache "cache1"
JVMSHRC166I Attached to cache "cache1", size=16777176 bytes
Hello
JVMSHRC168I Total shared class bytes read=0. Total bytes stored=1176392
verboseIO
选项为向缓存的每个类加载请求打印一条状态行。 要了解详细的verboseIO
输出,您应该了解类加载器的层次结构,因为对于任何由非引导类加载器加载的类,可以清楚地看到这一点。 每个类加载器都必须将层次结构委托给引导加载器以查找类。 在输出中,为每个类加载器分配了唯一的ID,但引导加载程序始终为0。
请注意, verboseIO
有时会显示从磁盘加载并存储在缓存中的类,即使它们已经被缓存是正常的。 例如,始终从磁盘加载并存储从应用程序类路径上的每个JAR进行的第一类加载,无论它是否存在于缓存中。
在清单8中 ,第一部分展示了缓存的填充,第二部分展示了读取缓存的类:
清单9中所示的verboseHelper
子选项是一个高级选项,它提供了Helper API的状态输出。 它旨在帮助使用Helper API的开发人员了解其驱动方式。 有关此输出的更多详细信息,请参见《 JVM诊断指南》( 有关链接,请参阅参考资料 )。
运行时字节码修改
运行时字节码修改正成为将行为检测到Java类中的流行手段。 它可以使用JVM工具接口(JVMTI)钩(参见来执行相关信息中的链接); 或者,可以在定义类之前用类加载器替换类字节。 这给类共享带来了额外的挑战,因为一个JVM可能会缓存检测到的字节码,而该字节码不应由共享同一缓存的另一个JVM加载。
但是,由于IBM Shared Classes实现的动态性质,使用不同修改类型的多个JVM可以安全地共享同一高速缓存。 确实,如果字节码修改很昂贵,则缓存修改后的类会带来更大的好处,因为该转换只需要执行一次。 唯一的条件是字节码修改应是确定性的和可预测的。 一旦修改并缓存了一个类,就无法再对其进行更改。
可以使用-Xshareclasses
modified=<context>
子选项来共享修改后的字节码。 上下文是用户定义的名称,该名称在缓存中创建一个分区,该JVM加载的所有类都存储在该分区中。 使用该特定修改的所有JVM应该使用相同的修改上下文名称,并且它们都从同一高速缓存分区加载类。 任何使用相同缓存但未modified
子选项的JVM都会正常查找并存储普通类。
潜在的陷阱
如果JVM与已注册用于修改类字节的JVMTI代理一起运行,并且未使用modified
子选项,则与其他原始JVM或使用其他代理的JVM进行类共享仍然可以安全地进行管理,尽管这样做会降低性能成本,因为额外的开销检查。 因此,使用modified
子选项总是更有效。
请注意,这仅是可能的,因为JVM知道由于JVMTI代理的存在,即将进行字节码修改。 因此,如果自定义类加载器修改类定义的类,而无需使用JVMTI,并且不使用前字节modified
子选项,被定义的类被假定为香草,并且可以通过其他JVM被不正确地加载。
有关共享修改后的字节码的更多详细信息,请参阅《 JVM诊断指南》(请参阅参考资料 )。
使用助手API
IBM提供了Shared Classes Helper API,以便开发人员可以将类共享支持集成到定制的类加载器中。 只有不扩展java.net.URLClassLoader
类加载器才需要这样做,因为这些类加载器会自动继承类共享支持。
关于Helper API的全面教程不在本文讨论范围之内,但是这里是一般概述。 如果您需要更详细的描述,可以在下载部分中找到完整的Javadoc,并且诊断指南(请参阅参考资料 )也提供了更多信息。
助手API:摘要
所有Helper API类都在com.ibm.oti.shared
包中,并包含在jre / lib目录的vm.jar中。 每个希望共享类的类加载器都必须从SharedClassHelperFactory
获取一个SharedClassHelper
对象。 一旦创建了SharedClassHelper
,它就属于请求它的类加载器,并且只能存储该类加载器定义的类。 SharedClassHelper
为类加载器提供了一个简单的API,用于在JVM连接到的类缓存中查找和存储类。 如果类加载器是垃圾收集的,则它的SharedClassHelper
也将被垃圾收集。
使用SharedClassHelperFactory
SharedClassHelperFactory
是使用静态方法com.ibm.oti.shared.Shared.getSharedClassHelperFactory()
获得的单例,如果在JVM中启用了类共享,则它将返回工厂。 否则,返回null
。
使用SharedClassHelpers
工厂可以返回三种不同类型的SharedClassHelper
,每种类型都设计用于不同类型的类加载器:
-
SharedClassURLClasspathHelper
:此帮助程序设计用于具有URL类路径概念的类加载器。 使用URL classpath数组在缓存中存储并找到类。 必须在文件系统上访问类路径中的URL资源,才能缓存类。 该帮助程序还对如何在其生存期内修改类路径进行了一些限制。 -
SharedClassURLHelper
:此帮助程序设计用于没有类路径概念并且可以从任何URL加载类的类加载器。 给定的URL资源必须在文件系统上可访问,才能缓存这些类。 -
SharedClassTokenHelper
:此帮助程序有效地将共享类高速缓存转换为简单的哈希表-根据对高速缓存无意义的字符串键令牌存储类。 这是唯一不提供动态更新功能的帮助程序,因为存储的类没有与之关联的文件系统上下文。
每个SharedClassHelper
有两个基本方法,这些参数在助手类型之间略有不同:
- 在类加载器向其父类请求类(如果存在)之后,应调用
byte[] findSharedClass(String classname...)
)。 如果findSharedClass()
不返回null
,则类加载器应在返回的字节数组上调用defineClass()
。 请注意,此函数为defineClass()
返回一个特殊的cookie,而不是实际的类字节,因此无法检测这些字节。 - 定义类后,应立即调用
boolean storeSharedClass(Class clazz...)
。 如果该类已成功存储,则该方法返回true
否则返回false
。
其他注意事项
在与应用程序部署类共享时,需要考虑诸如安全性和缓存调整之类的因素。 这些注意事项在此处简要概述
安全
默认情况下,类缓存是使用用户级安全性创建的,因此只有创建缓存的用户才能访问它。 因此,每个用户的默认缓存名称都不同,因此可以避免冲突。 在UNIX上,有一个子选项可以指定groupAccess
,它可以访问创建缓存的用户的主要组中的所有用户。 但是,无论使用何种访问级别,缓存只能由创建它的用户或root用户破坏。
除此之外,如果安装了SecurityManager
,则只有明确授予正确的权限,类加载器才能共享类。 有关设置这些权限的更多详细信息,请参阅用户指南(请参阅参考资料 )。
垃圾收集和即时编译
在启用类共享的情况下运行对类垃圾回收(GC)无效。 就像在非共享情况下一样,类和类加载器仍然可以被垃圾回收。 此外,使用类共享时,对GC模式或配置也没有任何限制。
无法将即时(JIT)编译后的代码缓存在类缓存中,因此在启用类共享的情况下运行时,JIT的行为不会发生变化。
缓存大小限制
当前最大理论高速缓存大小为2GB。 缓存大小受以下因素限制:
- 可用磁盘空间(仅适用于Microsoft Windows)。 在名为javasharedresources的目录中创建一个内存映射文件,以存储类数据。 此目录在用户的%APPDATA%目录中创建。 每次重新启动Windows时,共享的缓存文件都会被删除。
- 可用的系统内存(仅UNIX)。 在UNIX上,高速缓存存在于共享内存中,并且JVM将配置文件写入/ tmp / javasharedresouces,以允许所有JVM按名称查找共享内存区域。
- 可用的虚拟地址空间。 由于进程的虚拟地址空间是在共享类缓存和Java堆之间共享的,因此增加Java堆的最大大小会减小您可以创建的共享类缓存的大小。
一个例子
为了实际演示类共享的好处,本节提供了一个简单的图形演示。 源代码和二进制文件可从“ 下载”部分获得。
该演示应用程序查找jre \ lib目录并打开每个JAR,并在找到的每个类上调用class.forName()
。 这导致将大约12,000个类加载到JVM中。 该演示报告了JVM加载类所需的时间。 显然,这是一个有点人为的示例,因为该测试仅进行类加载,但是有效地证明了类共享的好处。 让我们运行该应用程序并查看结果。
类加载性能
- 从下载部分下载shcdemo.jar。
- 使用清单10中的命令,运行几次没有类共享的测试来预热系统磁盘缓存:
清单10.预热磁盘缓存
C:\j9vmwi3223\sdk\jre\bin>java -cp C:\shcdemo.jar ClassLoadStress
当出现图1中的窗口时,按按钮。 该应用程序将加载类。图1.按下按钮
加载了类之后,应用程序将报告它已加载了多少以及花费了多长时间,如图2所示:图2.结果已输入!
您会注意到,每次运行该应用程序时,它的运行速度可能都会有所提高。 这是因为操作系统优化。 - 现在运行启用了类共享的演示,如清单11所示 。 将创建一个新的缓存,因此此运行向您显示了填充新缓存所需的时间。 您应指定大约50MB的缓存大小,以确保所有类都有足够的空间。 清单11显示了命令行和一些示例输出。
如图3所示,由于该演示正在填充共享类缓存,因此该运行应该比之前的运行花费更长的时间。 您还可以选择使用printStats
,如清单12所示,查看共享类缓存中存储的类数:图3.冷缓存结果
清单12.查看缓存类的数量
C:\j9vmwi3223\sdk\jre\bin>java -Xshareclasses:name=demo,printStats Current statistics for cache "demo": base address = 0x41D10058 end address = 0x44F0FFF8 allocation pointer = 0x44884030 cache size = 52428712 free bytes = 6373120 ROMClass bytes = 45563864 Metadata bytes = 491728 Metadata % used = 1% # ROMClasses = 12212 # Classpaths = 3 # URLs = 0 # Tokens = 0 # Stale classes = 0 % Stale classes = 0% Cache is 87% full Could not create the Java virtual machine.
- 现在,使用完全相同的Java命令行再次启动演示。 这次,它应该从共享类缓存中读取类,如清单13所示 。
您可以清楚地看到类加载时间的显着改善。 同样,由于操作系统的优化,每次运行演示时,您应该会看到性能略有提高。 此特定测试是在运行Windows XP的单处理器,1.6 GHz x86兼容笔记本电脑上完成的:图4.热缓存结果
您可以尝试多种变体。 例如,您可以使用javaw
命令并启动多个演示,然后将它们一起触发所有加载类以查看并发性能。
在现实情况下,使用类共享可获得的总体JVM启动时间收益取决于应用程序加载的类数:HelloWorld程序不会带来太多好处,而大型Web服务器肯定会带来很多好处。 但是,该示例有望证明,尝试类共享非常简单,因此您可以轻松地测试其好处。
虚拟内存占用量
当在多个JVM中运行示例程序时,很容易看到虚拟内存的节省。
下面是使用与前面的示例相同的计算机获得的两个任务管理器快照。 在图5中,该演示的五个实例已运行完毕而没有类共享。 在图6中,使用与之前相同的命令行运行了五个实例,并启用了类共享:
图5.五个没有类共享的演示
图6.五个启用了类共享的演示
您可以清楚地看到启用了类共享的提交费用要低得多。 Windows似乎通过将VM大小加在一起来计算其提交费用。 因为共享的缓存类数据的总数约为45MB,所以您可以看到每个JVM的内存使用量大约是VM大小加上缓存类数据的数量。
这两个示例均以大约295MB的提交费用开始。 这意味着第一个示例使用了422MB,而第二个示例使用了244 MB,节省了178 MB。
结论
Java平台5.0版的IBM实现中的新Shared Classes功能提供了一种简单而灵活的方式来减少虚拟内存占用并缩短JVM启动时间。 在本文中,您已经了解了如何启用该功能,如何使用缓存实用程序以及如何对收益进行量化。
该系列的下一篇文章将介绍Java平台的IBM实现中可用的一些新的调试,监视和概要分析工具。 它还将展示如何使用它们来快速分析和调试Java应用程序。
翻译自: https://www.ibm.com/developerworks/java/library/j-ibmjava4/index.html