Skip to content

Commit e2368b9

Browse files
committed
Reduce memory consumption of fat/exploded jars
Refactor `spring-boot-loader` to reduce the amount of memory required to load fat & exploded jars. Jar files now no longer store a full list of entry data records, but instead use an array of entry name hashes. Since ClassLoaders often ask each JAR if they contain a particular entry (and mostly they do not), the hash array provides a quick way to deal with misses. Only when a hash does exist is data actually loaded from the underlying file. In addition to the JarFile changes, the Archive abstraction has also been updated to reduce memory consumption. See gh-4882
1 parent 858a854 commit e2368b9

24 files changed

+1132
-785
lines changed

spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.List;
2525
import java.util.Set;
2626
import java.util.jar.JarEntry;
27+
import java.util.jar.Manifest;
2728

2829
import org.springframework.boot.loader.archive.Archive;
2930
import org.springframework.boot.loader.archive.Archive.Entry;
@@ -66,7 +67,16 @@ protected final Archive getArchive() {
6667

6768
@Override
6869
protected String getMainClass() throws Exception {
69-
return this.archive.getMainClass();
70+
Manifest manifest = this.archive.getManifest();
71+
String mainClass = null;
72+
if (manifest != null) {
73+
mainClass = manifest.getMainAttributes().getValue("Start-Class");
74+
}
75+
if (mainClass == null) {
76+
throw new IllegalStateException(
77+
"No 'Start-Class' manifest entry specified in " + this);
78+
}
79+
return mainClass;
7080
}
7181

7282
@Override

spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.util.List;
2020

2121
import org.springframework.boot.loader.archive.Archive;
22-
import org.springframework.boot.loader.util.AsciiBytes;
2322

2423
/**
2524
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
@@ -29,7 +28,7 @@
2928
*/
3029
public class JarLauncher extends ExecutableArchiveLauncher {
3130

32-
private static final AsciiBytes LIB = new AsciiBytes("lib/");
31+
private static final String LIB = "lib/";
3332

3433
public JarLauncher() {
3534
}

spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,20 +204,15 @@ private void definePackageForFindClass(final String name, final String packageNa
204204
AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
205205
@Override
206206
public Object run() throws ClassNotFoundException {
207-
String path = name.replace('.', '/').concat(".class");
208207
for (URL url : getURLs()) {
209208
try {
210209
if (url.getContent() instanceof JarFile) {
211210
JarFile jarFile = (JarFile) url.getContent();
212-
// Check the jar entry data before needlessly creating the
213-
// manifest
214-
if (jarFile.getJarEntryData(path) != null
215-
&& jarFile.getManifest() != null) {
211+
if (jarFile.getManifest() != null) {
216212
definePackage(packageName, jarFile.getManifest(),
217213
url);
218214
return null;
219215
}
220-
221216
}
222217
}
223218
catch (IOException ex) {

spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java

Lines changed: 60 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@
2121
import java.io.IOException;
2222
import java.io.InputStream;
2323
import java.net.HttpURLConnection;
24+
import java.net.MalformedURLException;
2425
import java.net.URISyntaxException;
2526
import java.net.URL;
2627
import java.net.URLClassLoader;
2728
import java.net.URLConnection;
2829
import java.net.URLDecoder;
2930
import java.util.ArrayList;
30-
import java.util.Arrays;
3131
import java.util.Collections;
32+
import java.util.Iterator;
3233
import java.util.List;
3334
import java.util.Properties;
3435
import java.util.jar.Manifest;
@@ -39,9 +40,7 @@
3940
import org.springframework.boot.loader.archive.Archive.Entry;
4041
import org.springframework.boot.loader.archive.Archive.EntryFilter;
4142
import org.springframework.boot.loader.archive.ExplodedArchive;
42-
import org.springframework.boot.loader.archive.FilteredArchive;
4343
import org.springframework.boot.loader.archive.JarFileArchive;
44-
import org.springframework.boot.loader.util.AsciiBytes;
4544
import org.springframework.boot.loader.util.SystemPropertyUtils;
4645

4746
/**
@@ -122,8 +121,6 @@ public class PropertiesLauncher extends Launcher {
122121
*/
123122
public static final String SET_SYSTEM_PROPERTIES = "loader.system";
124123

125-
private static final List<String> DEFAULT_PATHS = Arrays.asList();
126-
127124
private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+");
128125

129126
private static final URL[] EMPTY_URLS = {};
@@ -132,7 +129,7 @@ public class PropertiesLauncher extends Launcher {
132129

133130
private final JavaAgentDetector javaAgentDetector;
134131

135-
private List<String> paths = new ArrayList<String>(DEFAULT_PATHS);
132+
private List<String> paths = new ArrayList<String>();
136133

137134
private final Properties properties = new Properties();
138135

@@ -168,7 +165,6 @@ private void initializeProperties(File home) throws Exception, IOException {
168165
config = SystemPropertyUtils.resolvePlaceholders(
169166
SystemPropertyUtils.getProperty(CONFIG_LOCATION, config));
170167
InputStream resource = getResource(config);
171-
172168
if (resource != null) {
173169
log("Found: " + config);
174170
try {
@@ -353,7 +349,6 @@ protected ClassLoader createClassLoader(List<Archive> archives) throws Exception
353349
@SuppressWarnings("unchecked")
354350
private ClassLoader wrapWithCustomClassLoader(ClassLoader parent,
355351
String loaderClassName) throws Exception {
356-
357352
Class<ClassLoader> loaderClass = (Class<ClassLoader>) Class
358353
.forName(loaderClassName, true, parent);
359354

@@ -363,15 +358,13 @@ private ClassLoader wrapWithCustomClassLoader(ClassLoader parent,
363358
catch (NoSuchMethodException ex) {
364359
// Ignore and try with URLs
365360
}
366-
367361
try {
368362
return loaderClass.getConstructor(URL[].class, ClassLoader.class)
369363
.newInstance(new URL[0], parent);
370364
}
371365
catch (NoSuchMethodException ex) {
372366
// Ignore and try without any arguments
373367
}
374-
375368
return loaderClass.newInstance();
376369
}
377370

@@ -384,21 +377,18 @@ private String getProperty(String propertyKey, String manifestKey) throws Except
384377
manifestKey = propertyKey.replace(".", "-");
385378
manifestKey = toCamelCase(manifestKey);
386379
}
387-
388380
String property = SystemPropertyUtils.getProperty(propertyKey);
389381
if (property != null) {
390382
String value = SystemPropertyUtils.resolvePlaceholders(property);
391383
log("Property '" + propertyKey + "' from environment: " + value);
392384
return value;
393385
}
394-
395386
if (this.properties.containsKey(propertyKey)) {
396387
String value = SystemPropertyUtils
397388
.resolvePlaceholders(this.properties.getProperty(propertyKey));
398389
log("Property '" + propertyKey + "' from properties: " + value);
399390
return value;
400391
}
401-
402392
try {
403393
// Prefer home dir for MANIFEST if there is one
404394
Manifest manifest = new ExplodedArchive(this.home, false).getManifest();
@@ -412,7 +402,6 @@ private String getProperty(String propertyKey, String manifestKey) throws Except
412402
catch (IllegalStateException ex) {
413403
// Ignore
414404
}
415-
416405
// Otherwise try the parent archive
417406
Manifest manifest = createArchive().getManifest();
418407
if (manifest != null) {
@@ -478,7 +467,7 @@ private Archive getArchive(File file) throws IOException {
478467
return null;
479468
}
480469

481-
private Archive getNestedArchive(final String root) throws Exception {
470+
private Archive getNestedArchive(String root) throws Exception {
482471
if (root.startsWith("/")
483472
|| this.parent.getUrl().equals(this.home.toURI().toURL())) {
484473
// If home dir is same as parent archive, no need to add it twice.
@@ -628,40 +617,84 @@ private void log(String message) {
628617
}
629618
}
630619

620+
/**
621+
* Convenience class for finding nested archives that have a prefix in their file path
622+
* (e.g. "lib/").
623+
*/
624+
private static final class PrefixMatchingArchiveFilter implements EntryFilter {
625+
626+
private final String prefix;
627+
628+
private final ArchiveEntryFilter filter = new ArchiveEntryFilter();
629+
630+
private PrefixMatchingArchiveFilter(String prefix) {
631+
this.prefix = prefix;
632+
}
633+
634+
@Override
635+
public boolean matches(Entry entry) {
636+
return entry.getName().startsWith(this.prefix) && this.filter.matches(entry);
637+
}
638+
639+
}
640+
631641
/**
632642
* Convenience class for finding nested archives (archive entries that can be
633643
* classpath entries).
634644
*/
635645
private static final class ArchiveEntryFilter implements EntryFilter {
636646

637-
private static final AsciiBytes DOT_JAR = new AsciiBytes(".jar");
647+
private static final String DOT_JAR = ".jar";
638648

639-
private static final AsciiBytes DOT_ZIP = new AsciiBytes(".zip");
649+
private static final String DOT_ZIP = ".zip";
640650

641651
@Override
642652
public boolean matches(Entry entry) {
643653
return entry.getName().endsWith(DOT_JAR) || entry.getName().endsWith(DOT_ZIP);
644654
}
655+
645656
}
646657

647658
/**
648-
* Convenience class for finding nested archives that have a prefix in their file path
649-
* (e.g. "lib/").
659+
* Decorator to apply an {@link Archive.EntryFilter} to an existing {@link Archive}.
650660
*/
651-
private static final class PrefixMatchingArchiveFilter implements EntryFilter {
661+
private static class FilteredArchive implements Archive {
652662

653-
private final AsciiBytes prefix;
663+
private final Archive parent;
654664

655-
private final ArchiveEntryFilter filter = new ArchiveEntryFilter();
665+
private final EntryFilter filter;
656666

657-
private PrefixMatchingArchiveFilter(String prefix) {
658-
this.prefix = new AsciiBytes(prefix);
667+
FilteredArchive(Archive parent, EntryFilter filter) {
668+
this.parent = parent;
669+
this.filter = filter;
659670
}
660671

661672
@Override
662-
public boolean matches(Entry entry) {
663-
return entry.getName().startsWith(this.prefix) && this.filter.matches(entry);
673+
public URL getUrl() throws MalformedURLException {
674+
return this.parent.getUrl();
675+
}
676+
677+
@Override
678+
public Manifest getManifest() throws IOException {
679+
return this.parent.getManifest();
680+
}
681+
682+
@Override
683+
public Iterator<Entry> iterator() {
684+
throw new UnsupportedOperationException();
685+
}
686+
687+
@Override
688+
public List<Archive> getNestedArchives(final EntryFilter filter)
689+
throws IOException {
690+
return this.parent.getNestedArchives(new EntryFilter() {
691+
@Override
692+
public boolean matches(Entry entry) {
693+
return FilteredArchive.this.filter.matches(entry)
694+
&& filter.matches(entry);
695+
}
696+
});
664697
}
665-
}
666698

699+
}
667700
}

spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.boot.loader;
1818

1919
import org.springframework.boot.loader.archive.Archive;
20-
import org.springframework.boot.loader.util.AsciiBytes;
2120

2221
/**
2322
* {@link Launcher} for WAR based archives. This launcher for standard WAR archives.
@@ -29,14 +28,13 @@
2928
*/
3029
public class WarLauncher extends ExecutableArchiveLauncher {
3130

32-
private static final AsciiBytes WEB_INF = new AsciiBytes("WEB-INF/");
31+
private static final String WEB_INF = "WEB-INF/";
3332

34-
private static final AsciiBytes WEB_INF_CLASSES = WEB_INF.append("classes/");
33+
private static final String WEB_INF_CLASSES = WEB_INF + "classes/";
3534

36-
private static final AsciiBytes WEB_INF_LIB = WEB_INF.append("lib/");
35+
private static final String WEB_INF_LIB = WEB_INF + "lib/";
3736

38-
private static final AsciiBytes WEB_INF_LIB_PROVIDED = WEB_INF
39-
.append("lib-provided/");
37+
private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/";
4038

4139
public WarLauncher() {
4240
super();

0 commit comments

Comments
 (0)