From 0451e5ab444f9a25cf31dc55be39dd54715ea77d Mon Sep 17 00:00:00 2001 From: David Beaumont Date: Tue, 28 Apr 2026 18:47:50 +0200 Subject: [PATCH 1/3] minimal but hacky changes for letting files be closed --- .../jdk/internal/jimage/BasicImageReader.java | 55 ++++++++++++++----- .../jdk/internal/jimage/ImageReader.java | 52 +++++++++--------- .../jdk/internal/jrtfs/JrtFileSystem.java | 9 ++- .../com/sun/tools/javac/code/ClassFinder.java | 22 +++++--- .../com/sun/tools/javac/file/JRTIndex.java | 12 +--- 5 files changed, 90 insertions(+), 60 deletions(-) diff --git a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java index 446d3576a0d..391d30da4cc 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java @@ -32,6 +32,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.IntBuffer; +import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -68,6 +69,7 @@ public Boolean run() { isSystemProperty("sun.arch.data.model", "64", "32"); private static final boolean USE_JVM_MAP = isSystemProperty("jdk.image.use.jvm.map", "true", "true"); + // Whether to map the entire file contents. private static final boolean MAP_ALL = isSystemProperty("jdk.image.map.all", "true", IS_64_BIT ? "true" : "false"); @@ -77,7 +79,7 @@ public Boolean run() { private final ByteBuffer memoryMap; private final FileChannel channel; private final ImageHeader header; - private final long indexSize; + private final int indexSize; private final IntBuffer redirect; private final IntBuffer offsets; private final ByteBuffer locations; @@ -92,13 +94,15 @@ protected BasicImageReader(Path path, ByteOrder byteOrder) this.byteOrder = Objects.requireNonNull(byteOrder); this.name = this.imagePath.toString(); + // Only the runtime image is loaded with the root class-loader. + final boolean isRuntimeImage = BasicImageReader.class.getClassLoader() == null; ByteBuffer map; - if (USE_JVM_MAP && BasicImageReader.class.getClassLoader() == null) { + if (USE_JVM_MAP && isRuntimeImage) { // Check to see if the jvm has opened the file using libjimage // native entry when loading the image for this runtime map = NativeImageBuffer.getNativeMap(name); - } else { + } else { map = null; } @@ -111,7 +115,7 @@ protected BasicImageReader(Path path, ByteOrder byteOrder) AccessController.doPrivileged(new PrivilegedAction() { @Override public Void run() { - if (BasicImageReader.class.getClassLoader() == null) { + if (isRuntimeImage) { try { Class fileChannelImpl = Class.forName("sun.nio.ch.FileChannelImpl"); @@ -134,33 +138,44 @@ public Void run() { // If no memory map yet and 64 bit jvm then memory map entire file if (MAP_ALL && map == null) { - map = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); + if (isRuntimeImage) { + map = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); + } else { + // Non-runtime images are used for compiler tasks, and we avoid + // memory mapping them so the file handles can be closed when the task + // completes (rather than having to wait for garbage collection). + // This is important when long-lived server processes compile code + // across multiple runtime images. Once this code no longer requires + // JDK-8, we might be able to switch to using MemorySegment here. + int imageFileSize = (int) channel.size(); + if ((long) imageFileSize != channel.size()) { + throw new IOException("\"" + name + "\" too large"); + } + map = readDirectBuffer(channel, imageFileSize, name); + } } - // Assume we have a memory map to read image file header + // headerBuffer is a temporary buffer for reading the heading (which + // may be the full buffer, or a scratch buffer we discard later). ByteBuffer headerBuffer = map; int headerSize = ImageHeader.getHeaderSize(); // If no memory map then read header from image file if (headerBuffer == null) { - headerBuffer = ByteBuffer.allocateDirect(headerSize); - if (channel.read(headerBuffer, 0L) == headerSize) { - headerBuffer.rewind(); - } else { - throw new IOException("\"" + name + "\" is not an image file"); - } + headerBuffer = readDirectBuffer(channel, headerSize, name); } else if (headerBuffer.capacity() < headerSize) { + // A fully mapped file cannot be smaller than the header size. throw new IOException("\"" + name + "\" is not an image file"); } - // Interpret the image file header + // Read the header and find the index size, which is the minimum buffer + // size we need to read if we haven't already mapped the entire file. header = readHeader(intBuffer(headerBuffer, 0, headerSize)); indexSize = header.getIndexSize(); // If no memory map yet then must be 32 bit jvm not previously mapped if (map == null) { - // Just map the image index - map = channel.map(FileChannel.MapMode.READ_ONLY, 0, indexSize); + map = readDirectBuffer(channel, indexSize, name); } memoryMap = map.asReadOnlyBuffer(); @@ -178,6 +193,16 @@ public Void run() { decompressor = new Decompressor(); } + private static ByteBuffer readDirectBuffer(FileChannel channel, int size, String name) + throws IOException { + ByteBuffer map = ByteBuffer.allocateDirect(size); + if (channel.read(map, 0L) != size) { + throw new IOException("\"" + name + "\" is not an image file"); + } + map.rewind(); + return map; + } + protected BasicImageReader(Path imagePath) throws IOException { this(imagePath, ByteOrder.nativeOrder()); } diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java index 5cab6055022..980b0648346 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java @@ -27,6 +27,7 @@ import jdk.internal.jimage.ImageLocation.LocationType; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.IntBuffer; @@ -44,6 +45,7 @@ import java.util.Objects; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; @@ -89,12 +91,11 @@ * to the jimage file provided by the shipped JDK by tools running on JDK 8. */ public final class ImageReader implements AutoCloseable { - private final SharedImageReader reader; - - private volatile boolean closed; + // Set to null when closed to allow GC of underlying buffers to unmap files. + private final AtomicReference reader; private ImageReader(SharedImageReader reader) { - this.reader = reader; + this.reader = new AtomicReference<>(reader); } /** @@ -123,23 +124,19 @@ public static ImageReader open(Path imagePath, ByteOrder byteOrder, PreviewMode @Override public void close() throws IOException { - if (closed) { + SharedImageReader r = reader.getAndSet(null); + if (r == null) { throw new IOException("image file already closed"); } - reader.close(this); - closed = true; + r.close(this); } - private void ensureOpen() throws IOException { - if (closed) { + private SharedImageReader ensureOpen() throws IOException { + SharedImageReader r = reader.get(); + if (r == null) { throw new IOException("image file closed"); } - } - - private void requireOpen() { - if (closed) { - throw new IllegalStateException("image file closed"); - } + return r; } /** @@ -150,8 +147,7 @@ private void requireOpen() { * @return a node representing a resource, directory or symbolic link. */ public Node findNode(String name) throws IOException { - ensureOpen(); - return reader.findNode(name); + return ensureOpen().findNode(name); } /** @@ -170,8 +166,7 @@ public Node findNode(String name) throws IOException { */ public Node findResourceNode(String moduleName, String resourcePath) throws IOException { - ensureOpen(); - return reader.findResourceNode(moduleName, resourcePath); + return ensureOpen().findResourceNode(moduleName, resourcePath); } /** @@ -189,8 +184,7 @@ public Node findResourceNode(String moduleName, String resourcePath) */ public boolean containsResource(String moduleName, String resourcePath) throws IOException { - ensureOpen(); - return reader.containsResource(moduleName, resourcePath); + return ensureOpen().containsResource(moduleName, resourcePath); } /** @@ -202,24 +196,30 @@ public boolean containsResource(String moduleName, String resourcePath) * given node is not a resource node). */ public byte[] getResource(Node node) throws IOException { - ensureOpen(); - return reader.getResource(node); + return ensureOpen().getResource(node); } /** * Returns the content of a resource node in a newly allocated byte buffer. */ public ByteBuffer getResourceBuffer(Node node) { - requireOpen(); if (!node.isResource()) { throw new IllegalArgumentException("Not a resource node: " + node); } - return reader.getResourceBuffer(node.getLocation()); + try { + return ensureOpen().getResourceBuffer(node.getLocation()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } // Package protected for use only by SystemImageReader. ResourceEntries getResourceEntries() { - return reader.getResourceEntries(); + try { + return ensureOpen().getResourceEntries(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } private static final class SharedImageReader extends BasicImageReader { diff --git a/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java b/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java index 4d739d1e200..69d327678b1 100644 --- a/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java +++ b/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,6 +28,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.URLClassLoader; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; @@ -313,6 +314,12 @@ synchronized void cleanup() throws IOException { isOpen = false; image.close(); image = null; + + // Closes the jrt-fs.jar !!! + ClassLoader cl = provider.getClass().getClassLoader(); + if (cl instanceof URLClassLoader) { + ((URLClassLoader) cl).close(); + } } } diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/ClassFinder.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/ClassFinder.java index bfdde1293ee..150ff2c4577 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/ClassFinder.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/ClassFinder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,7 +26,9 @@ package com.sun.tools.javac.code; import java.io.IOException; +import java.nio.file.FileSystemNotFoundException; import java.nio.file.Path; +import java.nio.file.ProviderNotFoundException; import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; @@ -217,14 +219,20 @@ protected ClassFinder(Context context) { } else { useCtProps = false; } - if (useCtProps && JRTIndex.isAvailable()) { + JRTIndex index = null; + if (useCtProps) { Preview preview = Preview.instance(context); - JavaCompiler comp = JavaCompiler.instance(context); - jrtIndex = JRTIndex.instance(preview.isEnabled()); - comp.closeables = comp.closeables.prepend(jrtIndex); - } else { - jrtIndex = null; + try { + index = JRTIndex.instance(preview.isEnabled()); + } catch (ProviderNotFoundException | FileSystemNotFoundException e) { + // Leave index null. + } + if (index != null) { + JavaCompiler comp = JavaCompiler.instance(context); + comp.closeables = comp.closeables.prepend(index); + } } + jrtIndex = index; profile = Profile.instance(context); cachedCompletionFailure = new CompletionFailure(null, () -> null, dcfh); diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/file/JRTIndex.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/file/JRTIndex.java index 1688a03d19f..0a9625ee47e 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/file/JRTIndex.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/file/JRTIndex.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -98,16 +98,6 @@ public static JRTIndex instance(boolean previewMode) { } } - /** {@return whether the JRT file-system is available to create an index} */ - public static boolean isAvailable() { - try { - FileSystems.getFileSystem(URI.create("jrt:/")); - return true; - } catch (ProviderNotFoundException | FileSystemNotFoundException e) { - return false; - } - } - /** * Underlying file system resources potentially shared between many indexes. * From ba594709bb865c1847df16e8083c453404e16226 Mon Sep 17 00:00:00 2001 From: David Beaumont Date: Wed, 29 Apr 2026 19:44:11 +0200 Subject: [PATCH 2/3] avoid copying complete buffer and tidy code --- .../jdk/internal/jimage/BasicImageReader.java | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java index 391d30da4cc..fe10ea8c9b5 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java @@ -32,7 +32,6 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.IntBuffer; -import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -69,22 +68,32 @@ public Boolean run() { isSystemProperty("sun.arch.data.model", "64", "32"); private static final boolean USE_JVM_MAP = isSystemProperty("jdk.image.use.jvm.map", "true", "true"); - // Whether to map the entire file contents. - private static final boolean MAP_ALL = + // Whether to map the entire file contents for runtime images. + private static final boolean FULLY_MAP_RUNTIME_IMAGE = isSystemProperty("jdk.image.map.all", "true", IS_64_BIT ? "true" : "false"); private final Path imagePath; private final ByteOrder byteOrder; private final String name; + // A buffer holding either the entire image file (memory mapped), or just + // the index data (copied). Only the runtime image can be fully mapped, + // but it might not be (see FULLY_MAP_RUNTIME_IMAGE). private final ByteBuffer memoryMap; + // Whether memoryMap contains the complete file content or just the index. + private final boolean isFullyMapped; + // Channel from which file data is loaded (null if using a native buffer). private final FileChannel channel; + // Header information parsed from the start of the image file. private final ImageHeader header; + // Size (bytes) of the index region at the start of the image file. private final int indexSize; + // Sliced views taken from the index region of memoryMap. private final IntBuffer redirect; private final IntBuffer offsets; private final ByteBuffer locations; private final ByteBuffer strings; private final ImageStringsReader stringsReader; + // Decompressor for resource entries. private final Decompressor decompressor; @SuppressWarnings({ "removal", "this-escape", "suppression" }) @@ -107,7 +116,8 @@ protected BasicImageReader(Path path, ByteOrder byteOrder) } // Open the file only if no memory map yet or is 32 bit jvm - if (map != null && MAP_ALL) { + if (map != null && FULLY_MAP_RUNTIME_IMAGE) { + // No channel needed if we have the fully mapped native buffer. channel = null; } else { channel = FileChannel.open(imagePath, StandardOpenOption.READ); @@ -136,23 +146,10 @@ public Void run() { }); } - // If no memory map yet and 64 bit jvm then memory map entire file - if (MAP_ALL && map == null) { - if (isRuntimeImage) { - map = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); - } else { - // Non-runtime images are used for compiler tasks, and we avoid - // memory mapping them so the file handles can be closed when the task - // completes (rather than having to wait for garbage collection). - // This is important when long-lived server processes compile code - // across multiple runtime images. Once this code no longer requires - // JDK-8, we might be able to switch to using MemorySegment here. - int imageFileSize = (int) channel.size(); - if ((long) imageFileSize != channel.size()) { - throw new IOException("\"" + name + "\" too large"); - } - map = readDirectBuffer(channel, imageFileSize, name); - } + // If no memory map yet and 64 bit jvm then memory map the runtime file. + isFullyMapped = isRuntimeImage && FULLY_MAP_RUNTIME_IMAGE; + if (map == null && isFullyMapped) { + map = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); } // headerBuffer is a temporary buffer for reading the heading (which @@ -162,7 +159,7 @@ public Void run() { // If no memory map then read header from image file if (headerBuffer == null) { - headerBuffer = readDirectBuffer(channel, headerSize, name); + headerBuffer = copyBuffer(headerSize); } else if (headerBuffer.capacity() < headerSize) { // A fully mapped file cannot be smaller than the header size. throw new IOException("\"" + name + "\" is not an image file"); @@ -175,7 +172,7 @@ public Void run() { // If no memory map yet then must be 32 bit jvm not previously mapped if (map == null) { - map = readDirectBuffer(channel, indexSize, name); + map = copyBuffer(indexSize); } memoryMap = map.asReadOnlyBuffer(); @@ -193,16 +190,6 @@ public Void run() { decompressor = new Decompressor(); } - private static ByteBuffer readDirectBuffer(FileChannel channel, int size, String name) - throws IOException { - ByteBuffer map = ByteBuffer.allocateDirect(size); - if (channel.read(map, 0L) != size) { - throw new IOException("\"" + name + "\" is not an image file"); - } - map.rewind(); - return map; - } - protected BasicImageReader(Path imagePath) throws IOException { this(imagePath, ByteOrder.nativeOrder()); } @@ -427,6 +414,22 @@ private byte[] getBufferBytes(ByteBuffer buffer) { return bytes; } + /** + * Reads the image file to return a newly allocated buffer. + */ + private ByteBuffer copyBuffer(int size) + throws IOException { + ByteBuffer map = ByteBuffer.allocate(size); + if (channel.read(map, 0L) != size) { + throw new IOException("\"" + name + "\" is not an image file"); + } + map.rewind(); + return map; + } + + /** + * Reads entry data either from the mapped buffer, or directly from the channel. + */ private ByteBuffer readBuffer(long offset, long size) { if (offset < 0 || Integer.MAX_VALUE <= offset) { throw new IndexOutOfBoundsException("Bad offset: " + offset); @@ -438,7 +441,7 @@ private ByteBuffer readBuffer(long offset, long size) { } int checkedSize = (int) size; - if (MAP_ALL) { + if (isFullyMapped) { ByteBuffer buffer = slice(memoryMap, checkedOffset, checkedSize); buffer.order(ByteOrder.BIG_ENDIAN); From 5935eb403d36e7a13a0a9323811718875669b977 Mon Sep 17 00:00:00 2001 From: David Beaumont Date: Thu, 30 Apr 2026 10:42:07 +0200 Subject: [PATCH 3/3] rewrite buffer handling for clarity --- .../jdk/internal/jimage/BasicImageReader.java | 185 ++++++++---------- 1 file changed, 84 insertions(+), 101 deletions(-) diff --git a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java index fe10ea8c9b5..274b577a4f0 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java @@ -75,12 +75,8 @@ public Boolean run() { private final Path imagePath; private final ByteOrder byteOrder; private final String name; - // A buffer holding either the entire image file (memory mapped), or just - // the index data (copied). Only the runtime image can be fully mapped, - // but it might not be (see FULLY_MAP_RUNTIME_IMAGE). + // The memory mapped image file (only used for the runtime image). private final ByteBuffer memoryMap; - // Whether memoryMap contains the complete file content or just the index. - private final boolean isFullyMapped; // Channel from which file data is loaded (null if using a native buffer). private final FileChannel channel; // Header information parsed from the start of the image file. @@ -96,7 +92,15 @@ public Boolean run() { // Decompressor for resource entries. private final Decompressor decompressor; - @SuppressWarnings({ "removal", "this-escape", "suppression" }) + public static BasicImageReader open(Path imagePath) throws IOException { + return new BasicImageReader(imagePath, ByteOrder.nativeOrder()); + } + + protected BasicImageReader(Path imagePath) throws IOException { + this(imagePath, ByteOrder.nativeOrder()); + } + + @SuppressWarnings({ "this-escape", "suppression" }) protected BasicImageReader(Path path, ByteOrder byteOrder) throws IOException { this.imagePath = Objects.requireNonNull(path); @@ -104,98 +108,90 @@ protected BasicImageReader(Path path, ByteOrder byteOrder) this.name = this.imagePath.toString(); // Only the runtime image is loaded with the root class-loader. - final boolean isRuntimeImage = BasicImageReader.class.getClassLoader() == null; - ByteBuffer map; - - if (USE_JVM_MAP && isRuntimeImage) { - // Check to see if the jvm has opened the file using libjimage - // native entry when loading the image for this runtime - map = NativeImageBuffer.getNativeMap(name); - } else { - map = null; - } + boolean isRuntimeImage = BasicImageReader.class.getClassLoader() == null; - // Open the file only if no memory map yet or is 32 bit jvm - if (map != null && FULLY_MAP_RUNTIME_IMAGE) { - // No channel needed if we have the fully mapped native buffer. - channel = null; - } else { - channel = FileChannel.open(imagePath, StandardOpenOption.READ); - // No lambdas during bootstrap - AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Void run() { - if (isRuntimeImage) { - try { - Class fileChannelImpl = - Class.forName("sun.nio.ch.FileChannelImpl"); - Method setUninterruptible = - fileChannelImpl.getMethod("setUninterruptible"); - setUninterruptible.invoke(channel); - } catch (ClassNotFoundException | - NoSuchMethodException | - IllegalAccessException | - InvocationTargetException ex) { - // fall thru - will only happen on JDK-8 systems where this code - // is only used by tools using jrt-fs (non-critical.) - } - } + // Check to see if the jvm has opened the file using libjimage + // native entry when loading the image for this runtime + ByteBuffer map = + (USE_JVM_MAP && isRuntimeImage) ? NativeImageBuffer.getNativeMap(name) : null; - return null; - } - }); - } + // Open the file channel if we don't have a native memory map. + this.channel = (map == null) ? openFileChannel(imagePath, isRuntimeImage) : null; - // If no memory map yet and 64 bit jvm then memory map the runtime file. - isFullyMapped = isRuntimeImage && FULLY_MAP_RUNTIME_IMAGE; - if (map == null && isFullyMapped) { + // Manually map the entire runtime image (if configured to). + if (map == null && isRuntimeImage && FULLY_MAP_RUNTIME_IMAGE) { map = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); } - // headerBuffer is a temporary buffer for reading the heading (which - // may be the full buffer, or a scratch buffer we discard later). - ByteBuffer headerBuffer = map; - int headerSize = ImageHeader.getHeaderSize(); + // Either we have the entire file mapped and use it for all content, + // or we load the index separately and read entries on demand. + this.memoryMap = map != null ? map.asReadOnlyBuffer() : null; - // If no memory map then read header from image file - if (headerBuffer == null) { - headerBuffer = copyBuffer(headerSize); - } else if (headerBuffer.capacity() < headerSize) { - // A fully mapped file cannot be smaller than the header size. - throw new IOException("\"" + name + "\" is not an image file"); - } - - // Read the header and find the index size, which is the minimum buffer - // size we need to read if we haven't already mapped the entire file. - header = readHeader(intBuffer(headerBuffer, 0, headerSize)); - indexSize = header.getIndexSize(); - - // If no memory map yet then must be 32 bit jvm not previously mapped - if (map == null) { - map = copyBuffer(indexSize); - } - - memoryMap = map.asReadOnlyBuffer(); + // Read the header and find the index size, which is the minimum we need + // to copy if we haven't already mapped the entire file. + int headerSize = ImageHeader.getHeaderSize(); + this.header = readHeader(intBuffer(getOrCopyBuffer(headerSize), 0, headerSize)); + this.indexSize = header.getIndexSize(); - // Interpret the image index - if (memoryMap.capacity() < indexSize) { - throw new IOException("The image file \"" + name + "\" is corrupted"); - } - redirect = intBuffer(memoryMap, header.getRedirectOffset(), header.getRedirectSize()); - offsets = intBuffer(memoryMap, header.getOffsetsOffset(), header.getOffsetsSize()); - locations = slice(memoryMap, header.getLocationsOffset(), header.getLocationsSize()); - strings = slice(memoryMap, header.getStringsOffset(), header.getStringsSize()); + // Now we have the index size, get the complete index buffer and slice it. + ByteBuffer indexBuffer = getOrCopyBuffer(indexSize); + this.redirect = intBuffer(indexBuffer, header.getRedirectOffset(), header.getRedirectSize()); + this.offsets = intBuffer(indexBuffer, header.getOffsetsOffset(), header.getOffsetsSize()); + this.locations = slice(indexBuffer, header.getLocationsOffset(), header.getLocationsSize()); + this.strings = slice(indexBuffer, header.getStringsOffset(), header.getStringsSize()); - stringsReader = new ImageStringsReader(this); - decompressor = new Decompressor(); + this.stringsReader = new ImageStringsReader(this); + this.decompressor = new Decompressor(); } - protected BasicImageReader(Path imagePath) throws IOException { - this(imagePath, ByteOrder.nativeOrder()); + @SuppressWarnings({ "removal", "suppression" }) + private static FileChannel openFileChannel(Path imagePath, boolean isRuntimeImage) throws IOException { + final FileChannel channel; + channel = FileChannel.open(imagePath, StandardOpenOption.READ); + // No lambdas during bootstrap + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Void run() { + if (isRuntimeImage) { + try { + Class fileChannelImpl = + Class.forName("sun.nio.ch.FileChannelImpl"); + Method setUninterruptible = + fileChannelImpl.getMethod("setUninterruptible"); + setUninterruptible.invoke(channel); + } catch (ClassNotFoundException | + NoSuchMethodException | + IllegalAccessException | + InvocationTargetException ex) { + // fall thru - will only happen on JDK-8 systems where this code + // is only used by tools using jrt-fs (non-critical.) + } + } + return null; + } + }); + return channel; } - public static BasicImageReader open(Path imagePath) throws IOException { - return new BasicImageReader(imagePath, ByteOrder.nativeOrder()); + /** + * Gets a buffer from the start of the image file of at least the given size. + * If possible this method just returns the memory mapped file buffer. + */ + private ByteBuffer getOrCopyBuffer(int trustedSize) throws IOException { + if (memoryMap != null) { + if (memoryMap.capacity() < trustedSize) { + throw new IOException("\"" + name + "\" is not an image file"); + } + return memoryMap; + } else { + // Channel must be non-null if there's no memory map. + ByteBuffer buffer = ByteBuffer.allocate(trustedSize); + if (channel.read(buffer, 0L) != trustedSize) { + throw new IOException("\"" + name + "\" is not an image file"); + } + buffer.rewind(); + return buffer; + } } public ImageHeader getHeader() { @@ -415,20 +411,7 @@ private byte[] getBufferBytes(ByteBuffer buffer) { } /** - * Reads the image file to return a newly allocated buffer. - */ - private ByteBuffer copyBuffer(int size) - throws IOException { - ByteBuffer map = ByteBuffer.allocate(size); - if (channel.read(map, 0L) != size) { - throw new IOException("\"" + name + "\" is not an image file"); - } - map.rewind(); - return map; - } - - /** - * Reads entry data either from the mapped buffer, or directly from the channel. + * Reads entry data either from the mapped buffer, or copied from the channel. */ private ByteBuffer readBuffer(long offset, long size) { if (offset < 0 || Integer.MAX_VALUE <= offset) { @@ -441,12 +424,12 @@ private ByteBuffer readBuffer(long offset, long size) { } int checkedSize = (int) size; - if (isFullyMapped) { + if (memoryMap != null) { ByteBuffer buffer = slice(memoryMap, checkedOffset, checkedSize); buffer.order(ByteOrder.BIG_ENDIAN); - return buffer; } else { + // If there's no memory map, there must be a file channel. if (channel == null) { throw new InternalError("Image file channel not open"); }