JDK 8升级高版本JDK指南

从Java SE 下载下载并安装新的 JDK 版本。

尝试在最新的 JDK 版本 上运行您的应用程序。大多数代码和库无需任何更改即可在 新版本上运行,但可能有一些库需要升级。

当您运行应用程序时,请查看来自 JVM 的有关过时 VM 选项的警告。如果 VM 无法启动,则查找Removed GC Options。

如果您的应用程序成功启动,请仔细查看您的测试并确保其行为与您一直使用的 JDK 版本相同。例如,一些早期采用者注意到他们的日期和货币格式不同。请参阅默认使用 CLDR 区域设置数据。

要使您的代码在最新的 JDK 版本上运行,请了解每个 JDK 版本中的新功能和更改。

  • 有关 JDK 17 中的新功能和更改的详细信息,请参阅JDK 17 中的新增功能 – 新功能和增强功能。
  • 有关 JDK 16 中的新功能和更改的详细信息,请参阅JDK 16 中的新增功能 – 新功能和增强功能。
  • 有关 JDK 15 中的新功能和更改的详细信息,请参阅JDK 15 中的新增功能 – 新功能和增强功能。
  • 有关 JDK 14 中的新功能和更改的详细信息,请参阅JDK 14 中的新增功能 – 新功能和增强功能。
  • 有关 JDK 13 中的新功能和更改的详细信息,请参阅JDK 13 中的新增功能 – 新功能和增强功能。
  • 有关 JDK 12 中的新功能和更改的详细信息,请参阅JDK 12 中的新增功能 – 新功能和增强功能。
  • 有关 JDK 11 中的新功能和更改的详细信息,请参阅JDK 11 中的新增功能 – 新功能和增强功能。
  • 有关 JDK 10 中的新功能和更改的详细信息,请参阅 JDK 10 中的新增功能。
  • 有关 JDK 9 的所有新功能的完整列表,请参阅 JDK 9 中的新增功能。
  • 有关 JDK 9 中更改的详细信息,请参阅JDK 9 发行说明。

即使您的程序看起来运行成功,您也应该完成本指南中的其余步骤并查看问题列表。

对于您使用的每个工具和第三方库,您可能需要一个支持最新 JDK 版本的更新版本。

检查第三方库和工具供应商的网站,了解每个库或工具的版本,这些库或工具设计用于最新的 JDK。如果存在,则下载并安装新版本。

如果您使用 Maven 或 Gradle 构建应用程序,请确保升级到支持最新 JDK 版本的最新版本。

如果您使用 IDE 开发应用程序,那么它可能有助于迁移现有代码。NetBeans、Eclipse 和 IntelliJ IDE 都有可用的版本,包括对最新 JDK 的支持。

您可以在 OpenJDK wiki 上的 Quality Outreach上查看使用 OpenJDK 构建的许多免费开源软件 (FOSS) 项目的测试状态。

使用新版本的 JDK 编译器编译您的代码将简化向新版本的迁移,因为代码可能依赖于已被确定为有问题的 API 和特性。但是,这不是绝对必要的。

如果您需要使用 JDK 11 和更高版本的编译器编译代码,请注意以下几点:

  • 如果您(\”_\”)在源代码中使用下划线字符作为单字符标识符,那么您的代码将无法在 JDK 11 及更高版本中编译。它会在 JDK 8 中生成警告,并从 JDK 9 开始生成错误。

举个例子:

static Object _ = new Object();

此代码从编译器生成以下错误消息:

MyClass.java:2: error: as of release 9, \’_\’ is a keyword, and may not be used as a legal identifier.

  • 如果您将-sourceand-target选项与 一起使用javac,请检查您使用的值。

支持的-source/-target 值为 17(默认值)、16、15、14、13、12、11、10、9、8 和 7。

在 JDK 8 中,不推荐设置 -source和-target的值为1.5/5或更早的值,如果设置这样值有警告。在 JDK 9 及更高版本中,这些值会直接抛出Error。

>javac -source 5 -target 5 Sample.java warning: [options] bootstrap class path not set in conjunction with -source 5 error: Source option 5 is no longer supported. Use 6 or later. error: Target option 1.5 is no longer supported. Use 1.6 or later.

使用新的–release标志而不是-source -target选项。请参阅Java Development Kit Tool Specifications中的javac。

—release标志的有效值和-source -target选项相同。

javac命令可以识别和处理大于等于 JDK 1.0.2 版本源文件。

请参阅JEP 182:停用 javac -source 和 -target 选项的策略。

  • JDK的内部API,例如sun.misc.Unsafe在 JDK 11 及更高版本中仍然可以访问,但大多数 JDK 的内部 API 在编译时无法访问。您可能会收到编译错误,表明您的应用程序或其库依赖于内部 API。

识别依赖关系,请运行 Java 依赖关系分析工具。如果可能,请更新您的代码以使用支持的替换 API。

您可以使用–add-exports和–add-opens 选项作为临时解决方法来编译引用 JDK 内部类的源代码。有关这些选项的更多信息, 请参阅JEP 261:JDK 中的模块系统和强封装。

  • 您可能会看到比以前更多的弃用警告。

在您的应用程序上运行该jdeps工具以查看您的应用程序和库所依赖的包和类。如果您使用内部 API,则jdeps可能会建议替换以帮助您更新代码。

要查找对内部 JDK API 的依赖关系,请jdeps使用该-jdkinternals选项运行。例如,在调用了sun.misc.BASE64Encoder的类上运行jdeps,您将看到:

>jdeps -jdkinternals Sample.class Sample.class -> JDK removed internal API Sample -> sun.misc.BASE64Encoder JDK internal API (JDK removed internal API) Warning: JDK internal APIs are unsupported and private to JDK implementation that are subject to be removed or changed incompatibly and could break your application. Please modify your code to eliminate dependency on any JDK internal APIs. For the most recent update on JDK internal API replacements, please check: https://wiki.openjdk.java.net/display/JDK8/Java+Dependency+Analysis+Tool JDK Internal API Suggested Replacement —————- ——————— sun.misc.BASE64Encoder Use java.util.Base64 @since 1.8

如果您使用 Maven,则有一个jdeps 可用的插件。

有关jdeps语法,请参阅jdepsJava开发工具包工具规范

但是注意:jdeps是静态分析工具,代码的静态分析可能无法提供完整的依赖关系列表。比如如果代码使用反射来调用内部 API,则jdeps无法发出警告。

JDK 8 和更高版本的 JDK 之间发生了重大变化。

每个新的 Java SE 版本都会引入一些与以前版本的二进制、源代码和行为不兼容的问题。JDK 9 及之后的 Java SE 平台的模块化带来了许多好处,但也带来了许多变化。仅使用官方 Java SE 平台 API 和受支持的特定于 JDK 的 API(supported JDK-specific APIs) 的代码应继续工作而无需更改。使用 JDK 内部 API 的代码应继续运行,但应迁移以使用受支持的 API。

某些 API 在其默认行为中已被设置为不可访问、删除或更改。编译或运行应用程序时可能会遇到问题。请参阅 已删除的工具和组件以及安全更新。

以下部分描述了将 JDK 8 应用程序迁移到更高版本的 JDK 时应注意的 JDK 包中的更改。

查看运行应用程序时可能遇到的更改列表。

一些工具和库使用反射来访问仅供内部使用的 JDK 部分。这种反射的使用会对 JDK 的安全性和可维护性产生负面影响。为了帮助迁移,JDK 9 到 JDK 16 允许这种反射继续进行,但发出有关非法反射访问的警告。但是,JDK 17 是 强封装(strongly encapsulated)的,所以默认情况下不再允许这种反射。访问 API 的非public字段和非public方法的代码java.*将抛出 InaccessibleObjectException.

请注意,所有 JDK 版本(包括 JDK 17)中的工具和库都可以使用sun.misc和sun.reflect 包进行反射。

java启动选项–illegal-access 允许在 JDK 9 到 JDK 16 中使用反射调用JDK 内部Api。您可以指定以下参数:

  • –illegal-access=permit: 允许类路径上的代码反映java.*JDK 8 中存在的包的内部结构。对任何此类元素的第一次反射访问操作会导致发出警告,但在此之后不会发出警告。
  • –illegal-access=warn:导致为每个非法反射访问操作发出警告消息。
  • –illegal-access=debug: 导致为每个非法反射访问操作显示警告消息和堆栈跟踪。
  • –illegal-access=deny: 禁用所有非法反射访问操作,但由其他命令行选项启用的操作除外,例如 –add-opens.

许多工具和库已更新以避免依赖 JDK 内部Api,而是使用在 JDK 8 和 17 之间引入的标准 Java API。同时–illegal-access启动选项在 JDK 17 中已过时。在 JDK 17 中使用此启动器选项,无论是使用permit, warn,debug或deny, 除了发出警告消息外没有任何作用。

如果您无法获取或部署较新版本的工具和库,则有两个命令行选项可让您授予对旧版本工具和库的特定内部 API 的访问权限:

  • –add-exports:如果您有一个较旧的工具或库需要使用已被强封装的内部 API,则使用 –add-exports运行时选项。您还可以 –add-exports在编译时使用来访问内部 API。
  • –add-opensjava.*:如果您有一个较旧的工具或库需要通过反射访问 API 的非public字段和非public方法,请使用该–add-opens选项。

请参阅JEP 403:默认情况下强封装 JDK 内部。

如果您有一个较旧的工具或库需要使用已被强封装的内部 API,请使用 –add-exports运行时选项。您还可以–add-exports在编译时使用来访问内部 API。

该–add-exports选项的语法是:

–add-exports <source-module>/<package>=<target-module>(,<target-module>)*

其中 <source-module><target-module>是模块名称,<package>是包的名称。

–add-exports如果目标模块读取源模块, 该选项允许目标模块中的代码访问源模块的命名包中的类型。

作为一种特殊情况,如果<target-module>是 ALL-UNNAMED,则源包将导出到所有未命名的模块,无论它们最初存在还是稍后创建。例如:

–add-exports java.management/sun.management=ALL-UNNAMED

此示例允许所有未命名模块中的代码(类路径上的代码)访问 java.management/sun.management。

如果类路径上的代码使用反射 API (setAccessible(true)) 尝试访问java.*API 的非公共字段和方法,则代码将失败。默认情况下,JDK 17 不允许这样做。但是,您可以使用该 –add-opens选项来允许这样做。有关更多信息,请参见–add-opens部分。

如果在类路径上运行的应用程序oldApp必须使用模块的未导出com.sun.jmx.remote.internal包java.management,则可以通过以下方式授予它所需的访问权限:

–add-exports java.management/com.sun.jmx.remote.internal=ALL-UNNAMED

您还可以使用Add-ExportsJAR 文件清单属性:

Add-Exports:java.management/sun.management

谨慎使用该–add-exports选项。您可以使用它来访问库模块甚至 JDK 本身的内部 API,但这样做的风险由您自己承担。如果该内部 API 更改或被删除,那么您的库或应用程序将失败。

参见JEP 261:模块系统。

一些工具和库使用反射 API (setAccessible(true)) 尝试访问java.*API 的非公共字段和方法。默认情况下,这在 JDK 17 上不再可能,但您可以使用–add-opens命令行上的选项为特定工具和库启用它。

–add-opens语法如下:

–add-opens <module>/<package>=<target-module>(,<target-module>)*

无论模块声明如何, 此选项都允许<module>打开<package><target-module>。

作为一种特殊情况,如果<target-module> 是ALL-UNNAMED,则源包将导出到所有未命名的模块,无论它们最初存在还是稍后创建。例如:

–add-opens java.management/sun.management=ALL-UNNAMED

此示例允许类路径上的所有代码访问java.management/sun.management包中公共类型的非公共成员。

JDK 10 对 JDK 9 中引入的版本字符串方案引入了一些小的更改,以更好地适应基于时间的发布模型。JDK 11 及更高版本保留了 JDK 10 中引入的版本字符串格式。

如果您的代码依赖于版本字符串格式来区分主要、次要、安全和补丁更新版本,那么您可能需要更新它。

新版本字符串的格式为:

$FEATURE.$INTERIM.$UPDATE.$PATCH

添加了用于解析、验证和比较版本字符串的简单 Java API。看java.lang.Runtime.Version.

请参阅Java 平台中的版本字符串格式,标准版安装指南

有关 JDK 9 中引入的版本字符串的更改,请参阅 JEP 223:新版本字符串方案。

有关 JDK 10 中引入的版本字符串更改,请参阅JEP 322: Time-Based Release Versioning。

对 JDK 和 JRE 进行了重大更改。

安装 JDK 后,如果查看文件系统,您会注意到目录布局与 JDK 9 之前的版本不同。

JDK 11 及更高版本

JDK 11 及更高版本没有 JRE 映像。请参阅Java 平台中 JDK的已安装目录结构,标准版安装指南

DK 9 和 JDK 10

以前的版本有两种类型的运行时映像:JRE,它是 Java SE 平台的完整实现,以及 JDK,它将整个 JRE 包含在一个jre/目录中,以及开发工具和库。

在 JDK 9 和 JDK 10 中,JDK 和 JRE 是两种类型的模块化运行时映像,包含以下目录:

  • bin: 包含二进制可执行文件。
  • conf: 包含.properties、.policy和其他类型的文件,供开发人员、部署人员和最终用户编辑。这些文件以前在lib目录或其子目录中找到。
  • lib: 包含动态链接的库和 JDK 的完整内部实现。

在 JDK 9 和 JDK 10 中,仍然有单独的 JDK 和 JRE 下载,但每个都有相同的目录结构。JDK 映像包含历史上在 JDK 中发现的额外工具和库。没有jdk/与jre/包装器目录,并且二进制文件(例如java命令)不重复。

请参阅JEP 220:模块化运行时映像。

JDK 9 和更高版本维护了自 1.2 版本以来存在的类加载器的层次结构。但是,为了实现模块系统,进行了以下更改:

  • 应用程序类加载器不再是URLClassLoader而是一个内部类。它是既不是 Java SE 也不是 JDK 模块的模块中的类的默认加载器。
  • 扩展类加载器已重命名;它现在是平台类加载器。Java SE 平台中的所有类都保证通过平台类加载器可见。
  • 仅仅因为一个类通过平台类加载器可见并不意味着该类实际上是由平台类加载器定义的。Java SE 平台中的一些类是由平台类加载器定义的,而其他类是由引导类加载器定义的。应用程序不应依赖于哪个类加载器定义了哪个平台类。
  • 在 JDK 9 中实现的更改可能会影响创建类加载null器(即引导类加载器)作为父类加载器并假定所有平台类对父类可见的代码。可能需要更改此类代码以使用平台类加载器作为父级(请参阅 ClassLoader.getPlatformClassLoader)。
  • 平台类加载器不是URLClassLoader,而是一个内部类。
  • 引导类加载器仍然内置在 Java 虚拟机null中,由ClassLoaderAPI。它定义了一些关键模块中的类,例如java.base. 因此,它定义的类远少于 JDK 8 中的类,因此部署-Xbootclasspath/a或创建类加载null器作为父级的应用程序可能需要如前所述进行更改。

以前存储在lib/rt.jar、lib/tools.jar中的类和资源文件lib/dt.jar以及各种其他内部 JAR 文件以更有效的格式存储在lib目录中特定于实现的文件中。

删除rt.jar和类似文件会导致以下方面的问题:

  • 从 JDK 9 开始,ClassLoader.getSystemResource不返回指向 JAR 文件的 URL(因为没有 JAR 文件)。相反,它返回一个 jrtURL,该 URL 命名存储在运行时映像中的模块、类和资源,而不显示映像的内部结构或格式。

例如:

ClassLoader.getSystemResource(\”java/lang/Class.class\”);

在 JDK 8 上运行时,此方法返回以下形式的 JAR URL:

jar:file:/usr/local/jdk8/jre/lib/rt.jar!/java/lang/Class.class

模块化的镜像(modular image)不包含任何 JAR 文件,因此这种形式的 URL 没有意义。在 JDK 9 及更高版本上,此方法返回:

jrt:/java.base/java/lang/Class.class

  • java.security.CodeSource API 和安全策略文件使用 URL 来命名要被授予特定权限的代码库的位置。请参阅Java 平台中的策略文件语法,标准版安全开发人员指南。运行时组件需要的特定权限当前使用文件URL的方式定义在conf/security/java.policy文件中(Components of the runtime system that require specific permissions are currently identified in the conf/security/java.policy file by using file URLs) 。
  • 旧版本的 IDE 和其他开发工具需要能够枚举存储在运行时映像中的类和资源文件,并通过打开和读取rt.jar类似文件来直接读取它们的内容。这对于模块化镜像是不可能的。

在 JDK 8 及更早版本中,扩展机制使运行时环境可以查找和加载扩展类,而无需在类路径上专门命名它们。从 JDK 9 开始,如果您需要使用扩展类,请确保 JAR 文件位于类路径上。

在 JDK 9 和 JDK 10 中,如果设置了系统属性或目录存在,javac编译器和java 启动器将退出。要另外检查特定于平台的系统范围目录,请指定 命令行选项。如果目录存在且不为空,这将导致发生相同的退出行为。扩展类加载器保留在 JDK 9(及更高版本)中,并被指定为平台类加载器(参见java.ext.dirslib/ext-XX:+CheckEndorsedAndExtDirsgetPlatformClassLoader.) 但是,在 JDK 11 中,此选项已过时,使用时会发出警告。

以下错误表示您的系统配置为使用扩展机制:

<JAVA_HOME>/lib/ext exists, extensions mechanism no longer supported; Use -classpath instead. .Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit.

如果设置了java.ext.dirs系统属性, 您将看到类似的错误。

要修复此错误,请删除ext/目录或java.ext.dirs系统属性。

请参阅JEP 220:模块化运行时映像。

java.endorsed.dirs系统属性和lib/endorsed目录将不再存在。如果检测到任何一个,编译器和启动器将退出javac。java

从 JDK 9 开始,您可以使用可升级模块或将 JAR 文件放在类路径中实现类似功能。

此机制旨在让应用程序服务器覆盖 JDK 中使用的组件。要更新的包将被放入 JAR 文件中,系统属性java.endorsed.dirs会告诉 Java 运行时环境在哪里可以找到它们。如果未指定此属性的值,则使用默认值$JAVA_HOME/lib/endorsed。

在 JDK 8 中,您可以使用-XX:+CheckEndorsedAndExtDirs命令行参数来检查系统上任何位置的此类目录。

在 JDK 9 及更高版本中,如果设置了java.endorsed.dirs系统属性或lib/endorsed目录存在 ,javac编译器和java启动器将退出。

以下错误意味着您的系统配置为使用认可的标准覆盖机制:

<JAVA_HOME>/lib/endorsed is not supported. Endorsed standards and standalone APIs in modular form will be supported via the concept of upgradeable modules. Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit.

如果设置了java.endorsed.dirs系统属性, 您将看到类似的错误。

要修复此错误,请删除lib/endorsed目录或取消设置java.endorsed.dirs系统属性。

请参阅JEP 220:模块化运行时映像。

从 JDK 9 开始删除了macOS 特定功能。

该类包含对 Apple 特定的和包java.awt.Desktop中 API 的替换。新的 API 取代了 macOS API,并且独立于具体操作系统。 com.apple.eawtcom.apple.eio

com.apple.eawt和包中的 APIcom.apple.eio是封装的,因此您将无法在 JDK 9 或更高版本中针对它们进行编译。但是,它们在运行时仍可访问,因此编译为旧版本的现有代码继续运行。apple最终,使用andcom.apple 包及其子包 中的内部类的库或应用程序 将需要迁移到新的 API。

com.apple.concurrent 和包被删除,apple.applescript没有任何替换。

请参阅JEP 272:特定于平台的桌面功能。

AppleScript 引擎,特定于平台的javax.script实现,已在 JDK 中删除,没有任何替换。

AppleScript 引擎在最近的版本中几乎无法使用。该功能仅在 JDK 7 或 JDK 8 中有效,系统上已经有 Apple 版本的AppleScriptEngine.jar文件。

Java 11 及更高版本的安装程序会在安装 JDK 时创建 Windows 注册表项。对于 JDK 16,安装程序会创建以下 Windows 注册表项:

  • “HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK”
  • “HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\16”

如果安装了两个版本的 JDK,则会创建两个不同的 Windows 注册表项。例如,如果 JDK 15.0.1 与 JDK 16 一起安装,则安装程序会创建另一个 Windows 注册表项,如下所示:

  • “HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK”
  • “HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\16”
  • “HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\15.0.1”

Java 部署技术在 JDK 9 中被弃用,并在 JDK 11 中被删除。

Java Applet 和 WebStart 功能,包括 Applet API、Java 插件、Java Applet 查看器、JNLP 和 Java Web Start(包括 javaws 工具)在 JDK 9 中均已弃用

使用jlinkJDK 9 引入的工具来打包和部署专用运行时,而不是依赖于预安装的系统 JRE。

从 JDK 9 开始,删除了请求不是在启动时启动的 JRE 的 JRE 版本的能力。

现代应用程序通常使用 Java Web Start (JNLP)、本机 OS 打包系统或活动安装程序进行部署。这些技术有自己的方法来管理所需的 JRE,方法是根据需要查找或下载和更新所需的 JRE。这使得启动器的启动时可以选择已过时JRE 版本。

在以前的版本中,您可以指定启动应用程序时要使用的 JRE 版本(或版本范围)。可以通过命令行选项和应用程序 JAR 文件中的清单条目来选择版本。

从 JDK 9 开始,java launcher 修改如下:

  • -version:如果在命令行上给出 了选项,则发出错误消息并退出。
  • JRE-Version如果在 JAR 文件中找到清单条目 ,则发出警告消息并继续。

请参阅JEP 231:删除启动时 JRE 版本选择。

以下 GC 组合将导致您的应用程序在 JDK 9 及更高版本中无法启动:

  • DefNew + CMS
  • ParNew + SerialOld
  • Incremental CMS

CMS 的前台模式也已被删除。删除的命令行标志是-Xincgc、-XX:+CMSIncrementalMode、 -XX:+UseCMSCompactAtFullCollection、-XX:+CMSFullGCsBeforeCompaction和 -XX:+UseCMSCollectionPassing。

命令行标志-XX:+UseParNewGC不再有效。该ParNew标志只能用于 CMS 和 CMS 需要ParNew。因此,该-XX:+UseParNewGC标志已被弃用,并且在未来的版本中删除。

请参阅JEP 214:删除 JDK 8 中已弃用的 GC 组合。

删除了永久代

JDK 8 中删除了永久代,相关的 VM 选项会导致打印警告。您应该从脚本中删除这些选项:

  • -XX:MaxPermSize=size
  • -XX:PermSize=size

在 JDK 9 及更高版本中,JVM 会显示如下警告:

Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option MaxPermSize; support was removed in 8.0

使用了永久代参数的工具可能必须更新。

请参阅JEP 122:删除永久代和JDK 9 发行说明 – 删除的 API、功能和选项。

垃圾收集(GC)日志使用JVM统一的日志框架,新旧日志存在一些差异。您正在使用的任何 GC 日志解析器都可能需要更改。

您可能还需要更新 JVM 日志记录选项。所有与 GC 相关的日志记录都应使用 gc标签(例如—Xlog:gc),通常与其他标签结合使用。和选项已被弃用 —XX:+PrintGCDetails。-XX:+PrintGC

请参阅Java Development Kit Tool Specifications和JEP 271: Unified GC Logging中的使用 JVM Unified Logging Framework 启用日志记录。

java.util.regex.Pattern用方括号定义正则表达式中的字符类。例如,[abc] 匹配a,b,或c。否定字符类是用紧跟在左大括号后面的插入符号定义的。例如, [^abc]匹配除a,b, 或之外的任何字符c。

在 JDK 8 及更早版本中,否定字符类不会否定嵌套字符类。例如,[^a-b[c-d]e-f]匹配 c但不匹配a,或者 e因为它们不在嵌套类中。运算符一个接一个地应用。^在此示例中,在嵌套之前应用了否定运算符。在 JDK 8 及更早的版本中,运算符^仅应用于字符类中的最外层字符,而不应用于到嵌套的字符类。这种行为令人困惑且难以理解。

但是,在 JDK 9 及更高版本中,否定运算符应用于所有嵌套字符类。例如, [^a-b[c-d]e-f]不匹配 c。

为了进一步解释,请考虑以下正则表达式:

[^a-d&&c-f]

在 JDK 8 中,^首先应用运算符,因此该示例被解释为 [^a-d] 与[c-f]. 这匹配 e and f但不 匹配a, b, c, or d。

在 JDK 9 及更高版本中,&&首先应用运算符,因此该示例被解释为[a-d]&&[c-f]. 这匹配 a, b,e和f但不匹配c or d。

作为最佳实践,寻找使用字符类的正则表达式以及否定、交集和嵌套类的某种组合。可能需要调整这些正则表达式以考虑更改的行为。

  • 如果需要,使用javac工具 -–release中的新标志交叉编译到平台的旧版本 。
  • 利用 IDE 的建议来使用最新功能更新代码。
  • 通过运行静态分析工具jdeprscan查看您的代码是否使用了已弃用的 API 。正如本指南中已经提到的,可以从 JDK 中删除 API,但必须提前通知。
  • 熟悉多版本 JAR 文件等新功能(请参阅jar)。

使用JDK 17 构建hutool,新建springboot工程并调用JDK 17调用hutool中的功能。

先用JDK 17直接构建,修改pom.xml,将JDK版本修改为17

首先确保已经安装配置好JDK 17:这里使用了更高版本的19

运行mvn clean compile进行编译

编译失败,因为javax.xml包已经被移除。

在hutool-core/pom.xml中添加javax.xml的依赖

<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency>

重新编译,全部编译成功

执行mvn install -Dmaven.javadoc.skip=true 执行单元测试并本地安装hutool,执行单元测试在这里很重要,因为不能保证升级JDK之后,代码逻辑不受影响。

对于Unable to make protected native java.lang.Object java.lang.Object.clone() throws java.lang.CloneNotSupportedException accessible: module java.base does not \”opens java.lang\” to unnamed module @5a65309b 错误,明显是因为Java 9开始的模块系统强封装造成的,我们需要添加–add-exports。

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <argLine>–add-opens java.base/java.lang=ALL-UNNAMED</argLine> </configuration> </plugin>

添加上面插件和配置之后,java.lang.CloneNotSupportedException不再出现,现在来处理java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached。这个错误是因为要创建的线程数超过了系统的限制,调大即可。

解决这个错误之后,出现module java.base does not \”opens java.util.regex\” to unnamed module @15615099

继续添加opens

出现新的错误,这是因为JDK移除了javax.activation,添加依赖就好

添加如下依赖

出现新的错误java.lang.NoSuchFieldException: modifiers

因为JDK 12开始,field字段不再有modifiers,所以更改代码以满足要求

再次重新构建,出现新的错误

添加opens:

再次构建,出现如下错误

debug之后发现,这个错误是因为高版本jdk使用MemberName来判断field的modifier,让前面代码中修改失败,所以只能先暂时跳过这个测试并不使用这个功能,等待项目组更新版本支持高版本JDK。

至此构建全部成功

新建SpringBoot工程

引入之前构建在本地的hutool

编写一个简单的测试类:

@RestController public class TestController { @GetMapping(\”/test/{plainText}\”) public ResponseEntity test(@PathVariable(\”plainText\”) String plainText) { int hash = HashUtil.apHash(plainText); return ResponseEntity.ok(hash); } }

启动应用进行测试:

偷天换日,用JavaAgent欺骗你的JVM

原创:微信公众号 码农参上,欢迎分享,转载请保留出处。

熟悉Spring的小伙伴们应该都对aop比较了解,面向切面编程允许我们在目标方法的前后织入想要执行的逻辑,而今天要给大家介绍的Java Agent技术,在思想上与aop比较类似,翻译过来可以被称为Java代理Java探针技术。

Java Agent出现在JDK1.5版本以后,它允许程序员利用agent技术构建一个独立于应用程序的代理程序,用途也非常广泛,可以协助监测、运行、甚至替换其他JVM上的程序,先从下面这张图直观的看一下它都被应用在哪些场景:

看到这里你是不是也很好奇,究竟是什么神仙技术,能够应用在这么多场景下,那今天我们就来挖掘一下,看看神奇的Java Agent是如何工作在底层,默默支撑了这么多优秀的应用。

回到文章开头的类比,我们还是用和aop比较的方式,来先对Java Agent有一个大致的了解:

  • 作用级别:aop运行于应用程序内的方法级别,而agent能够作用于虚拟机级别
  • 组成部分:aop的实现需要目标方法和逻辑增强部分的方法,而Java Agent要生效需要两个工程,一个是agent代理,另一个是需要被代理的主程序
  • 执行场合:aop可以运行在切面的前后或环绕等场合,而Java Agent的执行只有两种方式,jdk1.5提供的preMain模式在主程序运行前执行,jdk1.6提供的agentMain在主程序运行后执行

下面我们就分别看一下在两种模式下,如何动手实现一个agent代理程序。

Premain模式允许在主程序执行前执行一个agent代理,实现起来非常简单,下面我们分别实现两个组成部分。

先写一个简单的功能,在主程序执行前打印一句话,并打印传递给代理的参数:

在写完了agent的逻辑后,需要把它打包成jar文件,这里我们直接使用maven插件打包的方式,在打包前进行一些配置。

配置的打包参数中,通过manifestEntries的方式添加属性到MANIFEST.MF文件中,解释一下里面的几个参数:

  • Premain-Class:包含premain方法的类,需要配置为类的全路径
  • Can-Redefine-Classes:为true时表示能够重新定义class
  • Can-Retransform-Classes:为true时表示能够重新转换class,实现字节码替换
  • Can-Set-Native-Method-Prefix: 为true时表示能够设置native方法的前缀

其中Premain-Class为必须配置,其余几项是非必须选项,默认情况下都为false,通常也建议加入,这几个功能我们会在后面具体介绍。在配置完成后,使用mvn命令打包:

打包完成后生成myAgent-1.0.jar文件,我们可以解压jar文件,看一下生成的MANIFEST.MF文件:

可以看到,添加的属性已经被加入到了文件中。到这里,agent代理部分就完成了,因为代理不能够直接运行,需要附着于其他程序,所以下面新建一个工程来实现主程序。

在主程序的工程中,只需要一个能够执行的main方法的入口就可以了。

在主程序完成后,要考虑的就是应该如何将主程序与agent工程连接起来。这里可以通过-javaagent参数来指定运行的代理,命令格式如下:

并且,可以指定的代理的数量是没有限制的,会根据指定的顺序先后依次执行各个代理,如果要同时运行两个代理,就可以按照下面的命令执行:

以我们在idea中执行程序为例,在VM options中加入添加启动参数:

执行main方法,查看输出结果:

根据执行结果的打印语句可以看出,在执行主程序前,依次执行了两次我们的agent代理。可以通过下面的图来表示执行代理与主程序的执行顺序。

在提供便利的同时,premain模式也有一些缺陷,例如如果agent在运行过程中出现异常,那么也会导致主程序的启动失败。我们对上面例子中agent的代码进行一下改造,手动抛出一个异常。

再次运行主程序:

可以看到,在agent抛出异常后主程序也没有启动。针对premain模式的一些缺陷,在jdk1.6之后引入了agentmain模式。

agentmain模式可以说是premain的升级版本,它允许代理的目标主程序的jvm先行启动,再通过attach机制连接两个jvm,下面我们分3个部分实现。

agent部分和上面一样,实现简单的打印功能:

修改maven插件配置,指定Agent-Class

这里我们直接启动主程序等待代理被载入,在主程序中使用了System.in进行阻塞,防止主进程提前结束。

和premain模式不同,我们不能再通过添加启动参数的方式来连接agent和主程序了,这里需要借助com.sun.tools.attach包下的VirtualMachine工具类,需要注意该类不是jvm标准规范,是由Sun公司自己实现的,使用前需要引入依赖:

VirtualMachine代表了一个要被附着的java虚拟机,也就是程序中需要监控的目标虚拟机,外部进程可以使用VirtualMachine的实例将agent加载到目标虚拟机中。先看一下它的静态方法attach

通过attach方法可以获取一个jvm的对象实例,这里传入的参数是目标虚拟机运行时的进程号pid。也就是说,我们在使用attach前,需要先获取刚才启动的主程序的pid,使用jps命令查看线程pid

获取到主程序AgentmainTest运行时pid是16392,将它应用于虚拟机的连接。

在获取到VirtualMachine实例后,就可以通过loadAgent方法可以实现注入agent代理类的操作,方法的第一个参数是代理的本地路径,第二个参数是传给代理的参数。执行AttachTest,再回到主程序AgentmainTest的控制台,可以看到执行了了agent中的代码:

这样,一个简单的agentMain模式代理就实现完成了,可以通过下面这张图再梳理一下三个模块之间的关系。

到这里,我们就已经简单地了解了两种模式的实现方法,但是作为高质量程序员,我们肯定不能满足于只用代理单纯地打印语句,下面我们再来看看能怎么利用Java Agent搞点实用的东西。

在上面的两种模式中,agent部分的逻辑分别是在premain方法和agentmain方法中实现的,并且,这两个方法在签名上对参数有严格的要求,premain方法允许以下面两种方式定义:

agentmain方法允许以下面两种方式定义:

如果在agent中同时存在两种签名的方法,带有Instrumentation参数的方法优先级更高,会被jvm优先加载,它的实例inst会由jvm自动注入,下面我们就看看能通过Instrumentation实现什么功能。

先大体介绍一下Instrumentation接口,其中的方法允许在运行时操作java程序,提供了诸如改变字节码,新增jar包,替换class等功能,而通过这些功能使Java具有了更强的动态控制和解释能力。在我们编写agent代理的过程中,Instrumentation中下面3个方法比较重要和常用,我们来着重看一下。

addTransformer方法允许我们在类加载之前,重新定义Class,先看一下方法的定义:

ClassFileTransformer是一个接口,只有一个transform方法,它在主程序的main方法执行前,装载的每个类都要经过transform执行一次,可以将它称为转换器。我们可以实现这个方法来重新定义Class,下面就通过一个例子看看具体如何使用。

首先,在主程序工程创建一个Fruit类:

编译完成后复制一份class文件,并将其重命名为Fruit2.class,再修改Fruit中的方法为:

创建主程序,在主程序中创建了一个Fruit对象并调用了其getFruit方法:

这时执行结果会打印apple,接下来开始实现premain代理部分。

在代理的premain方法中,使用InstrumentationaddTransformer方法拦截类的加载:

FruitTransformer类实现了ClassFileTransformer接口,转换class部分的逻辑都在transform方法中:

transform方法中,主要做了两件事:

  • 因为addTransformer方法不能指明需要转换的类,所以需要通过className判断当前加载的class是否我们要拦截的目标class,对于非目标class直接返回原字节数组,注意className的格式,需要将类全限定名中的.替换为/
  • 读取我们之前复制出来的class文件,读入二进制字符流,替换原有classfileBuffer字节数组并返回,完成class定义的替换

将agent部分打包完成后,在主程序添加启动参数:

再次执行主程序,结果打印:

这样,就实现了在main方法执行前class的替换。

我们可以直观地从方法的名字上来理解它的作用,重定义class,通俗点来讲的话就是实现指定类的替换。方法定义如下:

它的参数是可变长的ClassDefinition数组,再看一下ClassDefinition的构造方法:

ClassDefinition中指定了的Class对象和修改后的字节码数组,简单来说,就是使用提供的类文件字节,替换了原有的类。并且,在redefineClasses方法重定义的过程中,传入的是ClassDefinition的数组,它会按照这个数组顺序进行加载,以便满足在类之间相互依赖的情况下进行更改。

下面通过一个例子来看一下它的生效过程,premain代理部分:

主程序可以直接复用上面的,执行后打印:

可以看到,用我们指定的class文件的字节替换了原有类,即实现了指定类的替换。

retransformClasses应用于agentmain模式,可以在类加载之后重新定义Class,即触发类的重新加载。首先看一下该方法的定义:

它的参数classes是需要转换的类数组,可变长参数也说明了它和redefineClasses方法一样,也可以批量转换类的定义。

下面,我们通过例子来看看如何使用retransformClasses方法,agent代理部分代码如下:

看一下这里调用的addTransformer方法的定义,与上面略有不同:

ClassFileTransformer转换器依旧复用了上面的FruitTransformer,重点看一下新加的第二个参数,当canRetransformtrue时,表示允许重新定义class。这时,相当于调用了转换器ClassFileTransformer中的transform方法,会将转换后class的字节作为新类定义进行加载。

主程序部分代码,我们在死循环中不断的执行打印语句,来监控类是否发生了改变:

最后,使用attach api注入agent代理到主程序中:

回到主程序控制台,查看运行结果:

可以看到在注入代理后,打印语句发生变化,说明类的定义已经被改变并进行了重新加载。

除了这几个主要的方法外,Instrumentation中还有一些其他方法,这里仅简单列举一下常用方法的功能:

  • removeTransformer:删除一个ClassFileTransformer类转换器
  • getAllLoadedClasses:获取当前已经被加载的Class
  • getInitiatedClasses:获取由指定的ClassLoader加载的Class
  • getObjectSize:获取一个对象占用空间的大小
  • appendToBootstrapClassLoaderSearch:添加jar包到启动类加载器
  • appendToSystemClassLoaderSearch:添加jar包到系统类加载器
  • isNativeMethodPrefixSupported:判断是否能给native方法添加前缀,即是否能够拦截native方法
  • setNativeMethodPrefix:设置native方法的前缀

在上面的几个例子中,我们都是直接读取的class文件中的字节来进行class的重定义或转换,但是在实际的工作环境中,可能更多的是去动态的修改class文件的字节码,这时候就可以借助javassist来更简单的修改字节码文件。

简单来说,javassist是一个分析、编辑和创建java字节码的类库,在使用时我们可以直接调用它提供的api,以编码的形式动态改变或生成class的结构。相对于ASM等其他要求了解底层虚拟机指令的字节码框架,javassist真的是非常简单和快捷。

下面,我们就通过一个简单的例子,看看如何将Java agent和Javassist结合在一起使用。首先引入javassist的依赖:

我们要实现的功能是通过代理,来计算方法执行的时间。premain代理部分和之前基本一致,先添加一个转换器:

calculate方法中,使用javassist动态的改变了方法的定义:

在上面的代码中,主要实现了这些功能:

  • 利用全限定名获取类CtClass
  • 根据方法名获取方法CtMethod,并通过CtNewMethod.copy方法复制一个新的方法
  • 修改旧方法的方法名为getFruit$agent
  • 通过setBody方法修改复制出来方法的内容,在新方法中进行了逻辑增强并调用了旧方法,最后将新方法添加到类中

主程序仍然复用之前的代码,执行查看结果,完成了代理中的执行时间统计功能:

这时候我们可以再通过反射看一下:

查看结果,可以看到类中确实已经新增了一个方法:

除此之外,javassist还有很多其他的功能,例如新建Class、设置父类、读取和写入字节码等等,大家可以在具体的场景中学习它的用法。

虽然我们在平常的工作中,直接用到Java Agent的场景可能并不是很多,但是在热部署、监控、性能分析等工具中,它们可能隐藏在业务系统的角落里,一直在默默发挥着巨大的作用。

本文从Java Agent的两种模式入手,手动实现并简要分析了它们的工作流程,虽然在这里只利用它们完成了一些简单的功能,但是不得不说,正是Java Agent的出现,让程序的运行不再循规蹈矩,也为我们的代码提供了无限的可能性。

作者简介,码农参上,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术。

SDK_JDK_JRE_JVM的关系

JRE JVM 四者的关系

一:SDK与JDK的关系(可以认为jdk只是sdk的一种子集)

SDK是Software Development Kit的缩写,中文意思是“软件开发工具包”。这是一个覆盖面相当广泛的名词,可以这么说:辅助开发某一类软件的相关文档、范例和工具的集合都可以叫做“SDK”。SDK是一系列文件的组合,它为软件的开发提供一个平台(它为软件开发使用各种API提供便利)。

JDK(Java Development Kit,Java开发工具包)是Sun Microsystems针对Java开发员的产品。自从Java推出以来,JDK已经成为使用最广泛的Java SDK(Software development kit)。可以认为jdk只是sdk的一种(子集),因为它是开发java程序的一个平台,开发其他程序的sdk可以没有jdk。

二:JDK JRE JVM的关系(如图)

JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。JRE(Java Runtime Environment)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。

JDK中包含JRE,在JDK的安装目录下有一个名为jre的目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。

JDK是整个JAVA的核心,包括了Java运行环境JRE(Java Runtime Envirnment)、一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)。

  ①SE(J2SE),standard edition,标准版,是我们通常用的一个版本,从JDK 5.0开始,改名为Java SE。

  ②EE(J2EE),enterprise edition,企业版,使用这种JDK开发J2EE应用程序,从JDK 5.0开始,改名为Java EE。

  ③ME(J2ME),micro edition,主要用于移动设备、嵌入式设备上的java应用程序,从JDK 5.0开始,改名为Java ME

JRE(Java Runtime Environment)

  是运行基于Java语言编写的程序所不可缺少的运行环境。也是通过它,Java的开发者才得以将自己开发的程序发布到用户手中,让用户使用。

  JRE中包含了Java virtual machine(JVM),runtime class libraries和Java application launcher,这些是运行Java程序的必要组件。

  与大家熟知的JDK不同,JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器),只是针对于使用Java程序的用户。

JVM:Java Virtual Mechinal(JAVA虚拟机)。JVM是JRE的一部分,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM 的主要工作是解释自己的指令集(即字节码)并映射到本地的 CPU 的指令集或 OS 的系统调用。Java语言是跨平台运行的,其实就是不同的操作系统,使用不同的JVM映射规则,让其与操作系统无关,完成了跨平台性。JVM 对上层的 Java 源文件是不关心的,它关注的只是由源文件生成的类文件( class file )。类文件的组成包括 JVM 指令集,符号表以及一些补助信息。

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。