diff --git a/src/hotspot/share/cds/filemap.cpp b/src/hotspot/share/cds/filemap.cpp index 00d8fba4411..91a2a57dee5 100644 --- a/src/hotspot/share/cds/filemap.cpp +++ b/src/hotspot/share/cds/filemap.cpp @@ -53,15 +53,18 @@ #include "memory/oopFactory.hpp" #include "memory/universe.hpp" #include "nmt/memTracker.hpp" +#include "oops/access.hpp" #include "oops/compressedOops.hpp" #include "oops/compressedOops.inline.hpp" #include "oops/compressedKlass.hpp" #include "oops/objArrayOop.hpp" #include "oops/oop.inline.hpp" +#include "oops/typeArrayKlass.hpp" #include "prims/jvmtiExport.hpp" #include "runtime/arguments.hpp" #include "runtime/globals_extension.hpp" #include "runtime/java.hpp" +#include "runtime/javaCalls.hpp" #include "runtime/mutexLocker.hpp" #include "runtime/os.hpp" #include "runtime/vm_version.hpp" @@ -2678,11 +2681,44 @@ ClassFileStream* FileMapInfo::open_stream_for_jvmti(InstanceKlass* ik, Handle cl const char* const file_name = ClassLoader::file_name_for_class_name(class_name, name->utf8_length()); ClassLoaderData* loader_data = ClassLoaderData::class_loader_data(class_loader()); - ClassFileStream* cfs = cpe->open_stream_for_loader(THREAD, file_name, loader_data); + ClassFileStream* cfs; + if (class_loader() != nullptr && !cpe->is_modules_image()) { + cfs = get_stream_from_class_loader(class_loader, cpe, file_name, CHECK_NULL); + } else { + cfs = cpe->open_stream_for_loader(THREAD, file_name, loader_data); + } assert(cfs != nullptr, "must be able to read the classfile data of shared classes for built-in loaders."); log_debug(cds, jvmti)("classfile data for %s [%d: %s] = %d bytes", class_name, path_index, cfs->source(), cfs->length()); return cfs; } +ClassFileStream* FileMapInfo::get_stream_from_class_loader(Handle class_loader, + ClassPathEntry* cpe, + const char* file_name, + TRAPS) { + JavaValue result(T_OBJECT); + TempNewSymbol class_name_sym = SymbolTable::new_symbol(file_name); + Handle ext_class_name = java_lang_String::externalize_classname(class_name_sym, CHECK_NULL); + + // byte[] ClassLoader.getResourceAsByteArray(String name) + JavaCalls::call_virtual(&result, + class_loader, + vmClasses::ClassLoader_klass(), + vmSymbols::getResourceAsByteArray_name(), + vmSymbols::getResourceAsByteArray_signature(), + ext_class_name, + CHECK_NULL); + assert(result.get_type() == T_OBJECT, "just checking"); + oop obj = result.get_oop(); + assert(obj != nullptr, "ClassLoader.getResourceAsByteArray should not return null"); + + // copy from byte[] to a buffer + typeArrayOop ba = typeArrayOop(obj); + jint len = ba->length(); + u1* buffer = NEW_RESOURCE_ARRAY(u1, len); + ArrayAccess<>::arraycopy_to_native<>(ba, typeArrayOopDesc::element_offset(0), buffer, len); + + return new ClassFileStream(buffer, len, cpe->name()); +} #endif diff --git a/src/hotspot/share/cds/filemap.hpp b/src/hotspot/share/cds/filemap.hpp index 6319c51f1ce..cabb54769fe 100644 --- a/src/hotspot/share/cds/filemap.hpp +++ b/src/hotspot/share/cds/filemap.hpp @@ -507,6 +507,10 @@ public: #if INCLUDE_JVMTI // Caller needs a ResourceMark because parts of the returned cfs are resource-allocated. static ClassFileStream* open_stream_for_jvmti(InstanceKlass* ik, Handle class_loader, TRAPS); + static ClassFileStream* get_stream_from_class_loader(Handle class_loader, + ClassPathEntry* cpe, + const char* file_name, + TRAPS); #endif static SharedClassPathEntry* shared_path(int index) { diff --git a/src/hotspot/share/classfile/vmSymbols.hpp b/src/hotspot/share/classfile/vmSymbols.hpp index ca451572de7..6a6f7754c50 100644 --- a/src/hotspot/share/classfile/vmSymbols.hpp +++ b/src/hotspot/share/classfile/vmSymbols.hpp @@ -723,6 +723,8 @@ class SerializeClosure; template(dumpSharedArchive_signature, "(ZLjava/lang/String;)Ljava/lang/String;") \ template(generateLambdaFormHolderClasses, "generateLambdaFormHolderClasses") \ template(generateLambdaFormHolderClasses_signature, "([Ljava/lang/String;)[Ljava/lang/Object;") \ + template(getResourceAsByteArray_name, "getResourceAsByteArray") \ + template(getResourceAsByteArray_signature, "(Ljava/lang/String;)[B") \ template(java_lang_Enum, "java/lang/Enum") \ template(java_lang_invoke_Invokers_Holder, "java/lang/invoke/Invokers$Holder") \ template(java_lang_invoke_DirectMethodHandle_Holder, "java/lang/invoke/DirectMethodHandle$Holder") \ diff --git a/src/java.base/share/classes/java/lang/ClassLoader.java b/src/java.base/share/classes/java/lang/ClassLoader.java index 55341635d8a..b890ba51651 100644 --- a/src/java.base/share/classes/java/lang/ClassLoader.java +++ b/src/java.base/share/classes/java/lang/ClassLoader.java @@ -1685,6 +1685,15 @@ public abstract class ClassLoader { } } + /** + * Called by VM for reading class bytes. + */ + private byte[] getResourceAsByteArray(String name) throws IOException { + Objects.requireNonNull(name); + InputStream is = getResourceAsStream(name); + return is != null ? is.readAllBytes() : null; + } + /** * Open for reading, a resource of the specified name from the search path * used to load classes. This method locates the resource through the diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/ClassFileLoadHook.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/CFLH/ClassFileLoadHook.java similarity index 100% rename from test/hotspot/jtreg/runtime/cds/appcds/jvmti/ClassFileLoadHook.java rename to test/hotspot/jtreg/runtime/cds/appcds/jvmti/CFLH/ClassFileLoadHook.java diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/ClassFileLoadHookTest.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/CFLH/ClassFileLoadHookTest.java similarity index 100% rename from test/hotspot/jtreg/runtime/cds/appcds/jvmti/ClassFileLoadHookTest.java rename to test/hotspot/jtreg/runtime/cds/appcds/jvmti/CFLH/ClassFileLoadHookTest.java diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/CFLH/MultiReleaseJars.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/CFLH/MultiReleaseJars.java new file mode 100644 index 00000000000..92c5e3d5f02 --- /dev/null +++ b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/CFLH/MultiReleaseJars.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 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 + * @summary Test multi-release jar with CFLH + * @requires vm.cds + * @requires vm.jvmti + * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds + * @run main/othervm/native MultiReleaseJars + */ + +import java.io.File; +import java.io.FileOutputStream; +import java.io.PrintStream; +import java.io.IOException; +import jdk.test.lib.cds.CDSTestUtils; +import jdk.test.lib.process.OutputAnalyzer; + +public class MultiReleaseJars { + + static final int BASE_VERSION = 9; + static final String BASE_VERSION_STRING = Integer.toString(BASE_VERSION); + static final int MAJOR_VERSION = Runtime.version().major(); + static final String MAJOR_VERSION_STRING = String.valueOf(MAJOR_VERSION); + + static String getMain() { + String sts = """ + public class Main { + public static void main(String[] args) throws Exception { + System.out.println(Class.forName(\"Foo\")); + System.out.println(Class.forName(\"Bar\")); + } + } + """; + return sts; + } + + static String getFoo() { + String sts = """ + class Foo { + static { + System.out.println("Hello from Foo old version"); + } + } + """; + return sts; + } + + static String getFooNewVersion() { + String sts = """ + class Foo { + static { + System.out.println("Hello from Foo new version"); + } + } + """; + return sts; + } + + static String getBar() { + String sts = """ + class Bar { + static { + System.out.println("Hello from Bar"); + } + } + """; + return sts; + } + + static void writeFile(File file, String... contents) throws Exception { + if (contents == null) { + throw new java.lang.RuntimeException("No input for writing to file" + file); + } + try ( + FileOutputStream fos = new FileOutputStream(file); + PrintStream ps = new PrintStream(fos) + ) { + for (String str : contents) { + ps.println(str); + } + } + } + + /* version.jar entries and files: + * META-INF/ + * META-INF/MANIFEST.MF + * Bar.class + * Main.class + * META-INF/versions/9/ + * META-INF/versions/9/Bar.class + * META-INF/versions/9/Foo.class + * META-INF/versions/24/ + * META-INF/versions/24/Foo.class + */ + static void createClassFilesAndJar() throws Exception { + String tempDir = CDSTestUtils.getOutputDir(); + File baseDir = new File(tempDir + File.separator + "base"); + File vDir = new File(tempDir + File.separator + BASE_VERSION_STRING); + File vDir2 = new File(tempDir + File.separator + MAJOR_VERSION_STRING); + + baseDir.mkdirs(); + vDir.mkdirs(); + + File fileFoo = TestCommon.getOutputSourceFile("Foo.java"); + writeFile(fileFoo, getFoo()); + JarBuilder.compile(vDir.getAbsolutePath(), fileFoo.getAbsolutePath(), "--release", BASE_VERSION_STRING); + + writeFile(fileFoo, getFooNewVersion()); + JarBuilder.compile(vDir2.getAbsolutePath(), fileFoo.getAbsolutePath(), "--release", MAJOR_VERSION_STRING); + + File fileMain = TestCommon.getOutputSourceFile("Main.java"); + writeFile(fileMain, getMain()); + JarBuilder.compile(baseDir.getAbsolutePath(), fileMain.getAbsolutePath()); + File fileBar = TestCommon.getOutputSourceFile("Bar.java"); + writeFile(fileBar, getBar()); + JarBuilder.compile(baseDir.getAbsolutePath(), fileBar.getAbsolutePath()); + JarBuilder.compile(vDir.getAbsolutePath(), fileBar.getAbsolutePath(), "--release", BASE_VERSION_STRING); + + String[] meta = { + "Multi-Release: true", + "Main-Class: Main" + }; + File metainf = new File(tempDir, "mf.txt"); + writeFile(metainf, meta); + + JarBuilder.build("multi-version", baseDir, metainf.getAbsolutePath(), + "--release", BASE_VERSION_STRING, "-C", vDir.getAbsolutePath(), ".", + "--release", MAJOR_VERSION_STRING, "-C", vDir2.getAbsolutePath(), "."); + + } + + public static void main(String... args) throws Exception { + // create multi-version.jar which contains Main.class, Foo.class and Bar.class. + // Foo.class has two version: base version 9 and current major JDK version. + // Bar.class has two versions: base version 9 and default version. + // Since there is no default version for Foo, the class loader will get the + // highest version (current major JDK version in this case) which is the + // same or below the current JDK version. + createClassFilesAndJar(); + + String mainClass = "Main"; + String appJar = TestCommon.getTestJar("multi-version.jar"); + String appClasses[] = {"Foo", "Bar"}; + + OutputAnalyzer output = TestCommon.dump(appJar, appClasses); + output.shouldContain("Loading classes to share: done.") + .shouldHaveExitValue(0); + + String agentCmdArg = "-agentlib:SimpleClassFileLoadHook=Foo,Hello,HELLO"; + output = TestCommon.execAuto("-cp", appJar, + "-Xlog:cds=info,class+load", + agentCmdArg, + mainClass); + + output.shouldMatch(".*Foo.source:.*multi-version.jar") + // New version of Foo is loaded from jar since it was modified by CFLH + .shouldContain("HELLO from Foo new version") // CFLH changed "Hello" to "HELLO" + .shouldContain("class Foo") // output from Main + // Bar is loaded from archive + .shouldContain("Bar source: shared objects file") + .shouldContain("Hello from Bar") + .shouldContain("class Bar"); // output from Main + } +}