Closed
Description
Scenario:
- converting application from java 8 to java 17 a large increase in non heap memory occurred.
- determined critical region: during startup, application scans all classes looking for annotations. This is based on configuration settings, different threads initiate the scan for respective Annotations
- Prior to scanning the class - resource.isReadable is run.
- commenting out resource.isReadable check resolved issue.
- putting the application method (findAnnotation) in a synchronized block also resolved the issue
To reproduce:
- add the controller snippet below into any Spring RestController
- verified on both SpringBoot 2.x and 3.x (but no dependencies on SpringBoot)
- verified using threads = 20, iterations = 10 on a 50 core bare metal centos 9 host
- ideally there are many largish jars (verified with 90+), but increasing iteration count should expose the issue.
- verified on the latest java 17, with a smaller heap (-Xmx1G) to clearly identify non-heap memory
- a single invokation of /javaNativeMemory?threads=20&iterations=10 showed peak of 35g allocation, that settles on ~20G that is never released
Stacktrace:
[ 0] /usr/lib64/libz.so.1.2.7
[ 1] [unknown]
[ 2] java.util.zip.Inflater.inflateBytesBytes
[ 3] java.util.zip.Inflater.inflate
[ 4] java.util.zip.InflaterInputStream.read
[ 5] java.io.InputStream.readNBytes
[ 6] java.util.jar.JarFile.getBytes
[ 7] java.util.jar.JarFile.checkForSpecialAttributes
[ 8] java.util.jar.JarFile.isMultiRelease
[ 9] java.util.jar.JarFile.getEntry
[10] sun.net.www.protocol.jar.URLJarFile.getEntry
[11] sun.net.www.protocol.jar.JarURLConnection.connect
[12] sun.net.www.protocol.jar.JarURLConnection.getContentLengthLong
[13] org.springframework.core.io.AbstractFileResolvingResource.checkReadable
[14] org.springframework.core.io.AbstractFileResolvingResource.isReadable
Controller:
String SEARCH_PACKAGE = ""; // com, org.sample, ...
final String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + SEARCH_PACKAGE + "/**/*.class";
ResourceLoader RESOURCE_LOADER = new DefaultResourceLoader(ApiRoot.class.getClassLoader());
@GetMapping("/native-memory/run")
public ObjectNode javaNativeMemory(
int threads,
int iterations) {
logger.info("running using: {} threads and invoking each: {} iterations", threads, iterations);
ObjectNode report = jacksonMapper.createObjectNode();
report.put("started", LocalDateTime.now().format(DateTimeFormatter.ofPattern("hh:mm:ss")));
report.put("duration", -1);
Timer.Sample theTimer = metricUtilities.startTimer();
try {
Resource[] resources = new PathMatchingResourcePatternResolver(RESOURCE_LOADER).getResources(packageSearchPath);
int loaded = resources.length;
// isReadable is expensive
// - triggering deep call stack to zip.inflate,
// - allocates libz.so native memory, and leaks on zing ( thread concurrency )
ForkJoinPool customThreadPool = new ForkJoinPool(threads);
List<Integer> listOfNumbers = IntStream.range(0, threads)
.boxed()
.collect(Collectors.toList());
customThreadPool.submit(() ->
listOfNumbers.parallelStream().forEach(number -> {
IntStream.range(0, iterations).forEach(count -> {
logger.info("started: {} , name: {}", count, Thread.currentThread().getName());
for (final Resource resource : resources) {
if (resource.isReadable()) {
// do nothing
}
}
logger.info("completed: {} , name: {}", count, Thread.currentThread().getName());
});
})
);
customThreadPool.shutdown();
customThreadPool.awaitTermination(2, TimeUnit.MINUTES);
report.put("resourcesLocated", loaded);
report.put("totalChecked", loaded * iterations * threads);
} catch (final Exception e) {
logger.warn( "Failure while invoking resource.isReadable() {}", e) ;
}
long timeNanos = metricUtilities.stopTimer(theTimer, "csap.heap-tester.consume-heap");
report.put("duration", CSAP.autoFormatNanos(timeNanos));
doGc();
long freeBytes = Runtime.getRuntime().freeMemory();
report.put("heap-post-gc", CSAP.printBytesWithUnits(freeBytes));
return report;
}