diff --git a/src/hotspot/share/cds/aotArtifactFinder.cpp b/src/hotspot/share/cds/aotArtifactFinder.cpp
index e644a5e6f5fd1..f39aac24fed89 100644
--- a/src/hotspot/share/cds/aotArtifactFinder.cpp
+++ b/src/hotspot/share/cds/aotArtifactFinder.cpp
@@ -149,10 +149,11 @@ void AOTArtifactFinder::find_artifacts() {
   SystemDictionaryShared::dumptime_table()->iterate_all_live_classes([&] (InstanceKlass* k, DumpTimeClassInfo& info) {
     if (!info.is_excluded() && _seen_classes->get(k) == nullptr) {
       info.set_excluded();
+      info.set_has_checked_exclusion();
       assert(k->is_hidden(), "must be");
       if (log_is_enabled(Info, cds)) {
         ResourceMark rm;
-        log_info(cds)("Skipping %s: Hidden class", k->name()->as_C_string());
+        log_debug(cds)("Skipping %s: Unreferenced hidden class", k->name()->as_C_string());
       }
     }
   });
@@ -208,6 +209,10 @@ void AOTArtifactFinder::add_cached_instance_class(InstanceKlass* ik) {
   _seen_classes->put_if_absent(ik, &created);
   if (created) {
     _all_cached_classes->append(ik);
+    if (CDSConfig::is_dumping_final_static_archive() && ik->is_shared_unregistered_class()) {
+      // The following are not appliable to unregistered classes
+      return;
+    }
     scan_oops_in_instance_class(ik);
     if (ik->is_hidden() && CDSConfig::is_initing_classes_at_dump_time()) {
       bool succeed = AOTClassLinker::try_add_candidate(ik);
diff --git a/src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp b/src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp
index 042a0dadb201f..6f5ca6d791567 100644
--- a/src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp
+++ b/src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp
@@ -1,6 +1,6 @@
 
 /*
- * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 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
@@ -28,6 +28,7 @@
 
 #include "cds/dumpTimeClassInfo.hpp"
 
+#include "cds/cdsConfig.hpp"
 #include "classfile/systemDictionaryShared.hpp"
 #include "classfile/classLoaderData.inline.hpp"
 #include "oops/instanceKlass.hpp"
@@ -44,7 +45,10 @@ void DumpTimeSharedClassTable::iterate_all_live_classes(Function function) const
   auto wrapper = [&] (InstanceKlass* k, DumpTimeClassInfo& info) {
     assert(SafepointSynchronize::is_at_safepoint(), "invariant");
     assert_lock_strong(DumpTimeTable_lock);
-    if (k->is_loader_alive()) {
+    if (CDSConfig::is_dumping_final_static_archive() && !k->is_loaded()) {
+      assert(k->is_shared_unregistered_class(), "must be");
+      function(k, info);
+    } else if (k->is_loader_alive()) {
       function(k, info);
       assert(k->is_loader_alive(), "must not change");
     } else {
diff --git a/src/hotspot/share/cds/finalImageRecipes.cpp b/src/hotspot/share/cds/finalImageRecipes.cpp
index 55855679a1cfa..674a44937eb1f 100644
--- a/src/hotspot/share/cds/finalImageRecipes.cpp
+++ b/src/hotspot/share/cds/finalImageRecipes.cpp
@@ -102,7 +102,10 @@ void FinalImageRecipes::load_all_classes(TRAPS) {
     Klass* k = _all_klasses->at(i);
     if (k->is_instance_klass()) {
       InstanceKlass* ik = InstanceKlass::cast(k);
-      if (!ik->is_shared_unregistered_class() && !ik->is_hidden()) {
+      if (ik->is_shared_unregistered_class()) {
+        SystemDictionaryShared::init_dumptime_info(ik);
+        SystemDictionaryShared::add_unregistered_class(THREAD, ik);
+      } else if (!ik->is_hidden()) {
         Klass* actual = SystemDictionary::resolve_or_fail(ik->name(), class_loader, true, CHECK);
         if (actual != ik) {
           ResourceMark rm(THREAD);
diff --git a/src/hotspot/share/classfile/systemDictionaryShared.cpp b/src/hotspot/share/classfile/systemDictionaryShared.cpp
index c4e061b8fa993..78aafc2fe6487 100644
--- a/src/hotspot/share/classfile/systemDictionaryShared.cpp
+++ b/src/hotspot/share/classfile/systemDictionaryShared.cpp
@@ -662,7 +662,7 @@ bool SystemDictionaryShared::should_be_excluded(Klass* k) {
 }
 
 void SystemDictionaryShared::finish_exclusion_checks() {
-  if (CDSConfig::is_dumping_dynamic_archive()) {
+  if (CDSConfig::is_dumping_dynamic_archive() || CDSConfig::is_dumping_preimage_static_archive()) {
     // Do this first -- if a base class is excluded due to duplication,
     // all of its subclasses will also be excluded.
     ResourceMark rm;
diff --git a/src/hotspot/share/oops/klass.cpp b/src/hotspot/share/oops/klass.cpp
index 9f166e13751a7..71da5e1da095b 100644
--- a/src/hotspot/share/oops/klass.cpp
+++ b/src/hotspot/share/oops/klass.cpp
@@ -828,9 +828,15 @@ void Klass::remove_java_mirror() {
   if (CDSConfig::is_dumping_heap()) {
     Klass* src_k = ArchiveBuilder::current()->get_source_addr(this);
     oop orig_mirror = src_k->java_mirror();
-    oop scratch_mirror = HeapShared::scratch_java_mirror(orig_mirror);
-    if (scratch_mirror != nullptr) {
-      _archived_mirror_index = HeapShared::append_root(scratch_mirror);
+    if (orig_mirror == nullptr) {
+      assert(CDSConfig::is_dumping_final_static_archive(), "sanity");
+      assert(is_instance_klass(), "sanity");
+      assert(InstanceKlass::cast(this)->is_shared_unregistered_class(), "sanity");
+    } else {
+      oop scratch_mirror = HeapShared::scratch_java_mirror(orig_mirror);
+      if (scratch_mirror != nullptr) {
+        _archived_mirror_index = HeapShared::append_root(scratch_mirror);
+      }
     }
   }
 #endif
diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java b/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java
index 9db886e20a5b6..03b5c415ceee1 100644
--- a/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java
+++ b/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java
@@ -31,11 +31,13 @@
  * @requires vm.cds.supports.aot.class.linking
  * @comment work around JDK-8345635
  * @requires !vm.jvmci.enabled
- * @library /test/jdk/lib/testlibrary /test/lib
+ * @library /test/jdk/lib/testlibrary /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes
  * @build InitiatingLoaderTester BadOldClassA BadOldClassB
- * @build BulkLoaderTest
+ * @build BulkLoaderTest SimpleCusty
  * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar BulkLoaderTestApp.jar BulkLoaderTestApp MyUtil InitiatingLoaderTester
  *                 BadOldClassA BadOldClassB
+ * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar cust.jar
+ *                 SimpleCusty
  * @run driver BulkLoaderTest STATIC
  */
 
@@ -44,11 +46,13 @@
  * @requires vm.cds.supports.aot.class.linking
  * @comment work around JDK-8345635
  * @requires !vm.jvmci.enabled
- * @library /test/jdk/lib/testlibrary /test/lib
+ * @library /test/jdk/lib/testlibrary /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes
  * @build InitiatingLoaderTester BadOldClassA BadOldClassB
- * @build jdk.test.whitebox.WhiteBox BulkLoaderTest
+ * @build jdk.test.whitebox.WhiteBox BulkLoaderTest SimpleCusty
  * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar BulkLoaderTestApp.jar BulkLoaderTestApp MyUtil InitiatingLoaderTester
  *                 BadOldClassA BadOldClassB
+ * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar cust.jar
+ *                 SimpleCusty
  * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
  * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. BulkLoaderTest DYNAMIC
  */
@@ -58,16 +62,20 @@
  * @requires vm.cds.supports.aot.class.linking
  * @comment work around JDK-8345635
  * @requires !vm.jvmci.enabled
- * @library /test/jdk/lib/testlibrary /test/lib
+ * @library /test/jdk/lib/testlibrary /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes
  * @build InitiatingLoaderTester BadOldClassA BadOldClassB
- * @build BulkLoaderTest
+ * @build BulkLoaderTest SimpleCusty
  * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar BulkLoaderTestApp.jar BulkLoaderTestApp MyUtil InitiatingLoaderTester
  *                 BadOldClassA BadOldClassB
+ * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar cust.jar
+ *                 SimpleCusty
  * @run driver BulkLoaderTest AOT
  */
 
 import java.io.File;
 import java.lang.StackWalker.StackFrame;
+import java.net.URL;
+import java.net.URLClassLoader;
 import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -124,7 +132,7 @@ public String classpath(RunMode runMode) {
         @Override
         public String[] vmArgs(RunMode runMode) {
             return new String[] {
-                "-Xlog:cds,cds+aot+load",
+                "-Xlog:cds,cds+aot+load,cds+class=debug",
                 "-XX:+AOTClassLinking",
             };
         }
@@ -140,6 +148,12 @@ public String[] appCommandLine(RunMode runMode) {
         public void checkExecution(OutputAnalyzer out, RunMode runMode) throws Exception {
             if (isAOTWorkflow() && runMode == RunMode.TRAINING) {
                 out.shouldContain("Skipping BadOldClassA: Unlinked class not supported by AOTConfiguration");
+                out.shouldContain("Skipping SimpleCusty: Duplicated unregistered class");
+            }
+
+            if (isDumping(runMode)) {
+                // Check that we are archiving classes for custom class loaders.
+                out.shouldMatch("cds,class.* SimpleCusty");
             }
         }
     }
@@ -152,6 +166,7 @@ public static void main(String args[]) throws Exception {
         checkClasses();
         checkInitiatingLoader();
         checkOldClasses();
+        checkCustomLoader();
     }
 
     // Check the ClassLoader/Module/Package/ProtectionDomain/CodeSource of classes that are aot-linked
@@ -275,6 +290,24 @@ static void checkOldClasses() throws Exception {
             System.out.println("Caught VerifyError for BadOldClassB: " + e);
         }
     }
+
+
+    static void checkCustomLoader() throws Exception {
+        for (int i = 0; i < 2; i++) {
+            Object o = initFromCustomLoader();
+            System.out.println(o);
+        }
+    }
+
+    static Object initFromCustomLoader() throws Exception {
+        String path = "cust.jar";
+        URL url = new File(path).toURI().toURL();
+        URL[] urls = new URL[] {url};
+        URLClassLoader urlClassLoader =
+            new URLClassLoader("MyLoader", urls, null);
+        Class c = Class.forName("SimpleCusty", true, urlClassLoader);
+        return c.newInstance();
+    }
 }
 
 class MyUtil {
diff --git a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/LambdaCustomLoader.java b/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/LambdaCustomLoader.java
index 205901e376998..4563aa60c89f5 100644
--- a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/LambdaCustomLoader.java
+++ b/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/LambdaCustomLoader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 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
@@ -51,7 +51,7 @@ static void test() throws Exception {
             "-Xlog:class+load,cds=debug,cds+dynamic",
             "-cp", appJar, mainClass, appJar, "init", "keep-alive")
             .assertNormalExit(output -> {
-                output.shouldMatch("Skipping.LambHello[$][$]Lambda.*0x.*:.Hidden.class")
+                output.shouldMatch("Skipping.LambHello[$][$]Lambda.*0x.*:.Unreferenced.hidden.class")
                       .shouldHaveExitValue(0);
             });
 
diff --git a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/LambdaProxyCallerIsHidden.java b/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/LambdaProxyCallerIsHidden.java
index 0c8cad0b3a604..4ec7e81737891 100644
--- a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/LambdaProxyCallerIsHidden.java
+++ b/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/LambdaProxyCallerIsHidden.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 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
@@ -59,8 +59,8 @@ static void test() throws Exception {
             "-Xlog:class+load,cds+dynamic,cds=debug",
             "-cp", appJar, mainClass)
             .assertNormalExit(output -> {
-                output.shouldMatch("Skipping.LambdaHello_0x.*[$][$]Lambda.*:.Hidden.class")
-                      .shouldMatch("Skipping.LambdaHello.0x.*:.Hidden.class")
+                output.shouldMatch("Skipping.LambdaHello_0x.*[$][$]Lambda.*:.Unreferenced.hidden.class")
+                      .shouldMatch("Skipping.LambdaHello.0x.*:.Unreferenced.hidden.class")
                       .shouldHaveExitValue(0);
             });
 
diff --git a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/RegularHiddenClass.java b/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/RegularHiddenClass.java
index 957e4cb647979..7793fd07bc366 100644
--- a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/RegularHiddenClass.java
+++ b/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/RegularHiddenClass.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 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
@@ -57,7 +57,7 @@ static void test() throws Exception {
             "-Xlog:class+load=debug,cds+dynamic,cds=debug",
             "-cp", appJar, mainClass, "keep-alive")
             .assertNormalExit(output -> {
-                output.shouldMatch("cds.*Skipping.TestClass.0x.*Hidden.class")
+                output.shouldMatch("cds.*Skipping.TestClass.0x.*Unreferenced.hidden.class")
                       .shouldNotMatch("cds.dynamic.*Archiving.hidden.TestClass.*")
                       .shouldHaveExitValue(0);
             });
diff --git a/test/hotspot/jtreg/runtime/cds/appcds/test-classes/SimpleCusty.java b/test/hotspot/jtreg/runtime/cds/appcds/test-classes/SimpleCusty.java
new file mode 100644
index 0000000000000..9c82e0eb15d23
--- /dev/null
+++ b/test/hotspot/jtreg/runtime/cds/appcds/test-classes/SimpleCusty.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 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.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ *
+ */
+
+// This class is to be loaded by a custom class loader.
+public class SimpleCusty {
+    public String toString() {
+        return "Instance of SimpleCusty";
+    }
+}