On Mon, 7 Apr 2025 06:34:11 GMT, Jaikiran Pai <j...@openjdk.org> wrote:
> Can I please get a review of this change which proposes to address the > increase in memory footprint of an application that uses signed JAR files, > signed with `SHA-384` digest algorithm? This addresses > https://bugs.openjdk.org/browse/JDK-8353787. > > As noted in that issue and the linked mailing list discussion, it has been > noticed that when JARs signed with `SHA-384` digest algorithm (which is the > default since Java 19) are loaded, the number of `java.util.Attributes$Name` > instances held in memory increase, leading to an increase in the memory > footprint of the application. > > The `Attributes` class has an internal cache which is meant to store `Name` > instances of some well-known manifest attributes. It already has the `Name` > instances for `SHA1-Digest` and `SHA-256-Digest` manifest attributes cached, > but is missing an entry for `SHA-384-Digest`. The commit in this PR adds > `SHA-384-Digest` to that cache. > > Given the nature of this change, no new jtreg test has been introduced and > existing tests in tier1, tier2 and tier3 continue to pass with this change. > I've manually verified that this change does reduce the memory footprint of > an application which has signed JARs with `SHA-384` digest algorithm > (details in a comment of this PR). Here's a trivial application which creates and uses signed JARs for use in a `URLClassLoader`. Running this application will show that after the change proposed in this PR, the number of `java.util.jar.Attributes$Name` instances reduce drastically. Specifically, before this change, the `jmap -histo:live` of this application will show: num #instances #bytes class name (module) ------------------------------------------------------- ... 7: 100127 2403048 java.util.jar.Attributes$Name (java.base@24) ... (more than 2MB of `Attributes$Name` instances) and after this change, the `jmap -histo:live` will show: num #instances #bytes class name (module) ------------------------------------------------------- ... 164: 28 672 java.util.jar.Attributes$Name (java.base@25-internal) ... (just 672 bytes) import java.util.*; import java.nio.file.*; import java.nio.charset.*; import java.util.jar.*; import java.net.*; public class AttributesNameFootprint { private static final Path KEYSTORE = // Path.of(<the keystore path>); private static final String ALIAS = // alias in the keystore private static final String PASSPHRASE = // passphrase of the keystore public static void main(final String[] args) throws Exception { // the number of JARs that we want to be used by the application // in its classpath final int numJARs = 100; final List<URL> classpath = new ArrayList<>(); // create some signed JARs for (int i = 1; i <= numJARs; i++) { final String jarNamePrefix = "jar" + i; final Path jar = createJAR(jarNamePrefix, jarNamePrefix + "-entry"); final Path signed = signJAR(jar); classpath.add(signed.toUri().toURL()); } // use those signed JARs in the classpath of a URLClassLoader // and load some resources, to trigger the instantiation of the // Manifest instances of these JAR files try (final URLClassLoader cl = new URLClassLoader(classpath.toArray(URL[]::new))) { for (int i = 1; i <= numJARs; i++) { final String jarNamePrefix = "jar" + i; final String resName = jarNamePrefix + "-entry"; final URL resource = cl.getResource(resName); if (resource == null) { throw new RuntimeException("Failed to find " + resName); } //System.out.println("found " + resName + " - " + resource); } // check the memory footprint of the application, especially // the number of java.util.jar.Attributes$Name instances final long pid = ProcessHandle.current().pid(); System.out.println("You can now run "jmap -histo:live " + pid + "". Once done, press any key to exit"); System.in.read(); } System.out.println("done"); } private static Path createJAR(final String jarNamePrefix, final String entryName) throws Exception { final Path jarDir = Files.createDirectories(Path.of(".").resolve("jars")); final Path jarFile = Files.createTempFile(jarDir, jarNamePrefix, ".jar"); // create a manifest file which will trigger Manifest instance // creation when parsed by the URLClassLoader final Manifest manifest = new Manifest(); final Attributes mainAttrs = manifest.getMainAttributes(); mainAttrs.putValue("Manifest-Version", "1.0"); mainAttrs.putValue("Class-Path", "non-existent.jar"); // create the JAR file with this manifest try (final JarOutputStream jaros = new JarOutputStream(Files.newOutputStream(jarFile), manifest)) { final JarEntry jarEntry = new JarEntry(entryName); jaros.putNextEntry(jarEntry); jaros.write(("hello " + entryName).getBytes(StandardCharsets.US_ASCII)); jaros.closeEntry(); // create 1000 more entries in the JAR for (int i = 0; i < 1000; i++) { final String otherEntry = entryName + i; final JarEntry other = new JarEntry(otherEntry); jaros.putNextEntry(other); jaros.write(("hello " + otherEntry).getBytes(StandardCharsets.US_ASCII)); jaros.closeEntry(); } } return jarFile; } private static Path signJAR(final Path unsignedJAR) throws Exception { final Path signedJARDir = Files.createDirectories(Path.of(".").resolve("signed-jars")); final Path signedJAR = signedJARDir.resolve("signed-" + unsignedJAR.getFileName()); final String[] cmd = new String[] { "jarsigner", "-keystore", KEYSTORE.toString(), "-storepass", PASSPHRASE, "-signedjar", signedJAR.toString(), unsignedJAR.toString(), ALIAS }; final ProcessBuilder pb = new ProcessBuilder(); pb.command(cmd); pb.inheritIO(); final Process p = pb.start(); final int exitCode = p.waitFor(); if (exitCode != 0) { System.err.println(Arrays.toString(cmd) + " exit code: " + exitCode); throw new RuntimeException("failed to sign jar " + unsignedJAR + ", exit code: " + exitCode); } return signedJAR; } } ------------- PR Comment: https://git.openjdk.org/jdk/pull/24475#issuecomment-2782178725