diff a/src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java b/src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java
--- a/src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java
+++ b/src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2014, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2025, 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
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.  Oracle designates this
@@ -24,29 +24,185 @@
  */
 
 package jdk.internal.jimage;
 
 import java.nio.ByteBuffer;
+import java.util.List;
 import java.util.Objects;
+import java.util.function.Predicate;
 
 /**
  * @implNote This class needs to maintain JDK 8 source compatibility.
  *
  * It is used internally in the JDK to implement jimage/jrtfs access,
  * but also compiled and delivered as part of the jrtfs.jar to support access
  * to the jimage file provided by the shipped JDK by tools running on JDK 8.
  */
 public class ImageLocation {
+    // Also defined in src/java.base/share/native/libjimage/imageFile.hpp
+
+    /** End of attribute stream marker. */
     public static final int ATTRIBUTE_END = 0;
+    /** String table offset of module name. */
     public static final int ATTRIBUTE_MODULE = 1;
+    /** String table offset of resource path parent. */
     public static final int ATTRIBUTE_PARENT = 2;
+    /** String table offset of resource path base. */
     public static final int ATTRIBUTE_BASE = 3;
+    /** String table offset of resource path extension. */
     public static final int ATTRIBUTE_EXTENSION = 4;
+    /** Container byte offset of resource. */
     public static final int ATTRIBUTE_OFFSET = 5;
+    /** In-image byte size of the compressed resource. */
     public static final int ATTRIBUTE_COMPRESSED = 6;
+    /** In-memory byte size of the uncompressed resource. */
     public static final int ATTRIBUTE_UNCOMPRESSED = 7;
-    public static final int ATTRIBUTE_COUNT = 8;
+    /** Flags relating to preview mode resources. */
+    public static final int ATTRIBUTE_PREVIEW_FLAGS = 8;
+    /** Number of attribute kinds. */
+    public static final int ATTRIBUTE_COUNT = 9;
+
+    // Flag masks for the ATTRIBUTE_PREVIEW_FLAGS attribute. Defined so
+    // that zero is the overwhelmingly common case for normal resources.
+
+    /**
+     * Indicates that a non-preview location is associated with preview
+     * resources.
+     *
+     * <p>This can apply to both resources and directories in the
+     * {@code /modules/xxx/...} namespace, as well as {@code /packages/xxx}
+     * directories.
+     *
+     * <p>For {@code /packages/xxx} directories, it indicates that the package
+     * has preview resources in one of the modules in which it exists.
+     */
+    private static final int FLAGS_HAS_PREVIEW_VERSION = 0x1;
+    /**
+     * Set on all locations in the {@code /modules/xxx/META-INF/preview/...}
+     * namespace.
+     *
+     * <p>This flag is mutually exclusive with {@link #FLAGS_HAS_PREVIEW_VERSION}.
+     */
+    private static final int FLAGS_IS_PREVIEW_VERSION = 0x2;
+    /**
+     * Indicates that a location only exists due to preview resources.
+     *
+     * <p>This can apply to both resources and directories in the
+     * {@code /modules/xxx/...} namespace, as well as {@code /packages/xxx}
+     * directories.
+     *
+     * <p>For {@code /packages/xxx} directories it indicates that, for every
+     * module in which the package exists, it is preview only.
+     *
+     * <p>This flag is mutually exclusive with {@link #FLAGS_HAS_PREVIEW_VERSION}
+     * and need not imply that {@link #FLAGS_IS_PREVIEW_VERSION} is set (i.e.
+     * for {@code /packages/xxx} directories).
+     */
+    private static final int FLAGS_IS_PREVIEW_ONLY = 0x4;
+
+    // Also used in ImageReader.
+    static final String MODULES_PREFIX = "/modules";
+    static final String PACKAGES_PREFIX = "/packages";
+    static final String PREVIEW_INFIX = "/META-INF/preview";
+
+    /**
+     * Helper function to calculate preview flags (ATTRIBUTE_PREVIEW_FLAGS).
+     *
+     * <p>Since preview flags are calculated separately for resource nodes and
+     * directory nodes (in two quite different places) it's useful to have a
+     * common helper.
+     *
+     * <p>Based on the entry name, the flags are:
+     * <ul>
+     *     <li>{@code "[/modules]/<module>/<path>"} normal resource or directory:<br>
+     *     Zero, or {@code FLAGS_HAS_PREVIEW_VERSION} if a preview entry exists.
+     *     <li>{@code "[/modules]/<module>/META-INF/preview/<path>"} preview
+     *     resource or directory:<br>
+     *     {@code FLAGS_IS_PREVIEW_VERSION}, and additionally {@code
+     *     FLAGS_IS_PREVIEW_ONLY} if no normal version of the resource exists.
+     *     <li>In all other cases, returned flags are zero (note that {@code
+     *     "/packages/xxx"} entries may have flags, but these are calculated
+     *     elsewhere).
+     * </ul>
+     *
+     * @param name the jimage name of the resource or directory.
+     * @param hasEntry a predicate for jimage names returning whether an entry
+     *     is present.
+     * @return flags for the ATTRIBUTE_PREVIEW_FLAGS attribute.
+     */
+    public static int getFlags(String name, Predicate<String> hasEntry) {
+        if (name.startsWith(PACKAGES_PREFIX + "/")) {
+            throw new IllegalArgumentException(
+                    "Package sub-directory flags handled separately: " + name);
+        }
+        // Find suffix for either '/modules/xxx/suffix' or '/xxx/suffix' paths.
+        int idx = name.startsWith(MODULES_PREFIX + "/") ? MODULES_PREFIX.length() + 1 : 1;
+        int suffixStart = name.indexOf('/', idx);
+        if (suffixStart == -1) {
+            // No flags for '[/modules]/xxx' paths (esp. '/modules', '/packages').
+            // '/packages/xxx' entries have flags, but not calculated here.
+            return 0;
+        }
+        // Prefix is either '/modules/xxx' or '/xxx', and suffix starts with '/'.
+        String prefix = name.substring(0, suffixStart);
+        String suffix = name.substring(suffixStart);
+        if (suffix.startsWith(PREVIEW_INFIX + "/")) {
+            // Preview resources/directories.
+            String nonPreviewName = prefix + suffix.substring(PREVIEW_INFIX.length());
+            return FLAGS_IS_PREVIEW_VERSION
+                    | (hasEntry.test(nonPreviewName) ? 0 : FLAGS_IS_PREVIEW_ONLY);
+        } else if (!suffix.startsWith("/META-INF/")) {
+            // Non-preview resources/directories.
+            String previewName = prefix + PREVIEW_INFIX + suffix;
+            return hasEntry.test(previewName) ? FLAGS_HAS_PREVIEW_VERSION : 0;
+        } else {
+            // Suffix is '/META-INF/xxx' and no preview version is even possible.
+            return 0;
+        }
+    }
+
+    /**
+     * Helper function to calculate package flags for {@code "/packages/xxx"}
+     * directory entries.
+     *
+     * <p>Based on the module references, the flags are:
+     * <ul>
+     *     <li>{@code FLAGS_HAS_PREVIEW_VERSION} if <em>any</em> referenced
+     *     package has a preview version.
+     *     <li>{@code FLAGS_IS_PREVIEW_ONLY} if <em>all</em> referenced packages
+     *     are preview only.
+     * </ul>
+     *
+     * @return package flags for {@code "/packages/xxx"} directory entries.
+     */
+    public static int getPackageFlags(List<ModuleReference> moduleReferences) {
+        boolean hasPreviewVersion =
+                moduleReferences.stream().anyMatch(ModuleReference::hasPreviewVersion);
+        boolean isPreviewOnly =
+                moduleReferences.stream().allMatch(ModuleReference::isPreviewOnly);
+        return (hasPreviewVersion ? ImageLocation.FLAGS_HAS_PREVIEW_VERSION : 0)
+                | (isPreviewOnly ? ImageLocation.FLAGS_IS_PREVIEW_ONLY : 0);
+    }
+
+    /**
+     * Tests a non-preview image location's flags to see if it has preview
+     * content associated with it.
+     */
+    public static boolean hasPreviewVersion(int flags) {
+        return (flags & FLAGS_HAS_PREVIEW_VERSION) != 0;
+    }
+
+    /**
+     * Tests an image location's flags to see if it only exists in preview mode.
+     */
+    public static boolean isPreviewOnly(int flags) {
+        return (flags & FLAGS_IS_PREVIEW_ONLY) != 0;
+    }
+
+    public enum LocationType {
+        RESOURCE, MODULES_ROOT, MODULES_DIR, PACKAGES_ROOT, PACKAGES_DIR;
+    }
 
     protected final long[] attributes;
 
     protected final ImageStrings strings;
 
@@ -283,20 +439,24 @@
 
     public int getExtensionOffset() {
         return (int)getAttribute(ATTRIBUTE_EXTENSION);
     }
 
+    public int getFlags() {
+        return (int) getAttribute(ATTRIBUTE_PREVIEW_FLAGS);
+    }
+
     public String getFullName() {
         return getFullName(false);
     }
 
     public String getFullName(boolean modulesPrefix) {
         StringBuilder builder = new StringBuilder();
 
         if (getModuleOffset() != 0) {
             if (modulesPrefix) {
-                builder.append("/modules");
+                builder.append(MODULES_PREFIX);
             }
 
             builder.append('/');
             builder.append(getModule());
             builder.append('/');
@@ -315,40 +475,10 @@
         }
 
         return builder.toString();
     }
 
-    String buildName(boolean includeModule, boolean includeParent,
-            boolean includeName) {
-        StringBuilder builder = new StringBuilder();
-
-        if (includeModule && getModuleOffset() != 0) {
-            builder.append("/modules/");
-            builder.append(getModule());
-         }
-
-        if (includeParent && getParentOffset() != 0) {
-            builder.append('/');
-            builder.append(getParent());
-        }
-
-        if (includeName) {
-            if (includeModule || includeParent) {
-                builder.append('/');
-            }
-
-            builder.append(getBase());
-
-            if (getExtensionOffset() != 0) {
-                builder.append('.');
-                builder.append(getExtension());
-            }
-        }
-
-        return builder.toString();
-   }
-
     public long getContentOffset() {
         return getAttribute(ATTRIBUTE_OFFSET);
     }
 
     public long getCompressedSize() {
@@ -357,10 +487,46 @@
 
     public long getUncompressedSize() {
         return getAttribute(ATTRIBUTE_UNCOMPRESSED);
     }
 
+    // Fast (zero allocation) type determination for locations.
+    public LocationType getType() {
+        switch (getModuleOffset()) {
+            case ImageStrings.MODULES_STRING_OFFSET:
+                // Locations in /modules/... namespace are directory entries.
+                return LocationType.MODULES_DIR;
+            case ImageStrings.PACKAGES_STRING_OFFSET:
+                // Locations in /packages/... namespace are always 2-level
+                // "/packages/xxx" directories.
+                return LocationType.PACKAGES_DIR;
+            case ImageStrings.EMPTY_STRING_OFFSET:
+                // Only 2 choices, either the "/modules" or "/packages" root.
+                assert isRootDir() : "Invalid root directory: " + getFullName();
+                return getBase().charAt(1) == 'p'
+                        ? LocationType.PACKAGES_ROOT
+                        : LocationType.MODULES_ROOT;
+            default:
+                // Anything else is /<module>/<path> and references a resource.
+                return LocationType.RESOURCE;
+        }
+    }
+
+    private boolean isRootDir() {
+        if (getModuleOffset() == 0 && getParentOffset() == 0) {
+            String name = getFullName();
+            return name.equals(MODULES_PREFIX) || name.equals(PACKAGES_PREFIX);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        // Cannot use String.format() (too early in startup for locale code).
+        return "ImageLocation[name='" + getFullName() + "', type=" + getType() + ", flags=" + getFlags() + "]";
+    }
+
     static ImageLocation readFrom(BasicImageReader reader, int offset) {
         Objects.requireNonNull(reader);
         long[] attributes = reader.getAttributes(offset);
         ImageStringsReader strings = reader.getStrings();
 
