diff a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java
--- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java
+++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java
@@ -22,37 +22,38 @@
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 package jdk.tools.jlink.internal;
 
+import jdk.internal.jimage.ImageLocation;
+import jdk.internal.jimage.ModuleReference;
+
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
-import java.util.TreeSet;
+import java.util.stream.Collectors;
 
 /**
  * A class to build a sorted tree of Resource paths as a tree of ImageLocation.
- *
  */
 // XXX Public only due to the JImageTask / JImageTask code duplication
 public final class ImageResourcesTree {
-    public static boolean isTreeInfoResource(String path) {
-        return path.startsWith("/packages") || path.startsWith("/modules");
-    }
-
     /**
      * Path item tree node.
      */
-    private static class Node {
+    // Visible for testing only.
+    static class Node {
 
         private final String name;
         private final Map<String, Node> children = new TreeMap<>();
         private final Node parent;
         private ImageLocationWriter loc;
@@ -64,10 +65,18 @@
             if (parent != null) {
                 parent.children.put(name, this);
             }
         }
 
+        private void setLocation(ImageLocationWriter loc) {
+            // This *can* be called more than once, but only with the same instance.
+            if (this.loc != null && loc != this.loc) {
+                throw new IllegalStateException("Cannot add different locations: " + name);
+            }
+            this.loc = Objects.requireNonNull(loc);
+        }
+
         public String getPath() {
             if (parent == null) {
                 return "/";
             }
             return buildPath(this);
@@ -93,219 +102,210 @@
                 return path + "/" + item.getName();
             }
         }
     }
 
-    private static final class ResourceNode extends Node {
+    // Visible for testing only.
+    static final class ResourceNode extends Node {
 
         public ResourceNode(String name, Node parent) {
             super(name, parent);
         }
     }
 
-    private static class PackageNode extends Node {
-        /**
-         * A reference to a package. Empty packages can be located inside one or
-         * more modules. A package with classes exist in only one module.
-         */
-        static final class PackageReference {
-
-            private final String name;
-            private final boolean isEmpty;
+    /**
+     * A 2nd level package directory, {@code "/packages/<package-name>"}.
+     *
+     * <p>While package paths can exist within many modules, for each package
+     * there is at most one module in which that package has resources.
+     *
+     * <p>For example, the package path {@code java/util} exists in both the
+     * {@code java.base} and {@code java.logging} modules. This means both
+     * {@code "/packages/java.util/java.base"} and
+     * {@code "/packages/java.util/java.logging"} will exist, but only
+     * {@code "java.base"} entry will be marked as having content.
+     *
+     * <p>When processing module references in non-preview mode, entries marked
+     * as {@link ModuleReference#isPreviewOnly() preview-only} must be ignored.
+     *
+     * <p>If all references in a package are preview-only, then the entire
+     * package is marked as preview-only, and must be ignored.
+     */
+    // Visible for testing only.
+    static final class PackageNode extends Node {
+        private final List<ModuleReference> moduleReferences;
 
-            PackageReference(String name, boolean isEmpty) {
-                this.name = Objects.requireNonNull(name);
-                this.isEmpty = isEmpty;
+        PackageNode(String name, List<ModuleReference> moduleReferences, Node parent) {
+            super(name, parent);
+            if (moduleReferences.isEmpty()) {
+                throw new IllegalStateException("Package must be associated with modules: " + name);
             }
-
-            @Override
-            public String toString() {
-                return name + "[empty:" + isEmpty + "]";
+            if (moduleReferences.stream().filter(ModuleReference::hasResources).count() > 1) {
+                throw new IllegalStateException("Multiple modules contain non-empty package: " + name);
             }
+            this.moduleReferences = Collections.unmodifiableList(moduleReferences);
         }
 
-        private final Map<String, PackageReference> references = new TreeMap<>();
-
-        PackageNode(String name, Node parent) {
-            super(name, parent);
-        }
-
-        private void addReference(String name, boolean isEmpty) {
-            PackageReference ref = references.get(name);
-            if (ref == null || ref.isEmpty) {
-                references.put(name, new PackageReference(name, isEmpty));
-            }
+        List<ModuleReference> getModuleReferences() {
+            return moduleReferences;
         }
+    }
 
-        private void validate() {
-            boolean exists = false;
-            for (PackageReference ref : references.values()) {
-                if (!ref.isEmpty) {
-                    if (exists) {
-                        throw new RuntimeException("Multiple modules to contain package "
-                                + getName());
-                    } else {
-                        exists = true;
-                    }
-                }
-            }
+    // Not serialized, and never stored in any field of any class that is.
+    @SuppressWarnings("serial")
+    private static final class InvalidTreeException extends Exception {
+        public InvalidTreeException(Node badNode) {
+            super("Resources tree, invalid data structure, skipping: " + badNode.getPath());
         }
+        // Exception only used for program flow, not debugging.
+        @Override
+        public Throwable fillInStackTrace() {return this;}
     }
 
     /**
      * Tree of nodes.
      */
-    private static final class Tree {
+    // Visible for testing only.
+    static final class Tree {
+        private static final String PREVIEW_PREFIX = "META-INF/preview/";
 
         private final Map<String, Node> directAccess = new HashMap<>();
         private final List<String> paths;
         private final Node root;
-        private Node modules;
-        private Node packages;
+        private Node packagesRoot;
 
-        private Tree(List<String> paths) {
-            this.paths = paths;
+        // Visible for testing only.
+        Tree(List<String> paths) {
+            this.paths = paths.stream().sorted(Comparator.reverseOrder()).toList();
+            // Root node is not added to the directAccess map.
             root = new Node("", null);
             buildTree();
         }
 
         private void buildTree() {
-            modules = new Node("modules", root);
-            directAccess.put(modules.getPath(), modules);
-
-            Map<String, Set<String>> moduleToPackage = new TreeMap<>();
-            Map<String, Set<String>> packageToModule = new TreeMap<>();
-
-            for (String p : paths) {
-                if (!p.startsWith("/")) {
-                    continue;
-                }
-                String[] split = p.split("/");
-                // minimum length is 3 items: /<mod>/<pkg>
-                if (split.length < 3) {
-                    System.err.println("Resources tree, invalid data structure, "
-                            + "skipping " + p);
-                    continue;
-                }
-                Node current = modules;
-                String module = null;
-                for (int i = 0; i < split.length; i++) {
-                    // When a non terminal node is marked as being a resource, something is wrong.
+            Node modulesRoot = new Node("modules", root);
+            directAccess.put(modulesRoot.getPath(), modulesRoot);
+            packagesRoot = new Node("packages", root);
+            directAccess.put(packagesRoot.getPath(), packagesRoot);
+
+            // Map of dot-separated package names to module references (those
+            // in which the package appear). References are merged after to
+            // ensure each module name appears only once, but temporarily a
+            // module may have several entries per package (e.g. with-content,
+            // without-content, normal, preview-only etc..).
+            Map<String, Set<ModuleReference>> packageToModules = new TreeMap<>();
+            for (String fullPath : paths) {
+                try {
+                    processPath(fullPath, modulesRoot, packageToModules);
+                } catch (InvalidTreeException ex) {
                     // It has been observed some badly created jar file to contain
-                    // invalid directory entry marled as not directory (see 8131762)
-                    if (current instanceof ResourceNode) {
-                        System.err.println("Resources tree, invalid data structure, "
-                                + "skipping " + p);
-                        continue;
-                    }
-                    String s = split[i];
-                    if (!s.isEmpty()) {
-                        // First item, this is the module, simply add a new node to the
-                        // tree.
-                        if (module == null) {
-                            module = s;
-                        }
-                        Node n = current.children.get(s);
-                        if (n == null) {
-                            if (i == split.length - 1) { // Leaf
-                                n = new ResourceNode(s, current);
-                                String pkg = toPackageName(n.parent);
-                                //System.err.println("Adding a resource node. pkg " + pkg + ", name " + s);
-                                if (pkg != null && !pkg.startsWith("META-INF")) {
-                                    Set<String> pkgs = moduleToPackage.get(module);
-                                    if (pkgs == null) {
-                                        pkgs = new TreeSet<>();
-                                        moduleToPackage.put(module, pkgs);
-                                    }
-                                    pkgs.add(pkg);
-                                }
-                            } else { // put only sub trees, no leaf
-                                n = new Node(s, current);
-                                directAccess.put(n.getPath(), n);
-                                String pkg = toPackageName(n);
-                                if (pkg != null && !pkg.startsWith("META-INF")) {
-                                    Set<String> mods = packageToModule.get(pkg);
-                                    if (mods == null) {
-                                        mods = new TreeSet<>();
-                                        packageToModule.put(pkg, mods);
-                                    }
-                                    mods.add(module);
-                                }
-                            }
-                        }
-                        current = n;
-                    }
-                }
-            }
-            packages = new Node("packages", root);
-            directAccess.put(packages.getPath(), packages);
-            // The subset of package nodes that have some content.
-            // These packages exist only in a single module.
-            for (Map.Entry<String, Set<String>> entry : moduleToPackage.entrySet()) {
-                for (String pkg : entry.getValue()) {
-                    PackageNode pkgNode = new PackageNode(pkg, packages);
-                    pkgNode.addReference(entry.getKey(), false);
-                    directAccess.put(pkgNode.getPath(), pkgNode);
+                    // invalid directory entry marked as not directory (see 8131762).
+                    System.err.println(ex.getMessage());
                 }
             }
 
-            // All packages
-            for (Map.Entry<String, Set<String>> entry : packageToModule.entrySet()) {
-                // Do we already have a package node?
-                PackageNode pkgNode = (PackageNode) packages.getChildren(entry.getKey());
-                if (pkgNode == null) {
-                    pkgNode = new PackageNode(entry.getKey(), packages);
-                }
-                for (String module : entry.getValue()) {
-                    pkgNode.addReference(module, true);
-                }
+            // We've collected information for all "packages", including the root
+            // (empty) package and anything under "META-INF". However, these should
+            // not have entries in the "/packages" directory.
+            packageToModules.keySet().removeIf(p -> p.isEmpty() || p.equals("META-INF") || p.startsWith("META-INF."));
+            packageToModules.forEach((pkgName, modRefs) -> {
+                // Merge multiple refs for the same module.
+                List<ModuleReference> pkgModules = modRefs.stream()
+                        .collect(Collectors.groupingBy(ModuleReference::name))
+                        .values().stream()
+                        .map(refs -> refs.stream().reduce(ModuleReference::merge).orElseThrow())
+                        .sorted()
+                        .toList();
+                PackageNode pkgNode = new PackageNode(pkgName, pkgModules, packagesRoot);
                 directAccess.put(pkgNode.getPath(), pkgNode);
+            });
+        }
+
+        private void processPath(
+                String fullPath,
+                Node modulesRoot,
+                Map<String, Set<ModuleReference>> packageToModules)
+                throws InvalidTreeException {
+            // Paths are untrusted, so be careful about checking expected format.
+            if (!fullPath.startsWith("/") || fullPath.endsWith("/") || fullPath.contains("//")) {
+                return;
+            }
+            int modEnd = fullPath.indexOf('/', 1);
+            // Ensure non-empty module name with non-empty suffix.
+            if (modEnd <= 1) {
+                return;
             }
-            // Validate that the packages are well formed.
-            for (Node n : packages.children.values()) {
-                ((PackageNode)n).validate();
+            String modName = fullPath.substring(1, modEnd);
+            String pkgPath = fullPath.substring(modEnd + 1);
+
+            Node parentNode = getDirectoryNode(modName, modulesRoot);
+            boolean isPreviewPath = false;
+            if (pkgPath.startsWith(PREVIEW_PREFIX)) {
+                // For preview paths, process nodes relative to the preview directory.
+                pkgPath = pkgPath.substring(PREVIEW_PREFIX.length());
+                Node metaInf = getDirectoryNode("META-INF", parentNode);
+                parentNode = getDirectoryNode("preview", metaInf);
+                isPreviewPath = true;
             }
 
+            int pathEnd = pkgPath.lastIndexOf('/');
+            // From invariants tested above, this must now be well-formed.
+            String fullPkgName = (pathEnd == -1) ? "" : pkgPath.substring(0, pathEnd).replace('/', '.');
+            String resourceName = pkgPath.substring(pathEnd + 1);
+            // Intermediate packages are marked "empty" (no resources). This might
+            // later be merged with a non-empty reference for the same package.
+            ModuleReference emptyRef = ModuleReference.forEmptyPackage(modName, isPreviewPath);
+
+            // Work down through empty packages to final resource.
+            for (int i = pkgEndIndex(fullPkgName, 0); i != -1; i = pkgEndIndex(fullPkgName, i)) {
+                // Due to invariants already checked, pkgName is non-empty.
+                String pkgName = fullPkgName.substring(0, i);
+                packageToModules.computeIfAbsent(pkgName, p -> new HashSet<>()).add(emptyRef);
+                String childNodeName = pkgName.substring(pkgName.lastIndexOf('.') + 1);
+                parentNode = getDirectoryNode(childNodeName, parentNode);
+            }
+            // Reached non-empty (leaf) package (could still be a duplicate).
+            Node resourceNode = parentNode.getChildren(resourceName);
+            if (resourceNode == null) {
+                ModuleReference resourceRef = ModuleReference.forPackage(modName, isPreviewPath);
+                packageToModules.computeIfAbsent(fullPkgName, p -> new HashSet<>()).add(resourceRef);
+                // Init adds new node to parent (don't add resources to directAccess).
+                new ResourceNode(resourceName, parentNode);
+            } else if (!(resourceNode instanceof ResourceNode)) {
+                throw new InvalidTreeException(resourceNode);
+            }
         }
 
-        public String toResourceName(Node node) {
-            if (!node.children.isEmpty()) {
-                throw new RuntimeException("Node is not a resource");
+        private Node getDirectoryNode(String name, Node parent) throws InvalidTreeException {
+            Node child = parent.getChildren(name);
+            if (child == null) {
+                // Adds child to parent during init.
+                child = new Node(name, parent);
+                directAccess.put(child.getPath(), child);
+            } else if (child instanceof ResourceNode) {
+                throw new InvalidTreeException(child);
             }
-            return removeRadical(node);
+            return child;
         }
 
-        public String getModule(Node node) {
-            if (node.parent == null || node.getName().equals("modules")
-                    || node.getName().startsWith("packages")) {
-                return null;
-            }
-            String path = removeRadical(node);
-            // "/xxx/...";
-            path = path.substring(1);
-            int i = path.indexOf("/");
-            if (i == -1) {
-                return path;
-            } else {
-                return path.substring(0, i);
+        // Helper to iterate package names up to, and including, the complete name.
+        private int pkgEndIndex(String s, int i) {
+            if (i >= 0 && i < s.length()) {
+                i = s.indexOf('.', i + 1);
+                return i != -1 ? i : s.length();
             }
+            return -1;
         }
 
-        public String toPackageName(Node node) {
-            if (node.parent == null) {
-                return null;
-            }
-            String path = removeRadical(node.getPath(), "/modules/");
-            String module = getModule(node);
-            if (path.equals(module)) {
-                return null;
+        private String toResourceName(Node node) {
+            if (!node.children.isEmpty()) {
+                throw new RuntimeException("Node is not a resource");
             }
-            String pkg = removeRadical(path, module + "/");
-            return pkg.replace('/', '.');
+            return removeRadical(node);
         }
 
-        public String removeRadical(Node node) {
+        private String removeRadical(Node node) {
             return removeRadical(node.getPath(), "/modules");
         }
 
         private String removeRadical(String path, String str) {
             if (!(path.length() < str.length())) {
@@ -337,24 +337,27 @@
             addLocations(tree.getRoot());
         }
 
         private int addLocations(Node current) {
             if (current instanceof PackageNode) {
-                PackageNode pkgNode = (PackageNode) current;
-                int size = pkgNode.references.size() * 8;
-                writer.addLocation(current.getPath(), offset, 0, size);
+                List<ModuleReference> refs = ((PackageNode) current).getModuleReferences();
+                // "/packages/<pkg name>" entries have 8-byte entries (flags+offset).
+                int size = refs.size() * 8;
+                writer.addLocation(current.getPath(), offset, 0, size, ImageLocation.getPackageFlags(refs));
                 offset += size;
             } else {
                 int[] ret = new int[current.children.size()];
                 int i = 0;
                 for (java.util.Map.Entry<String, Node> entry : current.children.entrySet()) {
                     ret[i] = addLocations(entry.getValue());
                     i += 1;
                 }
                 if (current != tree.getRoot() && !(current instanceof ResourceNode)) {
+                    int locFlags = ImageLocation.getFlags(current.getPath(), tree.directAccess::containsKey);
+                    // Normal directory entries have 4-byte entries (offset only).
                     int size = ret.length * 4;
-                    writer.addLocation(current.getPath(), offset, 0, size);
+                    writer.addLocation(current.getPath(), offset, 0, size, locFlags);
                     offset += size;
                 }
             }
             return 0;
         }
@@ -367,31 +370,26 @@
             }
             // Attach location to node
             for (Map.Entry<String, ImageLocationWriter> entry : outLocations.entrySet()) {
                 Node item = tree.getMap().get(entry.getKey());
                 if (item != null) {
-                    item.loc = entry.getValue();
+                    item.setLocation(entry.getValue());
                 }
             }
             computeContent(tree.getRoot(), outLocations);
             return content;
         }
 
         private int computeContent(Node current, Map<String, ImageLocationWriter> outLocations) {
             if (current instanceof PackageNode) {
-                // /packages/<pkg name>
-                PackageNode pkgNode = (PackageNode) current;
-                int size = pkgNode.references.size() * 8;
-                ByteBuffer buff = ByteBuffer.allocate(size);
-                buff.order(writer.getByteOrder());
-                for (PackageNode.PackageReference mod : pkgNode.references.values()) {
-                    buff.putInt(mod.isEmpty ? 1 : 0);
-                    buff.putInt(writer.addString(mod.name));
-                }
-                byte[] arr = buff.array();
-                content.add(arr);
-                current.loc = outLocations.get(current.getPath());
+                // "/packages/<pkg name>" entries have 8-byte entries (flags+offset).
+                List<ModuleReference> refs = ((PackageNode) current).getModuleReferences();
+                ByteBuffer byteBuffer = ByteBuffer.allocate(8 * refs.size());
+                byteBuffer.order(writer.getByteOrder());
+                ModuleReference.write(refs, byteBuffer.asIntBuffer(), writer::addString);
+                content.add(byteBuffer.array());
+                current.setLocation(outLocations.get(current.getPath()));
             } else {
                 int[] ret = new int[current.children.size()];
                 int i = 0;
                 for (java.util.Map.Entry<String, Node> entry : current.children.entrySet()) {
                     ret[i] = computeContent(entry.getValue(), outLocations);
@@ -408,14 +406,14 @@
                     content.add(arr);
                 } else {
                     if (current instanceof ResourceNode) {
                         // A resource location, remove "/modules"
                         String s = tree.toResourceName(current);
-                        current.loc = outLocations.get(s);
+                        current.setLocation(outLocations.get(s));
                     } else {
                         // empty "/packages" or empty "/modules" paths
-                        current.loc = outLocations.get(current.getPath());
+                        current.setLocation(outLocations.get(current.getPath()));
                     }
                 }
                 if (current.loc == null && current != tree.getRoot()) {
                     System.err.println("Invalid path in metadata, skipping " + current.getPath());
                 }
