diff --git a/test/hotspot/jtreg/runtime/cds/appcds/applications/JavacBench.java b/test/hotspot/jtreg/runtime/cds/appcds/applications/JavacBench.java
new file mode 100644
index 0000000000000000000000000000000000000000..50696c8c1fcc35983446b06b831666b36b494a54
--- /dev/null
+++ b/test/hotspot/jtreg/runtime/cds/appcds/applications/JavacBench.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2023, 2024, 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 id=static
+ * @summary Run JavacBenchApp with the classic static archive workflow
+ * @requires vm.cds
+ * @library /test/lib
+ * @run driver JavacBench STATIC
+ */
+
+/*
+ * @test id=dynamic
+ * @summary Run JavacBenchApp with the classic dynamic archive workflow
+ * @requires vm.cds
+ * @library /test/lib
+ * @run driver JavacBench DYNAMIC
+ */
+
+import jdk.test.lib.cds.CDSAppTester;
+import jdk.test.lib.helpers.ClassFileInstaller;
+
+public class JavacBench {
+    static String mainClass = JavacBenchApp.class.getName();
+    static String appJar;
+
+    public static void main(String args[]) throws Exception {
+        appJar = ClassFileInstaller.writeJar("JavacBenchApp.jar",
+                                             "JavacBenchApp",
+                                             "JavacBenchApp$ClassFile",
+                                             "JavacBenchApp$FileManager",
+                                             "JavacBenchApp$SourceFile");
+        JavacBenchTester tester = new JavacBenchTester();
+        tester.run(args);
+    }
+
+    static class JavacBenchTester extends CDSAppTester {
+        public JavacBenchTester() {
+            super("JavacBench");
+        }
+
+        @Override
+        public String classpath(RunMode runMode) {
+            return appJar;
+        }
+
+        @Override
+        public String[] appCommandLine(RunMode runMode) {
+            return new String[] {
+                mainClass,
+                "90",
+            };
+        }
+    }
+}
diff --git a/test/hotspot/jtreg/runtime/cds/appcds/applications/JavacBenchApp.java b/test/hotspot/jtreg/runtime/cds/appcds/applications/JavacBenchApp.java
new file mode 100644
index 0000000000000000000000000000000000000000..a32069883af159b7529b8bae56964ca8e1b203dc
--- /dev/null
+++ b/test/hotspot/jtreg/runtime/cds/appcds/applications/JavacBenchApp.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (c) 2023, 2024, 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.lang.invoke.MethodHandles;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.FileObject;
+import javax.tools.ForwardingJavaFileManager;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.ToolProvider;
+
+/**
+ * This program tries to compile a large number of classes that exercise a fair amount of
+ * features in javac.
+ */
+public class JavacBenchApp {
+    static class ClassFile extends SimpleJavaFileObject {
+        private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        protected ClassFile(String name) {
+            super(URI.create("memo:///" + name.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS);
+        }
+        @Override
+        public ByteArrayOutputStream openOutputStream() {
+            return this.baos;
+        }
+        byte[] toByteArray() {
+            return baos.toByteArray();
+        }
+    }
+
+    static class FileManager extends ForwardingJavaFileManager<JavaFileManager> {
+        private Map<String, ClassFile> classesMap = new HashMap<String, ClassFile>();
+        protected FileManager(JavaFileManager fileManager) {
+            super(fileManager);
+        }
+        @Override
+        public ClassFile getJavaFileForOutput(Location location, String name, JavaFileObject.Kind kind, FileObject source) {
+            ClassFile classFile = new ClassFile(name);
+            classesMap.put(name, classFile);
+            return classFile;
+        }
+        public Map<String, byte[]> getCompiledClasses() {
+            Map<String, byte[]> result = new HashMap<>();
+            for (Map.Entry<String, ClassFile> entry : classesMap.entrySet()) {
+                result.put(entry.getKey(), entry.getValue().toByteArray());
+            }
+            return result;
+        }
+    }
+
+    static class SourceFile extends SimpleJavaFileObject {
+        private CharSequence sourceCode;
+        public SourceFile(String name, CharSequence sourceCode) {
+            super(URI.create("memo:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
+            this.sourceCode = sourceCode;
+        }
+        @Override
+        public CharSequence getCharContent(boolean ignore) {
+            return this.sourceCode;
+        }
+    }
+
+    public Map<String, byte[]> compile() {
+        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+        DiagnosticCollector<JavaFileObject> ds = new DiagnosticCollector<>();
+        Collection<SourceFile> sourceFiles = sources;
+
+        try (FileManager fileManager = new FileManager(compiler.getStandardFileManager(ds, null, null))) {
+            JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, sourceFiles);
+            if (task.call()) {
+                return fileManager.getCompiledClasses();
+            } else {
+                for (Diagnostic<? extends JavaFileObject> d : ds.getDiagnostics()) {
+                    System.out.format("Line: %d, %s in %s", d.getLineNumber(), d.getMessage(null), d.getSource().getName());
+                }
+                throw new InternalError("compilation failure");
+            }
+        } catch (IOException e) {
+            throw new InternalError(e);
+        }
+    }
+
+    List<SourceFile> sources;
+
+    static final String imports = """
+        import java.lang.*;
+        import java.util.*;
+        """;
+
+    static final String testClassBody = """
+        // Some comments
+        static long x;
+        static final long y;
+        static {
+            y = System.currentTimeMillis();
+        }
+        /* More comments */
+        @Deprecated
+        String func() { return "String " + this + y; }
+        public static void main(String args[]) {
+            try {
+                x = Long.parseLong(args[0]);
+            } catch (Throwable t) {
+                t.printStackTrace();
+            }
+            doit(() -> {
+                System.out.println("Hello Lambda");
+                Thread.dumpStack();
+            });
+        }
+        static List<String> list = List.of("1", "2");
+        class InnerClass1 {
+            static final long yy = y;
+        }
+        static void doit(Runnable r) {
+            for (var x : list) {
+                r.run();
+            }
+        }
+        static String patternMatch(String arg, Object o) {
+            if (o instanceof String s) {
+                return "1234";
+            }
+            final String b = "B";
+            return switch (arg) {
+                case "A" -> "a";
+                case b   -> "b";
+                default  -> "c";
+            };
+        }
+        public sealed class SealedInnerClass {}
+        public final class Foo extends SealedInnerClass {}
+        enum Expression {
+            ADDITION,
+            SUBTRACTION,
+            MULTIPLICATION,
+            DIVISION
+        }
+        public record Point(int x, int y) {
+            public Point(int x) {
+                this(x, 0);
+            }
+        }
+        """;
+
+    String sanitySource = """
+        public class Sanity implements java.util.concurrent.Callable<String> {
+            public String call() {
+                return "this is a test";
+            }
+        }
+        """;
+
+    void setup(int count) {
+        sources = new ArrayList<>(count);
+        for (int i = 0; i < count; i++) {
+            String source = imports + "public class Test" + i + " {" + testClassBody + "}";
+            sources.add(new SourceFile("Test" + i, source));
+        }
+
+        sources.add(new SourceFile("Sanity", sanitySource));
+    }
+
+    @SuppressWarnings("unchecked")
+    static void validate(byte[] sanityClassFile) throws Throwable {
+        MethodHandles.Lookup lookup = MethodHandles.lookup();
+        Class<?> cls = lookup.defineClass(sanityClassFile);
+        Callable<String> obj = (Callable<String>)cls.getDeclaredConstructor().newInstance();
+        String s = obj.call();
+        if (!s.equals("this is a test")) {
+            throw new RuntimeException("Expected \"this is a test\", but got \"" + s + "\"");
+        }
+    }
+
+    public static void main(String args[]) throws Throwable {
+        long started = System.currentTimeMillis();
+        JavacBenchApp bench = new JavacBenchApp();
+
+        int count = 0;
+        if (args.length > 0) {
+            count = Integer.parseInt(args[0]);
+            if (count >= 0) {
+                bench.setup(count);
+                Map<String, byte[]> allClasses = bench.compile();
+                validate(allClasses.get("Sanity"));
+            }
+        }
+        if (System.getProperty("JavacBenchApp.silent") == null) {
+            // Set this property when running with "perf stat", etc
+            long elapsed = System.currentTimeMillis() - started;
+            System.out.println("Generated source code for " + bench.sources.size() + " classes and compiled them in " + elapsed + " ms");
+        }
+    }
+}
+
diff --git a/test/lib/jdk/test/lib/StringArrayUtils.java b/test/lib/jdk/test/lib/StringArrayUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..11124701dcebad4b82ebf272b16827e309e78646
--- /dev/null
+++ b/test/lib/jdk/test/lib/StringArrayUtils.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2023, 2024, 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.
+ */
+
+package jdk.test.lib;
+
+import java.util.ArrayList;
+
+public class StringArrayUtils {
+    /**
+     * The various concat() functions in this class can be used for building
+     * a command-line argument array for ProcessTools.createTestJavaProcessBuilder(),
+     * etc. When some of the arguments are conditional, this is more convenient
+     * than alternatives like ArrayList.
+     *
+     * Example:
+     *
+     * <pre>
+     *     String args[] = StringArrayUtils.concat("-Xint", "-Xmx32m");
+     *     if (verbose) {
+     *         args = StringArrayUtils.concat(args, "-verbose");
+     *     }
+     *     args = StringArrayUtils.concat(args, "HelloWorld");
+     *     ProcessTools.createTestJavaProcessBuilder(args);
+     * </pre>
+     */
+    public static String[] concat(String... args) {
+        return args;
+    }
+
+    public static String[] concat(String[] prefix, String... extra) {
+        String[] ret = new String[prefix.length + extra.length];
+        System.arraycopy(prefix, 0, ret, 0, prefix.length);
+        System.arraycopy(extra, 0, ret, prefix.length, extra.length);
+        return ret;
+    }
+
+    public static String[] concat(String prefix, String[] extra) {
+        String[] ret = new String[1 + extra.length];
+        ret[0] = prefix;
+        System.arraycopy(extra, 0, ret, 1, extra.length);
+        return ret;
+    }
+}
diff --git a/test/lib/jdk/test/lib/cds/CDSAppTester.java b/test/lib/jdk/test/lib/cds/CDSAppTester.java
new file mode 100644
index 0000000000000000000000000000000000000000..c39e6bb8e9489c2a235fadaf413249e9c1b49f34
--- /dev/null
+++ b/test/lib/jdk/test/lib/cds/CDSAppTester.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2023, 2024, 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.
+ */
+
+package jdk.test.lib.cds;
+
+import java.io.File;
+import jdk.test.lib.cds.CDSTestUtils;
+import jdk.test.lib.process.ProcessTools;
+import jdk.test.lib.process.OutputAnalyzer;
+import jdk.test.lib.StringArrayUtils;
+
+/*
+ * This is a base class used for testing CDS functionalities with complex applications.
+ * You can define the application by overridding the vmArgs(), classpath() and appCommandLine()
+ * methods. Application-specific validation checks can be implemented with checkExecution().
+*/
+abstract public class CDSAppTester {
+    private final String name;
+    private final String classListFile;
+    private final String classListFileLog;
+    private final String staticArchiveFile;
+    private final String staticArchiveFileLog;
+    private final String dynamicArchiveFile;
+    private final String dynamicArchiveFileLog;
+    private final String productionRunLog;
+
+    public CDSAppTester(String name) {
+        // Old workflow
+        this.name = name;
+        classListFile = name() + ".classlist";
+        classListFileLog = classListFile + ".log";
+        staticArchiveFile = name() + ".static.jsa";
+        staticArchiveFileLog = staticArchiveFile + ".log";
+        dynamicArchiveFile = name() + ".dynamic.jsa";
+        dynamicArchiveFileLog = dynamicArchiveFile + ".log";
+        productionRunLog = name() + ".production.log";
+    }
+
+    private enum Workflow {
+        STATIC,        // classic -Xshare:dump workflow
+        DYNAMIC,       // classic -XX:ArchiveClassesAtExit
+    }
+
+    public enum RunMode {
+        CLASSLIST,
+        DUMP_STATIC,
+        DUMP_DYNAMIC,
+        PRODUCTION;
+
+        public boolean isStaticDump() {
+            return this == DUMP_STATIC;
+        }
+        public boolean isProductionRun() {
+            return this == PRODUCTION;
+        }
+    }
+
+    public final String name() {
+        return this.name;
+    }
+
+    // optional
+    public String[] vmArgs(RunMode runMode) {
+        return new String[0];
+    }
+
+    // optional
+    public String classpath(RunMode runMode) {
+        return null;
+    }
+
+    // must override
+    // main class, followed by arguments to the main class
+    abstract public String[] appCommandLine(RunMode runMode);
+
+    // optional
+    public void checkExecution(OutputAnalyzer out, RunMode runMode) throws Exception {}
+
+    private Workflow workflow;
+
+    public final boolean isStaticWorkflow() {
+        return workflow == Workflow.STATIC;
+    }
+
+    public final boolean isDynamicWorkflow() {
+        return workflow == Workflow.DYNAMIC;
+    }
+
+    private String logToFile(String logFile, String... logTags) {
+        StringBuilder sb = new StringBuilder("-Xlog:");
+        String prefix = "";
+        for (String tag : logTags) {
+            sb.append(prefix);
+            sb.append(tag);
+            prefix = ",";
+        }
+        sb.append(":file=" + logFile + "::filesize=0");
+        return sb.toString();
+    }
+
+    private void listOutputFile(String file) {
+        File f = new File(file);
+        if (f.exists()) {
+            System.out.println("[output file: " + file + " " + f.length() + " bytes]");
+        } else {
+            System.out.println("[output file: " + file + " does not exist]");
+        }
+    }
+
+    private OutputAnalyzer executeAndCheck(String[] cmdLine, RunMode runMode, String... logFiles) throws Exception {
+        ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(cmdLine);
+        Process process = pb.start();
+        OutputAnalyzer output = CDSTestUtils.executeAndLog(process, runMode.toString());
+        for (String logFile : logFiles) {
+            listOutputFile(logFile);
+        }
+        output.shouldHaveExitValue(0);
+        CDSTestUtils.checkCommonExecExceptions(output);
+        checkExecution(output, runMode);
+        return output;
+    }
+
+    private OutputAnalyzer createClassList() throws Exception {
+        RunMode runMode = RunMode.CLASSLIST;
+        String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode),
+                                                   "-Xshare:off",
+                                                   "-XX:DumpLoadedClassList=" + classListFile,
+                                                   "-cp", classpath(runMode),
+                                                   logToFile(classListFileLog,
+                                                             "class+load=debug"));
+        cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode));
+        return executeAndCheck(cmdLine, runMode, classListFile, classListFileLog);
+    }
+
+    private OutputAnalyzer dumpStaticArchive() throws Exception {
+        RunMode runMode = RunMode.DUMP_STATIC;
+        String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode),
+                                                   "-Xlog:cds",
+                                                   "-Xlog:cds+heap=error",
+                                                   "-Xshare:dump",
+                                                   "-XX:SharedArchiveFile=" + staticArchiveFile,
+                                                   "-XX:SharedClassListFile=" + classListFile,
+                                                   "-cp", classpath(runMode),
+                                                   logToFile(staticArchiveFileLog,
+                                                             "cds=debug",
+                                                             "cds+class=debug",
+                                                             "cds+heap=warning",
+                                                             "cds+resolve=debug"));
+        return executeAndCheck(cmdLine, runMode, staticArchiveFile, staticArchiveFileLog);
+    }
+
+    private OutputAnalyzer dumpDynamicArchive() throws Exception {
+        RunMode runMode = RunMode.DUMP_DYNAMIC;
+        String[] cmdLine = new String[0];
+        if (isDynamicWorkflow()) {
+          // "classic" dynamic archive
+          cmdLine = StringArrayUtils.concat(vmArgs(runMode),
+                                            "-Xlog:cds",
+                                            "-XX:ArchiveClassesAtExit=" + dynamicArchiveFile,
+                                            "-cp", classpath(runMode),
+                                            logToFile(dynamicArchiveFileLog,
+                                                      "cds=debug",
+                                                      "cds+class=debug",
+                                                      "cds+resolve=debug",
+                                                      "class+load=debug"));
+        }
+        cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode));
+        return executeAndCheck(cmdLine, runMode, dynamicArchiveFile, dynamicArchiveFileLog);
+    }
+
+    private OutputAnalyzer productionRun() throws Exception {
+        RunMode runMode = RunMode.PRODUCTION;
+        String[] cmdLine = StringArrayUtils.concat(vmArgs(runMode),
+                                                   "-cp", classpath(runMode),
+                                                   logToFile(productionRunLog, "cds"));
+
+        if (isStaticWorkflow()) {
+            cmdLine = StringArrayUtils.concat(cmdLine, "-XX:SharedArchiveFile=" + staticArchiveFile);
+        } else if (isDynamicWorkflow()) {
+            cmdLine = StringArrayUtils.concat(cmdLine, "-XX:SharedArchiveFile=" + dynamicArchiveFile);
+        }
+
+        cmdLine = StringArrayUtils.concat(cmdLine, appCommandLine(runMode));
+        return executeAndCheck(cmdLine, runMode, productionRunLog);
+    }
+
+    public void run(String args[]) throws Exception {
+        String err = "Must have exactly one command line argument of the following: ";
+        String prefix = "";
+        for (Workflow wf : Workflow.values()) {
+            err += prefix;
+            err += wf;
+            prefix = ", ";
+        }
+        if (args.length != 1) {
+            throw new RuntimeException(err);
+        } else {
+            if (args[0].equals("STATIC")) {
+                runStaticWorkflow();
+            } else if (args[0].equals("DYNAMIC")) {
+                runDynamicWorkflow();
+            } else {
+                throw new RuntimeException(err);
+            }
+        }
+    }
+
+    private void runStaticWorkflow() throws Exception {
+        this.workflow = Workflow.STATIC;
+        createClassList();
+        dumpStaticArchive();
+        productionRun();
+    }
+
+    private void runDynamicWorkflow() throws Exception {
+        this.workflow = Workflow.DYNAMIC;
+        dumpDynamicArchive();
+        productionRun();
+    }
+}