RFR: 8353787: Increased number of SHA-384-Digest java.util.jar.Attributes$Name instances leading to higher memory footprint
Jaikiran Pai
jpai at openjdk.org
Mon Apr 7 06:42:53 UTC 2025
On Mon, 7 Apr 2025 06:34:11 GMT, Jaikiran Pai <jpai at 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 at 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 at 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
More information about the core-libs-dev
mailing list