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

Reply via email to