diff --git a/src/hotspot/share/cds/archiveBuilder.cpp b/src/hotspot/share/cds/archiveBuilder.cpp
index 636a955ba74..dbf0583619b 100644
--- a/src/hotspot/share/cds/archiveBuilder.cpp
+++ b/src/hotspot/share/cds/archiveBuilder.cpp
@@ -32,6 +32,7 @@
 #include "cds/metaspaceShared.hpp"
 #include "cds/regeneratedClasses.hpp"
 #include "classfile/classLoaderDataShared.hpp"
+#include "classfile/javaClasses.hpp"
 #include "classfile/symbolTable.hpp"
 #include "classfile/systemDictionaryShared.hpp"
 #include "classfile/vmClasses.hpp"
@@ -47,6 +48,7 @@
 #include "oops/objArrayOop.inline.hpp"
 #include "oops/oopHandle.inline.hpp"
 #include "runtime/arguments.hpp"
+#include "runtime/fieldDescriptor.inline.hpp"
 #include "runtime/globals_extension.hpp"
 #include "runtime/javaThread.hpp"
 #include "runtime/sharedRuntime.hpp"
@@ -965,7 +967,7 @@ class ArchiveBuilder::CDSMapLogger : AllStatic {
       SourceObjInfo* src_info = src_objs->at(i);
       address src = src_info->source_addr();
       address dest = src_info->buffered_addr();
-      log_data(last_obj_base, dest, last_obj_base + buffer_to_runtime_delta());
+      log_as_hex(last_obj_base, dest, last_obj_base + buffer_to_runtime_delta());
       address runtime_dest = dest + buffer_to_runtime_delta();
       int bytes = src_info->size_in_bytes();
 
@@ -1007,12 +1009,12 @@ class ArchiveBuilder::CDSMapLogger : AllStatic {
       last_obj_end  = dest + bytes;
     }
 
-    log_data(last_obj_base, last_obj_end, last_obj_base + buffer_to_runtime_delta());
+    log_as_hex(last_obj_base, last_obj_end, last_obj_base + buffer_to_runtime_delta());
     if (last_obj_end < region_end) {
       log_debug(cds, map)(PTR_FORMAT ": @@ Misc data " SIZE_FORMAT " bytes",
                           p2i(last_obj_end + buffer_to_runtime_delta()),
                           size_t(region_end - last_obj_end));
-      log_data(last_obj_end, region_end, last_obj_end + buffer_to_runtime_delta());
+      log_as_hex(last_obj_end, region_end, last_obj_end + buffer_to_runtime_delta());
     }
   }
 
@@ -1038,44 +1040,167 @@ class ArchiveBuilder::CDSMapLogger : AllStatic {
     MemRegion r = heap_info->buffer_region();
     address start = address(r.start());
     address end = address(r.end());
-    log_region("heap", start, end, to_requested(start));
+    log_region("heap", start, end, ArchiveHeapWriter::buffered_addr_to_requested_addr(start));
+
+    LogStreamHandle(Info, cds, map) st;
 
     while (start < end) {
       size_t byte_size;
-      oop original_oop = ArchiveHeapWriter::buffered_addr_to_source_obj(start);
-      if (original_oop != nullptr) {
-        ResourceMark rm;
-        log_info(cds, map)(PTR_FORMAT ": @@ Object %s",
-                           p2i(to_requested(start)), original_oop->klass()->external_name());
-        byte_size = original_oop->size() * BytesPerWord;
+      oop source_oop = ArchiveHeapWriter::buffered_addr_to_source_obj(start);
+      address requested_start = ArchiveHeapWriter::buffered_addr_to_requested_addr(start);
+      st.print(PTR_FORMAT ": @@ Object ", p2i(requested_start));
+
+      if (source_oop != nullptr) {
+        // This is a regular oop that got archived.
+        print_oop_with_requested_addr_cr(&st, source_oop, false);
+        byte_size = source_oop->size() * BytesPerWord;
       } else if (start == ArchiveHeapWriter::buffered_heap_roots_addr()) {
-        // HeapShared::roots() is copied specially so it doesn't exist in
-        // HeapShared::OriginalObjectTable. See HeapShared::copy_roots().
-        log_info(cds, map)(PTR_FORMAT ": @@ Object HeapShared::roots (ObjArray)",
-                           p2i(to_requested(start)));
+        // HeapShared::roots() is copied specially, so it doesn't exist in
+        // ArchiveHeapWriter::BufferOffsetToSourceObjectTable.
+        // See ArchiveHeapWriter::copy_roots_to_buffer().
+        st.print_cr("HeapShared::roots[%d]", HeapShared::pending_roots()->length());
         byte_size = ArchiveHeapWriter::heap_roots_word_size() * BytesPerWord;
+      } else if ((byte_size = ArchiveHeapWriter::get_filler_size_at(start)) > 0) {
+        // We have a filler oop, which also does not exist in BufferOffsetToSourceObjectTable.
+        st.print_cr("filler " SIZE_FORMAT " bytes", byte_size);
       } else {
-        // We have reached the end of the region, but have some unused space
-        // at the end.
-        log_info(cds, map)(PTR_FORMAT ": @@ Unused heap space " SIZE_FORMAT " bytes",
-                           p2i(to_requested(start)), size_t(end - start));
-        log_data(start, end, to_requested(start), /*is_heap=*/true);
-        break;
+        ShouldNotReachHere();
       }
+
       address oop_end = start + byte_size;
-      log_data(start, oop_end, to_requested(start), /*is_heap=*/true);
+      log_as_hex(start, oop_end, requested_start, /*is_heap=*/true);
+
+      if (source_oop != nullptr) {
+        log_oop_details(heap_info, source_oop);
+      } else if (start == ArchiveHeapWriter::buffered_heap_roots_addr()) {
+        log_heap_roots();
+      }
       start = oop_end;
     }
   }
 
-  static address to_requested(address p) {
-    return ArchiveHeapWriter::buffered_addr_to_requested_addr(p);
+  // ArchivedFieldPrinter is used to print the fields of archived objects. We can't
+  // use _source_obj->print_on(), because we want to print the oop fields
+  // in _source_obj with their requested addresses using print_oop_with_requested_addr_cr().
+  class ArchivedFieldPrinter : public FieldClosure {
+    ArchiveHeapInfo* _heap_info;
+    outputStream* _st;
+    oop _source_obj;
+  public:
+    ArchivedFieldPrinter(ArchiveHeapInfo* heap_info, outputStream* st, oop src_obj) :
+      _heap_info(heap_info), _st(st), _source_obj(src_obj) {}
+
+    void do_field(fieldDescriptor* fd) {
+      _st->print(" - ");
+      BasicType ft = fd->field_type();
+      switch (ft) {
+      case T_ARRAY:
+      case T_OBJECT:
+        fd->print_on(_st); // print just the name and offset
+        print_oop_with_requested_addr_cr(_st, _source_obj->obj_field(fd->offset()));
+        break;
+      default:
+        if (ArchiveHeapWriter::is_marked_as_native_pointer(_heap_info, _source_obj, fd->offset())) {
+          print_as_native_pointer(fd);
+        } else {
+          fd->print_on_for(_st, _source_obj); // name, offset, value
+          _st->cr();
+        }
+      }
+    }
+
+    void print_as_native_pointer(fieldDescriptor* fd) {
+      LP64_ONLY(assert(fd->field_type() == T_LONG, "must be"));
+      NOT_LP64 (assert(fd->field_type() == T_INT,  "must be"));
+
+      // We have a field that looks like an integer, but it's actually a pointer to a MetaspaceObj.
+      address source_native_ptr = (address)
+          LP64_ONLY(_source_obj->long_field(fd->offset()))
+          NOT_LP64( _source_obj->int_field (fd->offset()));
+      ArchiveBuilder* builder = ArchiveBuilder::current();
+
+      // The value of the native pointer at runtime.
+      address requested_native_ptr = builder->to_requested(builder->get_buffered_addr(source_native_ptr));
+
+      // The address of _source_obj at runtime
+      oop requested_obj = ArchiveHeapWriter::source_obj_to_requested_obj(_source_obj);
+      // The address of this field in the requested space
+      address requested_field_addr = cast_from_oop<address>(requested_obj) + fd->offset();
+
+      fd->print_on(_st);
+      _st->print_cr(PTR_FORMAT " (marked metadata pointer @" PTR_FORMAT " )",
+                    p2i(requested_native_ptr), p2i(requested_field_addr));
+    }
+  };
+
+  // Print the fields of instanceOops, or the elements of arrayOops
+  static void log_oop_details(ArchiveHeapInfo* heap_info, oop source_oop) {
+    LogStreamHandle(Trace, cds, map, oops) st;
+    if (st.is_enabled()) {
+      Klass* source_klass = source_oop->klass();
+      ArchiveBuilder* builder = ArchiveBuilder::current();
+      Klass* requested_klass = builder->to_requested(builder->get_buffered_addr(source_klass));
+
+      st.print(" - klass: ");
+      source_klass->print_value_on(&st);
+      st.print(" " PTR_FORMAT, p2i(requested_klass));
+      st.cr();
+
+      if (source_oop->is_typeArray()) {
+        TypeArrayKlass::cast(source_klass)->oop_print_elements_on(typeArrayOop(source_oop), &st);
+      } else if (source_oop->is_objArray()) {
+        objArrayOop source_obj_array = objArrayOop(source_oop);
+        for (int i = 0; i < source_obj_array->length(); i++) {
+          st.print(" -%4d: ", i);
+          print_oop_with_requested_addr_cr(&st, source_obj_array->obj_at(i));
+        }
+      } else {
+        st.print_cr(" - fields (" SIZE_FORMAT " words):", source_oop->size());
+        ArchivedFieldPrinter print_field(heap_info, &st, source_oop);
+        InstanceKlass::cast(source_klass)->print_nonstatic_fields(&print_field);
+      }
+    }
   }
-#endif
+
+  static void log_heap_roots() {
+    LogStreamHandle(Trace, cds, map, oops) st;
+    if (st.is_enabled()) {
+      for (int i = 0; i < HeapShared::pending_roots()->length(); i++) {
+        st.print("roots[%4d]: ", i);
+        print_oop_with_requested_addr_cr(&st, HeapShared::pending_roots()->at(i));
+      }
+    }
+  }
+
+  // The output looks like this. The first number is the requested address. The second number is
+  // the narrowOop version of the requested address.
+  //     0x00000007ffc7e840 (0xfff8fd08) java.lang.Class
+  //     0x00000007ffc000f8 (0xfff8001f) [B length: 11
+  static void print_oop_with_requested_addr_cr(outputStream* st, oop source_oop, bool print_addr = true) {
+    if (source_oop == nullptr) {
+      st->print_cr("null");
+    } else {
+      ResourceMark rm;
+      oop requested_obj = ArchiveHeapWriter::source_obj_to_requested_obj(source_oop);
+      if (print_addr) {
+        st->print(PTR_FORMAT " ", p2i(requested_obj));
+      }
+      if (UseCompressedOops) {
+        st->print("(0x%08x) ", CompressedOops::narrow_oop_value(requested_obj));
+      }
+      if (source_oop->is_array()) {
+        int array_len = arrayOop(source_oop)->length();
+        st->print_cr("%s length: %d", source_oop->klass()->external_name(), array_len);
+      } else {
+        st->print_cr("%s", source_oop->klass()->external_name());
+      }
+    }
+  }
+#endif // INCLUDE_CDS_JAVA_HEAP
 
   // Log all the data [base...top). Pretend that the base address
   // will be mapped to requested_base at run-time.
-  static void log_data(address base, address top, address requested_base, bool is_heap = false) {
+  static void log_as_hex(address base, address top, address requested_base, bool is_heap = false) {
     assert(top >= base, "must be");
 
     LogStreamHandle(Trace, cds, map) lsh;
@@ -1107,7 +1232,7 @@ public:
     address header_end = header + mapinfo->header()->header_size();
     log_region("header", header, header_end, 0);
     log_header(mapinfo);
-    log_data(header, header_end, 0);
+    log_as_hex(header, header_end, 0);
 
     DumpRegion* rw_region = &builder->_rw_region;
     DumpRegion* ro_region = &builder->_ro_region;
@@ -1117,7 +1242,7 @@ public:
 
     address bitmap_end = address(bitmap + bitmap_size_in_bytes);
     log_region("bitmap", address(bitmap), bitmap_end, 0);
-    log_data((address)bitmap, bitmap_end, 0);
+    log_as_hex((address)bitmap, bitmap_end, 0);
 
 #if INCLUDE_CDS_JAVA_HEAP
     if (heap_info->is_used()) {
diff --git a/src/hotspot/share/cds/archiveHeapWriter.cpp b/src/hotspot/share/cds/archiveHeapWriter.cpp
index 49170dac10c..c52814440af 100644
--- a/src/hotspot/share/cds/archiveHeapWriter.cpp
+++ b/src/hotspot/share/cds/archiveHeapWriter.cpp
@@ -64,12 +64,19 @@ GrowableArrayCHeap<oop, mtClassShared>* ArchiveHeapWriter::_source_objs;
 ArchiveHeapWriter::BufferOffsetToSourceObjectTable*
   ArchiveHeapWriter::_buffer_offset_to_source_obj_table = nullptr;
 
+
+typedef ResourceHashtable<address, size_t,
+      127, // prime number
+      AnyObj::C_HEAP,
+      mtClassShared> FillersTable;
+static FillersTable* _fillers;
+
 void ArchiveHeapWriter::init() {
   if (HeapShared::can_write()) {
     Universe::heap()->collect(GCCause::_java_lang_system_gc);
 
     _buffer_offset_to_source_obj_table = new BufferOffsetToSourceObjectTable();
-
+    _fillers = new FillersTable();
     _requested_bottom = nullptr;
     _requested_top = nullptr;
 
@@ -255,7 +262,7 @@ int ArchiveHeapWriter::filler_array_length(size_t fill_bytes) {
   return -1;
 }
 
-void ArchiveHeapWriter::init_filler_array_at_buffer_top(int array_length, size_t fill_bytes) {
+HeapWord* ArchiveHeapWriter::init_filler_array_at_buffer_top(int array_length, size_t fill_bytes) {
   assert(UseCompressedClassPointers, "Archived heap only supported for compressed klasses");
   Klass* oak = Universe::objectArrayKlassObj(); // already relocated to point to archived klass
   HeapWord* mem = offset_to_buffered_address<HeapWord*>(_buffer_used);
@@ -264,6 +271,7 @@ void ArchiveHeapWriter::init_filler_array_at_buffer_top(int array_length, size_t
   narrowKlass nk = ArchiveBuilder::current()->get_requested_narrow_klass(oak);
   cast_to_oop(mem)->set_narrow_klass(nk);
   arrayOopDesc::set_length(mem, array_length);
+  return mem;
 }
 
 void ArchiveHeapWriter::maybe_fill_gc_region_gap(size_t required_byte_size) {
@@ -293,9 +301,19 @@ void ArchiveHeapWriter::maybe_fill_gc_region_gap(size_t required_byte_size) {
     int array_length = filler_array_length(fill_bytes);
     log_info(cds, heap)("Inserting filler obj array of %d elements (" SIZE_FORMAT " bytes total) @ buffer offset " SIZE_FORMAT,
                         array_length, fill_bytes, _buffer_used);
-    init_filler_array_at_buffer_top(array_length, fill_bytes);
-
+    HeapWord* filler = init_filler_array_at_buffer_top(array_length, fill_bytes);
     _buffer_used = filler_end;
+    _fillers->put((address)filler, fill_bytes);
+  }
+}
+
+size_t ArchiveHeapWriter::get_filler_size_at(address buffered_addr) {
+  size_t* p = _fillers->get(buffered_addr);
+  if (p != nullptr) {
+    assert(*p > 0, "filler must be larger than zero bytes");
+    return *p;
+  } else {
+    return 0; // buffered_addr is not a filler
   }
 }
 
@@ -514,6 +532,20 @@ void ArchiveHeapWriter::mark_native_pointer(oop src_obj, int field_offset) {
   }
 }
 
+// Do we have a jlong/jint field that's actually a pointer to a MetaspaceObj?
+bool ArchiveHeapWriter::is_marked_as_native_pointer(ArchiveHeapInfo* heap_info, oop src_obj, int field_offset) {
+  HeapShared::CachedOopInfo* p = HeapShared::archived_object_cache()->get(src_obj);
+  assert(p != nullptr, "must be");
+
+  // requested_field_addr = the address of this field in the requested space
+  oop requested_obj = requested_obj_from_buffer_offset(p->buffer_offset());
+  Metadata** requested_field_addr = (Metadata**)(cast_from_oop<address>(requested_obj) + field_offset);
+  assert((Metadata**)_requested_bottom <= requested_field_addr && requested_field_addr < (Metadata**) _requested_top, "range check");
+
+  BitMap::idx_t idx = requested_field_addr - (Metadata**) _requested_bottom;
+  return (idx < heap_info->ptrmap()->size()) && (heap_info->ptrmap()->at(idx) == true);
+}
+
 void ArchiveHeapWriter::compute_ptrmap(ArchiveHeapInfo* heap_info) {
   int num_non_null_ptrs = 0;
   Metadata** bottom = (Metadata**) _requested_bottom;
diff --git a/src/hotspot/share/cds/archiveHeapWriter.hpp b/src/hotspot/share/cds/archiveHeapWriter.hpp
index 546fcea2a2f..889646b78c8 100644
--- a/src/hotspot/share/cds/archiveHeapWriter.hpp
+++ b/src/hotspot/share/cds/archiveHeapWriter.hpp
@@ -188,7 +188,7 @@ private:
   static void maybe_fill_gc_region_gap(size_t required_byte_size);
   static size_t filler_array_byte_size(int length);
   static int filler_array_length(size_t fill_bytes);
-  static void init_filler_array_at_buffer_top(int array_length, size_t fill_bytes);
+  static HeapWord* init_filler_array_at_buffer_top(int array_length, size_t fill_bytes);
 
   static void set_requested_address(ArchiveHeapInfo* info);
   static void relocate_embedded_oops(GrowableArrayCHeap<oop, mtClassShared>* roots, ArchiveHeapInfo* info);
@@ -225,8 +225,10 @@ public:
   static size_t heap_roots_word_size() {
     return _heap_roots_word_size;
   }
+  static size_t get_filler_size_at(address buffered_addr);
 
   static void mark_native_pointer(oop src_obj, int offset);
+  static bool is_marked_as_native_pointer(ArchiveHeapInfo* heap_info, oop src_obj, int field_offset);
   static oop source_obj_to_requested_obj(oop src_obj);
   static oop buffered_addr_to_source_obj(address buffered_addr);
   static address buffered_addr_to_requested_addr(address buffered_addr);
diff --git a/src/hotspot/share/cds/heapShared.hpp b/src/hotspot/share/cds/heapShared.hpp
index 1a2a30f393e..d0f38219d85 100644
--- a/src/hotspot/share/cds/heapShared.hpp
+++ b/src/hotspot/share/cds/heapShared.hpp
@@ -381,6 +381,7 @@ private:
   // Dump-time only. Returns the index of the root, which can be used at run time to read
   // the root using get_root(index, ...).
   static int append_root(oop obj);
+  static GrowableArrayCHeap<oop, mtClassShared>* pending_roots() { return _pending_roots; }
 
   // Dump-time and runtime
   static objArrayOop roots();
diff --git a/src/hotspot/share/oops/typeArrayKlass.cpp b/src/hotspot/share/oops/typeArrayKlass.cpp
index 899531d33cd..e79854147e5 100644
--- a/src/hotspot/share/oops/typeArrayKlass.cpp
+++ b/src/hotspot/share/oops/typeArrayKlass.cpp
@@ -276,8 +276,6 @@ void TypeArrayKlass::print_value_on(outputStream* st) const {
   st->print("}");
 }
 
-#ifndef PRODUCT
-
 static void print_boolean_array(typeArrayOop ta, int print_len, outputStream* st) {
   for (int index = 0; index < print_len; index++) {
     st->print_cr(" - %3d: %s", index, (ta->bool_at(index) == 0) ? "false" : "true");
@@ -341,7 +339,10 @@ static void print_long_array(typeArrayOop ta, int print_len, outputStream* st) {
 
 void TypeArrayKlass::oop_print_on(oop obj, outputStream* st) {
   ArrayKlass::oop_print_on(obj, st);
-  typeArrayOop ta = typeArrayOop(obj);
+  oop_print_elements_on(typeArrayOop(obj), st);
+}
+
+void TypeArrayKlass::oop_print_elements_on(typeArrayOop ta, outputStream* st) {
   int print_len = MIN2((intx) ta->length(), MaxElementPrintSize);
   switch (element_type()) {
     case T_BOOLEAN: print_boolean_array(ta, print_len, st); break;
@@ -360,8 +361,6 @@ void TypeArrayKlass::oop_print_on(oop obj, outputStream* st) {
   }
 }
 
-#endif // PRODUCT
-
 const char* TypeArrayKlass::internal_name() const {
   return Klass::external_name();
 }
diff --git a/src/hotspot/share/oops/typeArrayKlass.hpp b/src/hotspot/share/oops/typeArrayKlass.hpp
index 5d4a739936f..48198d55110 100644
--- a/src/hotspot/share/oops/typeArrayKlass.hpp
+++ b/src/hotspot/share/oops/typeArrayKlass.hpp
@@ -123,10 +123,8 @@ class TypeArrayKlass : public ArrayKlass {
 
  public:
   // Printing
-#ifndef PRODUCT
   void oop_print_on(oop obj, outputStream* st);
-#endif
-
+  void oop_print_elements_on(typeArrayOop ta, outputStream* st);
   void print_on(outputStream* st) const;
   void print_value_on(outputStream* st) const;
 
diff --git a/test/hotspot/jtreg/runtime/cds/CDSMapReader.java b/test/hotspot/jtreg/runtime/cds/CDSMapReader.java
new file mode 100644
index 00000000000..8ad53dc6596
--- /dev/null
+++ b/test/hotspot/jtreg/runtime/cds/CDSMapReader.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+// This is a simple parser for parsing the output of
+//
+//   java -Xshare:dump -Xlog:cds+map=debug,cds+map+oops=trace:file=cds.map:none:filesize=0
+//
+// Currently it just check the output related to JDK-8308903.
+// I.e., each oop fields in the HeapObjects must point to a valid HeapObject.
+//
+// It can be extended to check for the other parts of the map file, or perform
+// more analysis on the HeapObjects.
+public class CDSMapReader {
+    public static class MapFile {
+        ArrayList<HeapObject> heapObjects = new ArrayList<>();
+        HashMap<Long, HeapObject> oopToObject = new HashMap<>();
+        HashMap<Long, HeapObject> narrowOopToObject = new HashMap<>();
+
+        void add(HeapObject heapObject) {
+            heapObjects.add(heapObject);
+            oopToObject.put(heapObject.address.oop, heapObject);
+            if (heapObject.address.narrowOop != 0) {
+                narrowOopToObject.put(heapObject.address.narrowOop, heapObject);
+            }
+        }
+
+        public int heapObjectCount() {
+            return heapObjects.size();
+        }
+    }
+
+    public static class HeapAddress {
+        long oop;
+        long narrowOop;
+
+        HeapAddress(String oopStr, String narrowOopStr) {
+            oop = Long.parseUnsignedLong(oopStr, 16);
+            if (narrowOopStr != null) {
+                narrowOop = Long.parseUnsignedLong(narrowOopStr, 16);
+            }
+        }
+    }
+
+    public static class Klass {
+        long address;
+        String name;
+
+        static Klass getKlass(String name, String addr) {
+            // TODO: look up from a table of known Klasses
+            Klass k = new Klass();
+            k.name = name;
+            k.address =  Long.parseUnsignedLong(addr, 16);
+            return k;
+        }
+    }
+
+    public static class HeapObject {
+        HeapAddress address;
+        ArrayList<Field> fields;
+        String className;
+        Klass klass;
+
+        HeapObject(String className, String oop, String narrowOop) {
+            this.className = className;
+            address = new HeapAddress(oop, narrowOop);
+        }
+
+        void setKlass(String klassName, String address) {
+            klass = Klass.getKlass(klassName, address);
+        }
+
+        void addOopField(String name, String offset, String oopStr, String narrowOopStr) {
+            if (fields == null) {
+                fields = new ArrayList<Field>();
+            }
+            fields.add(new Field(name, offset, oopStr, narrowOopStr));
+        }
+    }
+
+    public static class Field {
+        String name;
+        int offset;
+        HeapAddress referentAddress; // non-null iff this is an object field
+        int lineCount;
+
+        Field(String name, String offset, String oopStr, String narrowOopStr) {
+            this.name = name;
+            this.offset = Integer.parseInt(offset);
+            this.referentAddress = new HeapAddress(oopStr, narrowOopStr);
+            this.lineCount = CDSMapReader.lineCount;
+        }
+    }
+
+    // 0x00000007ffc00000:   4a5b8701 00000063 00010290 00000000 00010100 fff80003
+    static Pattern rawDataPattern = Pattern.compile("^0x([0-9a-f]+): *( [0-9a-f]+)+ *$");
+
+    // (one address)
+    // 0x00000007ffc00000: @@ Object java.lang.String
+    static Pattern objPattern1 = Pattern.compile("^0x([0-9a-f]+): @@ Object (.*)");
+
+    // (two addresses)
+    // 0x00000007ffc00000: @@ Object (0xfff80000) java.lang.String
+    static Pattern objPattern2 = Pattern.compile("^0x([0-9a-f]+): @@ Object [(]0x([0-9a-f]+)[)] (.*)");
+
+    //  - klass: 'java/lang/String' 0x0000000800010290
+    static Pattern instanceObjKlassPattern = Pattern.compile("^ - klass: '([^']+)' 0x([0-9a-f]+)");
+
+    //  - klass: {type array byte} 0x00000008000024c8
+    static Pattern typeArrayKlassPattern = Pattern.compile("^ - klass: [{]type array ([a-z]+)[}] 0x([0-9a-f]+)");
+
+    //  - klass: 'java/lang/Object'[] 0x00000008000013e0
+    static Pattern objArrayKlassPattern = Pattern.compile("^ - klass: ('[^']+'(\\[\\])+) 0x([0-9a-f]+)");
+
+    //  - fields (3 words):
+    static Pattern fieldsWordsPattern = Pattern.compile("^ - fields [(]([0-9]+) words[)]:$");
+
+    // (one address)
+    //  - final 'key' 'Ljava/lang/Object;' @16 0x00000007ffc68260 java.lang.String
+    static Pattern oopFieldPattern1 = Pattern.compile(" - [^']* '([^']+)'.*@([0-9]+) 0x([0-9a-f]+) (.*)");
+
+    // (two addresses)
+    //  - final 'key' 'Ljava/lang/Object;' @16 0x00000007ffc68260 (0xfff8d04c) java.lang.String
+    static Pattern oopFieldPattern2 = Pattern.compile(" - [^']* '([^']+)'.*@([0-9]+) 0x([0-9a-f]+) [(]0x([0-9a-f]+)[)] (.*)");
+
+    private static Matcher match(String line, Pattern pattern) {
+        Matcher m = pattern.matcher(line);
+        if (m.find()) {
+            return m;
+        } else {
+            return null;
+        }
+    }
+
+    private static void parseHeapObject(String className, String oop, String narrowOop) throws IOException {
+        HeapObject heapObject = parseHeapObjectImpl(className, oop, narrowOop);
+        mapFile.add(heapObject);
+    }
+
+    private static HeapObject parseHeapObjectImpl(String className, String oop, String narrowOop) throws IOException {
+        HeapObject heapObject = new HeapObject(className, oop, narrowOop);
+        Matcher m;
+
+        nextLine();
+        while (line != null && match(line, rawDataPattern) != null) { // skip raw data
+            nextLine();
+        }
+
+        if (line == null || !line.startsWith(" - ")) {
+            return heapObject;
+        }
+
+        if ((m = match(line, instanceObjKlassPattern)) != null) {
+            heapObject.setKlass(m.group(1), m.group(2));
+            nextLine();
+            if ((m = match(line, fieldsWordsPattern)) == null) {
+                throw new RuntimeException("Expected field size info");
+            }
+            // TODO: read all the array elements
+            while (true) {
+                nextLine();
+                if (line == null || !line.startsWith(" - ")) {
+                    return heapObject;
+                }
+                if (!line.contains("marked metadata pointer")) {
+                    if ((m = match(line, oopFieldPattern2)) != null) {
+                        heapObject.addOopField(m.group(1), m.group(2), m.group(3), m.group(4));
+                    } else if ((m = match(line, oopFieldPattern1)) != null) {
+                        heapObject.addOopField(m.group(1), m.group(2), m.group(3), null);
+                    }
+                }
+            }
+        } else if ((m = match(line, typeArrayKlassPattern)) != null) {
+            heapObject.setKlass(m.group(1), m.group(2));
+            // TODO: read all the array elements
+            while (true) {
+                nextLine();
+                if (line == null || !line.startsWith(" - ")) {
+                    return heapObject;
+                }
+            }
+        } else if ((m = match(line, objArrayKlassPattern)) != null) {
+            heapObject.setKlass(m.group(1), m.group(3));
+            // TODO: read all the array elements
+            while (true) {
+                nextLine();
+                if (line == null || !line.startsWith(" - ")) {
+                    return heapObject;
+                }
+            }
+        } else {
+            throw new RuntimeException("Expected klass info");
+        }
+    }
+
+    static MapFile mapFile;
+    static BufferedReader reader;
+    static String line = null; // current line being parsed
+    static int lineCount = 0;
+    static String nextLine()  throws IOException {
+        line = reader.readLine();
+        ++ lineCount;
+        return line;
+    }
+
+    public static MapFile read(String fileName) {
+        mapFile = new MapFile();
+
+        try (BufferedReader r = new BufferedReader(new FileReader(fileName))) {
+            reader = r;
+            nextLine();
+
+            Matcher m;
+            while (line != null) {
+                if ((m = match(line, objPattern2)) != null) {
+                    parseHeapObject(m.group(3), m.group(1), m.group(2));
+                } else if ((m = match(line, objPattern1)) != null) {
+                    parseHeapObject(m.group(2), m.group(1), null);
+                } else {
+                    nextLine();
+                }
+            }
+            return mapFile;
+        } catch (Throwable t) {
+            System.out.println("Error parsing line " + lineCount + ": " + line);
+            throw new RuntimeException(t);
+        } finally {
+            System.out.println("Parsed " + lineCount + " lines in " + fileName);
+            System.out.println("Found "  + mapFile.heapObjectCount() + " heap objects");
+            mapFile = null;
+            reader = null;
+            line = null;
+            lineCount = 0;
+        }
+    }
+
+    private static void mustContain(HashMap<Long, HeapObject> allObjects, Field field, long pointer, boolean isNarrow) {
+        if (allObjects.get(pointer) == null) {
+            throw new RuntimeException((isNarrow ? "narrowOop" : "oop") + " pointer 0x" + Long.toHexString(pointer) +
+                                       " on line " + field.lineCount + " doesn't point to a valid heap object");
+        }
+    }
+
+    // Check that each oop fields in the HeapObjects must point to a valid HeapObject.
+    public static int validate(MapFile mapFile) {
+        int count = 0;
+        for (HeapObject heapObject : mapFile.heapObjects) {
+            if (heapObject.fields != null) {
+                for (Field field : heapObject.fields) {
+                    HeapAddress referentAddress = field.referentAddress;
+                    long oop = referentAddress.oop;
+                    long narrowOop = referentAddress.narrowOop;
+                    // Is this test actually doing something?
+                    //     To see how an invalidate pointer may be found, change oop in the
+                    //     following line to oop+1
+                    mustContain(mapFile.oopToObject, field, oop, false);
+                    count ++;
+                    if (narrowOop != 0) {
+                        mustContain(mapFile.narrowOopToObject, field, narrowOop, true);
+                        count ++;
+                    }
+                }
+            }
+        }
+        System.out.println("Checked " + count + " oop field references");
+        return count;
+    }
+
+    public static void main(String args[]) {
+        MapFile mapFile = read(args[0]);
+        validate(mapFile);
+    }
+}
diff --git a/test/hotspot/jtreg/runtime/cds/CDSMapTest.java b/test/hotspot/jtreg/runtime/cds/CDSMapTest.java
new file mode 100644
index 00000000000..c2a53cac42d
--- /dev/null
+++ b/test/hotspot/jtreg/runtime/cds/CDSMapTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2023, 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.
+ */
+
+/**
+ * @test
+ * @bug 8308903
+ * @summary Test the contents of -Xlog:cds+map
+ * @requires vm.cds
+ * @library /test/lib
+ * @run driver CDSMapTest
+ */
+
+import jdk.test.lib.cds.CDSOptions;
+import jdk.test.lib.cds.CDSTestUtils;
+import jdk.test.lib.Platform;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class CDSMapTest {
+    public static void main(String[] args) throws Exception {
+        doTest(false);
+
+        if (Platform.is64bit()) {
+            // There's no oop/klass compression on 32-bit.
+            doTest(true);
+        }
+    }
+
+    public static void doTest(boolean compressed) throws Exception {
+        ArrayList<String> dumpArgs = new ArrayList<>();
+
+        // Use the same heap size as make/Images.gmk
+        dumpArgs.add("-Xmx128M");
+
+        if (Platform.is64bit()) {
+            // These options are available only on 64-bit.
+            String sign = (compressed) ?  "+" : "-";
+            dumpArgs.add("-XX:" + sign + "UseCompressedOops");
+        }
+
+        dump(dumpArgs);
+    }
+
+    static int id = 0;
+    static void dump(ArrayList<String> args, String... more) throws Exception {
+        String logName = "SharedArchiveFile" + (id++);
+        String archiveName = logName + ".jsa";
+        String mapName = logName + ".map";
+        CDSOptions opts = (new CDSOptions())
+            .addPrefix("-Xlog:cds=debug")
+            .addPrefix("-Xlog:cds+map=debug,cds+map+oops=trace:file=" + mapName + ":none:filesize=0")
+            .setArchiveName(archiveName)
+            .addSuffix(args)
+            .addSuffix(more);
+        CDSTestUtils.createArchiveAndCheck(opts);
+
+        CDSMapReader.MapFile mapFile = CDSMapReader.read(mapName);
+        int oopFieldCount = CDSMapReader.validate(mapFile);
+        if (mapFile.heapObjectCount() > 0 && oopFieldCount < 10000) {
+            // heapObjectCount() may be zero if the selected GC doesn't support heap object archiving.
+            throw new RuntimeException("CDS map file seems incorrect: " + mapFile.heapObjectCount() +
+                                       " objects but only " + oopFieldCount + " oop field references");
+        }
+    }
+}
diff --git a/test/hotspot/jtreg/runtime/cds/DeterministicDump.java b/test/hotspot/jtreg/runtime/cds/DeterministicDump.java
index 25c6148d326..57e4000fe7a 100644
--- a/test/hotspot/jtreg/runtime/cds/DeterministicDump.java
+++ b/test/hotspot/jtreg/runtime/cds/DeterministicDump.java
@@ -80,7 +80,7 @@ public class DeterministicDump {
         String mapName = logName + ".map";
         CDSOptions opts = (new CDSOptions())
             .addPrefix("-Xlog:cds=debug")
-            .addPrefix("-Xlog:cds+map=trace:file=" + mapName + ":none:filesize=0")
+            .addPrefix("-Xlog:cds+map*=trace:file=" + mapName + ":none:filesize=0")
             .setArchiveName(archiveName)
             .addSuffix(args)
             .addSuffix(more);