Skip to content

Java 17: resource.isReadable() with concurrency leaks large amounts of non-heap memory #30955

Closed
@peterdnight

Description

@peterdnight

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;
    }

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)type: enhancementA general enhancement

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions