1 /*
2 * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package jdk.internal.misc;
27
28 import java.io.BufferedReader;
29 import java.io.File;
30 import java.io.InputStreamReader;
31 import java.io.InputStream;
32 import java.io.IOException;
33 import java.io.PrintStream;
34 import java.nio.file.Files;
35 import java.nio.file.Path;
36 import java.util.Arrays;
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Objects;
42 import java.util.jar.JarFile;
43 import java.util.stream.Stream;
44
45 import jdk.internal.access.SharedSecrets;
46 import jdk.internal.util.StaticProperty;
47
48 public class CDS {
49 // Must be in sync with cdsConfig.hpp
50 private static final int IS_DUMPING_AOT_LINKED_CLASSES = 1 << 0;
51 private static final int IS_DUMPING_ARCHIVE = 1 << 1;
52 private static final int IS_DUMPING_METHOD_HANDLES = 1 << 2;
53 private static final int IS_DUMPING_STATIC_ARCHIVE = 1 << 3;
54 private static final int IS_LOGGING_LAMBDA_FORM_INVOKERS = 1 << 4;
55 private static final int IS_USING_ARCHIVE = 1 << 5;
56 private static final int IS_DUMPING_HEAP = 1 << 6;
57 private static final int IS_LOGGING_DYNAMIC_PROXIES = 1 << 7;
58
59 private static final int configStatus = getCDSConfigStatus();
60
61 /**
62 * Should we log the use of lambda form invokers?
63 */
64 public static boolean isLoggingLambdaFormInvokers() {
65 return (configStatus & IS_LOGGING_LAMBDA_FORM_INVOKERS) != 0;
66 }
67
68 /**
69 * Is the VM writing to a (static or dynamic) CDS archive.
70 */
71 public static boolean isDumpingArchive() {
72 return (configStatus & IS_DUMPING_ARCHIVE) != 0;
73 }
74
75 /**
76 * Is the VM using at least one CDS archive?
77 */
78 public static boolean isUsingArchive() {
79 return (configStatus & IS_USING_ARCHIVE) != 0;
80 }
81
82 /**
83 * Is dumping static archive.
84 */
85 public static boolean isDumpingStaticArchive() {
86 return (configStatus & IS_DUMPING_STATIC_ARCHIVE) != 0;
87 }
88
89 public static boolean isDumpingHeap() {
90 return (configStatus & IS_DUMPING_HEAP) != 0;
91 }
92
93 public static boolean isLoggingDynamicProxies() {
94 return (configStatus & IS_LOGGING_DYNAMIC_PROXIES) != 0;
95 }
96
97 public static boolean isDumpingAOTLinkedClasses() {
98 return (configStatus & IS_DUMPING_AOT_LINKED_CLASSES) != 0;
99 }
100
101 public static boolean isSingleThreadVM() {
102 return isDumpingStaticArchive();
103 }
104
105 private static native int getCDSConfigStatus();
106 private static native void logLambdaFormInvoker(String line);
107
108
109 // Used only when dumping static archive to keep weak references alive to
110 // ensure that Soft/Weak Reference objects can be reliably archived.
111 private static ArrayList<Object> keepAliveList;
112
113 public static void keepAlive(Object s) {
114 assert isSingleThreadVM(); // no need for synchronization
115 assert isDumpingStaticArchive();
116 if (keepAliveList == null) {
117 keepAliveList = new ArrayList<>();
118 }
119 keepAliveList.add(s);
120 }
121
122 // This is called by native JVM code at the very end of Java execution before
123 // dumping the static archive.
124 // It collects the objects from keepAliveList so that they can be easily processed
125 // by the native JVM code to check that any Reference objects that need special
126 // clean up must have been registed with keepAlive()
127 private static Object[] getKeepAliveObjects() {
128 return keepAliveList.toArray();
129 }
130
131 /**
132 * Initialize archived static fields in the given Class using archived
133 * values from CDS dump time. Also initialize the classes of objects in
134 * the archived graph referenced by those fields.
135 *
136 * Those static fields remain as uninitialized if there is no mapped CDS
137 * java heap data or there is any error during initialization of the
138 * object class in the archived graph.
139 */
140 public static native void initializeFromArchive(Class<?> c);
141
142 /**
143 * Ensure that the native representation of all archived java.lang.Module objects
144 * are properly restored.
145 */
146 public static native void defineArchivedModules(ClassLoader platformLoader, ClassLoader systemLoader);
147
148 /**
149 * Returns a predictable "random" seed derived from the VM's build ID and version,
150 * to be used by java.util.ImmutableCollections to ensure that archived
151 * ImmutableCollections are always sorted the same order for the same VM build.
152 */
153 public static native long getRandomSeedForDumping();
154
155 /**
156 * log lambda form invoker holder, name and method type
157 */
158 public static void logLambdaFormInvoker(String prefix, String holder, String name, String type) {
159 if (isLoggingLambdaFormInvokers()) {
160 logLambdaFormInvoker(prefix + " " + holder + " " + name + " " + type);
161 }
162 }
163
164 /**
165 * log species
166 */
167 public static void logSpeciesType(String prefix, String cn) {
168 if (isLoggingLambdaFormInvokers()) {
169 logLambdaFormInvoker(prefix + " " + cn);
170 }
171 }
172
173 public static void logDynamicProxy(ClassLoader loader, String proxyName,
174 Class<?>[] interfaces, int accessFlags) {
175 Objects.requireNonNull(proxyName);
176 Objects.requireNonNull(interfaces);
177 logDynamicProxy0(loader, proxyName, interfaces, accessFlags);
178 }
179 private static native void logDynamicProxy0(ClassLoader loader, String proxyName,
180 Class<?>[] interfaces, int accessFlags);
181
182 static final String DIRECT_HOLDER_CLASS_NAME = "java.lang.invoke.DirectMethodHandle$Holder";
183 static final String DELEGATING_HOLDER_CLASS_NAME = "java.lang.invoke.DelegatingMethodHandle$Holder";
184 static final String BASIC_FORMS_HOLDER_CLASS_NAME = "java.lang.invoke.LambdaForm$Holder";
185 static final String INVOKERS_HOLDER_CLASS_NAME = "java.lang.invoke.Invokers$Holder";
186
187 private static boolean isValidHolderName(String name) {
188 return name.equals(DIRECT_HOLDER_CLASS_NAME) ||
189 name.equals(DELEGATING_HOLDER_CLASS_NAME) ||
190 name.equals(BASIC_FORMS_HOLDER_CLASS_NAME) ||
191 name.equals(INVOKERS_HOLDER_CLASS_NAME);
192 }
193
194 private static boolean isBasicTypeChar(char c) {
195 return "LIJFDV".indexOf(c) >= 0;
196 }
197
198 private static boolean isValidMethodType(String type) {
199 String[] typeParts = type.split("_");
200 // check return type (second part)
201 if (typeParts.length != 2 || typeParts[1].length() != 1
202 || !isBasicTypeChar(typeParts[1].charAt(0))) {
203 return false;
204 }
205 // first part
206 if (!isBasicTypeChar(typeParts[0].charAt(0))) {
207 return false;
208 }
209 for (int i = 1; i < typeParts[0].length(); i++) {
210 char c = typeParts[0].charAt(i);
211 if (!isBasicTypeChar(c)) {
212 if (!(c >= '0' && c <= '9')) {
213 return false;
214 }
215 }
216 }
217 return true;
218 }
219
220 // Throw exception on invalid input
221 private static void validateInputLines(String[] lines) {
222 for (String s: lines) {
223 if (!s.startsWith("[LF_RESOLVE]") && !s.startsWith("[SPECIES_RESOLVE]")) {
224 throw new IllegalArgumentException("Wrong prefix: " + s);
225 }
226
227 String[] parts = s.split(" ");
228 boolean isLF = s.startsWith("[LF_RESOLVE]");
229
230 if (isLF) {
231 if (parts.length != 4) {
232 throw new IllegalArgumentException("Incorrect number of items in the line: " + parts.length);
233 }
234 if (!isValidHolderName(parts[1])) {
235 throw new IllegalArgumentException("Invalid holder class name: " + parts[1]);
236 }
237 if (!isValidMethodType(parts[3])) {
238 throw new IllegalArgumentException("Invalid method type: " + parts[3]);
239 }
240 } else {
241 if (parts.length != 2) {
242 throw new IllegalArgumentException("Incorrect number of items in the line: " + parts.length);
243 }
244 }
245 }
246 }
247
248 /**
249 * called from vm to generate MethodHandle holder classes
250 * @return {@code Object[]} if holder classes can be generated.
251 * @param lines in format of LF_RESOLVE or SPECIES_RESOLVE output
252 */
253 private static Object[] generateLambdaFormHolderClasses(String[] lines) {
254 Objects.requireNonNull(lines);
255 validateInputLines(lines);
256 Stream<String> lineStream = Arrays.stream(lines);
257 Map<String, byte[]> result = SharedSecrets.getJavaLangInvokeAccess().generateHolderClasses(lineStream);
258 int size = result.size();
259 Object[] retArray = new Object[size * 2];
260 int index = 0;
261 for (Map.Entry<String, byte[]> entry : result.entrySet()) {
262 retArray[index++] = entry.getKey();
263 retArray[index++] = entry.getValue();
264 };
265 return retArray;
266 }
267
268 private static native void dumpClassList(String listFileName);
269 private static native void dumpDynamicArchive(String archiveFileName);
270
271 private static String drainOutput(InputStream stream, long pid, String tail, List<String> cmds) {
272 String fileName = "java_pid" + pid + "_" + tail;
273 new Thread( ()-> {
274 try (InputStreamReader isr = new InputStreamReader(stream);
275 BufferedReader rdr = new BufferedReader(isr);
276 PrintStream prt = new PrintStream(fileName)) {
277 prt.println("Command:");
278 for (String s : cmds) {
279 prt.print(s + " ");
280 }
281 prt.println("");
282 String line;
283 while((line = rdr.readLine()) != null) {
284 prt.println(line);
285 }
286 } catch (IOException e) {
287 throw new RuntimeException("IOException happens during drain stream to file " +
288 fileName + ": " + e.getMessage());
289 }}).start();
290 return fileName;
291 }
292
293 private static String[] excludeFlags = {
294 "-XX:DumpLoadedClassList=",
295 "-XX:+RecordDynamicDumpInfo",
296 "-Xshare:",
297 "-XX:SharedClassListFile=",
298 "-XX:SharedArchiveFile=",
299 "-XX:ArchiveClassesAtExit="};
300 private static boolean containsExcludedFlags(String testStr) {
301 for (String e : excludeFlags) {
302 if (testStr.contains(e)) {
303 return true;
304 }
305 }
306 return false;
307 }
308
309 /**
310 * called from jcmd VM.cds to dump static or dynamic shared archive
311 * @param isStatic true for dump static archive or false for dynnamic archive.
312 * @param fileName user input archive name, can be null.
313 * @return The archive name if successfully dumped.
314 */
315 private static String dumpSharedArchive(boolean isStatic, String fileName) throws Exception {
316 String cwd = new File("").getAbsolutePath(); // current dir used for printing message.
317 String currentPid = String.valueOf(ProcessHandle.current().pid());
318 String archiveFileName = fileName != null ? fileName :
319 "java_pid" + currentPid + (isStatic ? "_static.jsa" : "_dynamic.jsa");
320
321 String tempArchiveFileName = archiveFileName + ".temp";
322 File tempArchiveFile = new File(tempArchiveFileName);
323 // The operation below may cause exception if the file or its dir is protected.
324 if (!tempArchiveFile.exists()) {
325 tempArchiveFile.createNewFile();
326 }
327 tempArchiveFile.delete();
328
329 if (isStatic) {
330 String listFileName = archiveFileName + ".classlist";
331 File listFile = new File(listFileName);
332 if (listFile.exists()) {
333 listFile.delete();
334 }
335 dumpClassList(listFileName);
336 String jdkHome = StaticProperty.javaHome();
337 String classPath = System.getProperty("java.class.path");
338 List<String> cmds = new ArrayList<String>();
339 cmds.add(jdkHome + File.separator + "bin" + File.separator + "java"); // java
340 cmds.add("-cp");
341 cmds.add(classPath);
342 cmds.add("-Xlog:cds");
343 cmds.add("-Xshare:dump");
344 cmds.add("-XX:SharedClassListFile=" + listFileName);
345 cmds.add("-XX:SharedArchiveFile=" + tempArchiveFileName);
346
347 // All runtime args.
348 String[] vmArgs = VM.getRuntimeArguments();
349 if (vmArgs != null) {
350 for (String arg : vmArgs) {
351 if (arg != null && !containsExcludedFlags(arg)) {
352 cmds.add(arg);
353 }
354 }
355 }
356
357 Process proc = Runtime.getRuntime().exec(cmds.toArray(new String[0]));
358
359 // Drain stdout/stderr to files in new threads.
360 String stdOutFileName = drainOutput(proc.getInputStream(), proc.pid(), "stdout", cmds);
361 String stdErrFileName = drainOutput(proc.getErrorStream(), proc.pid(), "stderr", cmds);
362
363 proc.waitFor();
364 // done, delete classlist file.
365 listFile.delete();
366
367 // Check if archive has been successfully dumped. We won't reach here if exception happens.
368 // Throw exception if file is not created.
369 if (!tempArchiveFile.exists()) {
370 throw new RuntimeException("Archive file " + tempArchiveFileName +
371 " is not created, please check stdout file " +
372 cwd + File.separator + stdOutFileName + " or stderr file " +
373 cwd + File.separator + stdErrFileName + " for more detail");
374 }
375 } else {
376 dumpDynamicArchive(tempArchiveFileName);
377 if (!tempArchiveFile.exists()) {
378 throw new RuntimeException("Archive file " + tempArchiveFileName +
379 " is not created, please check current working directory " +
380 cwd + " for process " +
381 currentPid + " output for more detail");
382 }
383 }
384 // Override the existing archive file
385 File archiveFile = new File(archiveFileName);
386 if (archiveFile.exists()) {
387 archiveFile.delete();
388 }
389 if (!tempArchiveFile.renameTo(archiveFile)) {
390 throw new RuntimeException("Cannot rename temp file " + tempArchiveFileName + " to archive file" + archiveFileName);
391 }
392 // Everything goes well, print out the file name.
393 String archiveFilePath = new File(archiveFileName).getAbsolutePath();
394 System.out.println("The process was attached by jcmd and dumped a " + (isStatic ? "static" : "dynamic") + " archive " + archiveFilePath);
395 return archiveFilePath;
396 }
397
398 /**
399 * Detects if we need to emit explicit class initialization checks in
400 * AOT-cached MethodHandles and VarHandles before accessing static fields
401 * and methods.
402 * @see jdk.internal.misc.Unsafe::shouldBeInitialized
403 *
404 * @return false only if a call to {@code ensureClassInitialized} would have
405 * no effect during the application's production run.
406 */
407 public static boolean needsClassInitBarrier(Class<?> c) {
408 if (c == null) {
409 throw new NullPointerException();
410 }
411
412 if ((configStatus & IS_DUMPING_METHOD_HANDLES) == 0) {
413 return false;
414 } else {
415 return needsClassInitBarrier0(c);
416 }
417 }
418
419 private static native boolean needsClassInitBarrier0(Class<?> c);
420
421 /**
422 * This class is used only by native JVM code at CDS dump time for loading
423 * "unregistered classes", which are archived classes that are intended to
424 * be loaded by custom class loaders during runtime.
425 * See src/hotspot/share/cds/unregisteredClasses.cpp.
426 */
427 private static class UnregisteredClassLoader extends ClassLoader {
428 static {
429 registerAsParallelCapable();
430 }
431
432 static interface Source {
433 public byte[] readClassFile(String className) throws IOException;
434 }
435
436 static class JarSource implements Source {
437 private final JarFile jar;
438
439 JarSource(File file) throws IOException {
440 jar = new JarFile(file);
441 }
442
443 @Override
444 public byte[] readClassFile(String className) throws IOException {
445 final var entryName = className.replace('.', '/').concat(".class");
446 final var entry = jar.getEntry(entryName);
447 if (entry == null) {
448 throw new IOException("No such entry: " + entryName + " in " + jar.getName());
449 }
450 try (final var in = jar.getInputStream(entry)) {
451 return in.readAllBytes();
452 }
453 }
454 }
455
456 static class DirSource implements Source {
457 private final String basePath;
458
459 DirSource(File dir) {
460 assert dir.isDirectory();
461 basePath = dir.toString();
462 }
463
464 @Override
465 public byte[] readClassFile(String className) throws IOException {
466 final var subPath = className.replace('.', File.separatorChar).concat(".class");
467 final var fullPath = Path.of(basePath, subPath);
468 return Files.readAllBytes(fullPath);
469 }
470 }
471
472 private final HashMap<String, Source> sources = new HashMap<>();
473
474 private Source resolveSource(String path) throws IOException {
475 Source source = sources.get(path);
476 if (source != null) {
477 return source;
478 }
479
480 final var file = new File(path);
481 if (!file.exists()) {
482 throw new IOException("No such file: " + path);
483 }
484 if (file.isFile()) {
485 source = new JarSource(file);
486 } else if (file.isDirectory()) {
487 source = new DirSource(file);
488 } else {
489 throw new IOException("Not a normal file: " + path);
490 }
491 sources.put(path, source);
492
493 return source;
494 }
495
496 /**
497 * Load the class of the given <code>name</code> from the given <code>source</code>.
498 * <p>
499 * All super classes and interfaces of the named class must have already been loaded:
500 * either defined by this class loader (unregistered ones) or loaded, possibly indirectly,
501 * by the system class loader (registered ones).
502 * <p>
503 * If the named class has a registered super class or interface named N there should be no
504 * unregistered class or interface named N loaded yet.
505 *
506 * @param name the name of the class to be loaded.
507 * @param source path to a directory or a JAR file from which the named class should be
508 * loaded.
509 */
510 private Class<?> load(String name, String source) throws IOException {
511 final Source resolvedSource = resolveSource(source);
512 final byte[] bytes = resolvedSource.readClassFile(name);
513 // 'defineClass()' may cause loading of supertypes of this unregistered class by VM
514 // calling 'this.loadClass()'.
515 //
516 // For any supertype S named SN specified in the classlist the following is ensured by
517 // the CDS implementation:
518 // - if S is an unregistered class it must have already been defined by this class
519 // loader and thus will be found by 'this.findLoadedClass(SN)',
520 // - if S is not an unregistered class there should be no unregistered class named SN
521 // loaded yet so either S has previously been (indirectly) loaded by this class loader
522 // and thus it will be found when calling 'this.findLoadedClass(SN)' or it will be
523 // found when delegating to the system class loader, which must have already loaded S,
524 // by calling 'this.getParent().loadClass(SN, false)'.
525 // See the implementation of 'ClassLoader.loadClass()' for details.
526 //
527 // Therefore, we should resolve all supertypes to the expected ones as specified by the
528 // "super:" and "interfaces:" attributes in the classlist. This invariant is validated
529 // by the C++ function 'ClassListParser::load_class_from_source()'.
530 assert getParent() == getSystemClassLoader();
531 return defineClass(name, bytes, 0, bytes.length);
532 }
533 }
534
535 /**
536 * This class is used only by native JVM code to spawn a child JVM process to assemble
537 * the AOT cache. <code>args[]</code> are passed in the <code>JAVA_TOOL_OPTIONS</code>
538 * environment variable.
539 */
540 private static class ProcessLauncher {
541 static int execWithJavaToolOptions(String javaLauncher, String args[]) throws IOException, InterruptedException {
542 ProcessBuilder pb = new ProcessBuilder().inheritIO().command(javaLauncher);
543 StringBuilder sb = new StringBuilder();
544
545 // Encode the args as described in
546 // https://docs.oracle.com/en/java/javase/24/docs/specs/jvmti.html#tooloptions
547 String prefix = "";
548 for (String arg : args) {
549 sb.append(prefix);
550
551 for (int i = 0; i < arg.length(); i++) {
552 char c = arg.charAt(i);
553 if (c == '"' || Character.isWhitespace(c)) {
554 sb.append('\'');
555 sb.append(c);
556 sb.append('\'');
557 } else if (c == '\'') {
558 sb.append('"');
559 sb.append(c);
560 sb.append('"');
561 } else {
562 sb.append(c);
563 }
564 }
565
566 prefix = " ";
567 }
568
569 Map<String, String> env = pb.environment();
570 env.put("JAVA_TOOL_OPTIONS", sb.toString());
571 env.remove("_JAVA_OPTIONS");
572 env.remove("CLASSPATH");
573 Process process = pb.start();
574 return process.waitFor();
575 }
576 }
577 }