1 /*
2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
3  * Copyright (c) 2017 SAP SE. All rights reserved.
4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5  *
6  * This code is free software; you can redistribute it and/or modify it
7  * under the terms of the GNU General Public License version 2 only, as
8  * published by the Free Software Foundation.
9  *
10  * This code is distributed in the hope that it will be useful, but WITHOUT
11  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
13  * version 2 for more details (a copy is included in the LICENSE file that
14  * accompanied this code).
15  *
16  * You should have received a copy of the GNU General Public License version
17  * 2 along with this work; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
19  *
20  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
21  * or visit www.oracle.com if you need additional information or have any
22  * questions.
23  */
24 
25 /**
26  * @test
27  * @bug 8173743
28  * @requires vm.compMode != "Xcomp"
29  * @summary Failures during class definition can lead to memory leaks in metaspace
30  * @requires vm.opt.final.ClassUnloading
31  * @library /test/lib
32  * @run main/othervm test.DefineClass defineClass
33  * @run main/othervm test.DefineClass defineSystemClass
34  * @run main/othervm -XX:+AllowParallelDefineClass
35                      test.DefineClass defineClassParallel
36  * @run main/othervm -XX:-AllowParallelDefineClass
37                      test.DefineClass defineClassParallel
38  * @run main/othervm -Djdk.attach.allowAttachSelf test.DefineClass redefineClass
39  * @run main/othervm -Djdk.attach.allowAttachSelf test.DefineClass redefineClassWithError
40  * @author volker.simonis@gmail.com
41  */
42 
43 package test;
44 
45 import java.io.ByteArrayOutputStream;
46 import java.io.File;
47 import java.io.FileOutputStream;
48 import java.io.InputStream;
49 import java.lang.instrument.ClassDefinition;
50 import java.lang.instrument.Instrumentation;
51 import java.lang.management.ManagementFactory;
52 import java.util.Scanner;
53 import java.util.concurrent.CountDownLatch;
54 import java.util.jar.Attributes;
55 import java.util.jar.JarEntry;
56 import java.util.jar.JarOutputStream;
57 import java.util.jar.Manifest;
58 
59 import javax.management.MBeanServer;
60 import javax.management.ObjectName;
61 
62 import com.sun.tools.attach.VirtualMachine;
63 
64 import jdk.test.lib.process.ProcessTools;
65 
66 public class DefineClass {
67 
68     private static Instrumentation instrumentation;
69 
getID(CountDownLatch start, CountDownLatch stop)70     public void getID(CountDownLatch start, CountDownLatch stop) {
71         String id = "AAAAAAAA";
72         System.out.println(id);
73         try {
74             // Signal that we've entered the activation..
75             start.countDown();
76             //..and wait until we can leave it.
77             stop.await();
78         } catch (InterruptedException e) {
79             e.printStackTrace();
80         }
81         System.out.println(id);
82         return;
83     }
84 
85     private static class MyThread extends Thread {
86         private DefineClass dc;
87         private CountDownLatch start, stop;
88 
MyThread(DefineClass dc, CountDownLatch start, CountDownLatch stop)89         public MyThread(DefineClass dc, CountDownLatch start, CountDownLatch stop) {
90             this.dc = dc;
91             this.start = start;
92             this.stop = stop;
93         }
94 
run()95         public void run() {
96             dc.getID(start, stop);
97         }
98     }
99 
100     private static class ParallelLoadingThread extends Thread {
101         private MyParallelClassLoader pcl;
102         private CountDownLatch stop;
103         private byte[] buf;
104 
ParallelLoadingThread(MyParallelClassLoader pcl, byte[] buf, CountDownLatch stop)105         public ParallelLoadingThread(MyParallelClassLoader pcl, byte[] buf, CountDownLatch stop) {
106             this.pcl = pcl;
107             this.stop = stop;
108             this.buf = buf;
109         }
110 
run()111         public void run() {
112             try {
113                 stop.await();
114             } catch (InterruptedException e) {
115                 e.printStackTrace();
116             }
117             try {
118                 @SuppressWarnings("unchecked")
119                 Class<DefineClass> dc = (Class<DefineClass>) pcl.myDefineClass(DefineClass.class.getName(), buf, 0, buf.length);
120             }
121             catch (LinkageError jle) {
122                 // Expected with a parallel capable class loader and
123                 // -XX:+AllowParallelDefineClass
124                 pcl.incrementLinkageErrors();
125             }
126 
127         }
128     }
129 
130     static private class MyClassLoader extends ClassLoader {
myDefineClass(String name, byte[] b, int off, int len)131         public Class<?> myDefineClass(String name, byte[] b, int off, int len) throws ClassFormatError {
132             return defineClass(name, b, off, len, null);
133         }
134     }
135 
136     static private class MyParallelClassLoader extends ClassLoader {
137         static {
138             System.out.println("parallelCapable : " + registerAsParallelCapable());
139         }
myDefineClass(String name, byte[] b, int off, int len)140         public Class<?> myDefineClass(String name, byte[] b, int off, int len) throws ClassFormatError {
141             return defineClass(name, b, off, len, null);
142         }
incrementLinkageErrors()143         public synchronized void incrementLinkageErrors() {
144             linkageErrors++;
145         }
getLinkageErrors()146         public int getLinkageErrors() {
147             return linkageErrors;
148         }
149         private volatile int linkageErrors;
150     }
151 
agentmain(String args, Instrumentation inst)152     public static void agentmain(String args, Instrumentation inst) {
153         System.out.println("Loading Java Agent.");
154         instrumentation = inst;
155     }
156 
157 
loadInstrumentationAgent(String myName, byte[] buf)158     private static void loadInstrumentationAgent(String myName, byte[] buf) throws Exception {
159         // Create agent jar file on the fly
160         Manifest m = new Manifest();
161         m.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
162         m.getMainAttributes().put(new Attributes.Name("Agent-Class"), myName);
163         m.getMainAttributes().put(new Attributes.Name("Can-Redefine-Classes"), "true");
164         File jarFile = File.createTempFile("agent", ".jar");
165         jarFile.deleteOnExit();
166         JarOutputStream jar = new JarOutputStream(new FileOutputStream(jarFile), m);
167         jar.putNextEntry(new JarEntry(myName.replace('.', '/') + ".class"));
168         jar.write(buf);
169         jar.close();
170         String pid = Long.toString(ProcessTools.getProcessId());
171         System.out.println("Our pid is = " + pid);
172         VirtualMachine vm = VirtualMachine.attach(pid);
173         vm.loadAgent(jarFile.getAbsolutePath());
174     }
175 
getBytecodes(String myName)176     private static byte[] getBytecodes(String myName) throws Exception {
177         InputStream is = DefineClass.class.getResourceAsStream(myName + ".class");
178         ByteArrayOutputStream baos = new ByteArrayOutputStream();
179         byte[] buf = new byte[4096];
180         int len;
181         while ((len = is.read(buf)) != -1) baos.write(buf, 0, len);
182         buf = baos.toByteArray();
183         System.out.println("sizeof(" + myName + ".class) == " + buf.length);
184         return buf;
185     }
186 
getStringIndex(String needle, byte[] buf)187     private static int getStringIndex(String needle, byte[] buf) {
188         return getStringIndex(needle, buf, 0);
189     }
190 
getStringIndex(String needle, byte[] buf, int offset)191     private static int getStringIndex(String needle, byte[] buf, int offset) {
192         outer:
193         for (int i = offset; i < buf.length - offset - needle.length(); i++) {
194             for (int j = 0; j < needle.length(); j++) {
195                 if (buf[i + j] != (byte)needle.charAt(j)) continue outer;
196             }
197             return i;
198         }
199         return 0;
200     }
201 
replaceString(byte[] buf, String name, int index)202     private static void replaceString(byte[] buf, String name, int index) {
203         for (int i = index; i < index + name.length(); i++) {
204             buf[i] = (byte)name.charAt(i - index);
205         }
206     }
207 
208     private static MBeanServer mbserver = ManagementFactory.getPlatformMBeanServer();
209 
getClassStats(String pattern)210     private static int getClassStats(String pattern) {
211         try {
212             ObjectName diagCmd = new ObjectName("com.sun.management:type=DiagnosticCommand");
213 
214             String result = (String)mbserver.invoke(diagCmd , "gcClassStats" , new Object[] { null }, new String[] {String[].class.getName()});
215             int count = 0;
216             try (Scanner s = new Scanner(result)) {
217                 if (s.hasNextLine()) {
218                     System.out.println(s.nextLine());
219                 }
220                 while (s.hasNextLine()) {
221                     String l = s.nextLine();
222                     if (l.endsWith(pattern)) {
223                         count++;
224                         System.out.println(l);
225                     }
226                 }
227             }
228             return count;
229         }
230         catch (Exception e) {
231             throw new RuntimeException("Test failed because we can't read the class statistics!", e);
232         }
233     }
234 
printClassStats(int expectedCount, boolean reportError)235     private static void printClassStats(int expectedCount, boolean reportError) {
236         int count = getClassStats("DefineClass");
237         String res = "Should have " + expectedCount +
238                      " DefineClass instances and we have: " + count;
239         System.out.println(res);
240         if (reportError && count != expectedCount) {
241             throw new RuntimeException(res);
242         }
243     }
244 
245     public static final int ITERATIONS = 10;
246 
main(String[] args)247     public static void main(String[] args) throws Exception {
248         String myName = DefineClass.class.getName();
249         byte[] buf = getBytecodes(myName.substring(myName.lastIndexOf(".") + 1));
250         int iterations = (args.length > 1 ? Integer.parseInt(args[1]) : ITERATIONS);
251 
252         if (args.length == 0 || "defineClass".equals(args[0])) {
253             MyClassLoader cl = new MyClassLoader();
254             for (int i = 0; i < iterations; i++) {
255                 try {
256                     @SuppressWarnings("unchecked")
257                     Class<DefineClass> dc = (Class<DefineClass>) cl.myDefineClass(myName, buf, 0, buf.length);
258                     System.out.println(dc);
259                 }
260                 catch (LinkageError jle) {
261                     // Can only define once!
262                     if (i == 0) throw new Exception("Should succeed the first time.");
263                 }
264             }
265             // We expect to have two instances of DefineClass here: the initial version in which we are
266             // executing and another version which was loaded into our own classloader 'MyClassLoader'.
267             // All the subsequent attempts to reload DefineClass into our 'MyClassLoader' should have failed.
268             printClassStats(2, false);
269             System.gc();
270             System.out.println("System.gc()");
271             // At least after System.gc() the failed loading attempts should leave no instances around!
272             printClassStats(2, true);
273         }
274         else if ("defineSystemClass".equals(args[0])) {
275             MyClassLoader cl = new MyClassLoader();
276             int index = getStringIndex("test/DefineClass", buf);
277             replaceString(buf, "java/DefineClass", index);
278             while ((index = getStringIndex("Ltest/DefineClass;", buf, index + 1)) != 0) {
279                 replaceString(buf, "Ljava/DefineClass;", index);
280             }
281             index = getStringIndex("test.DefineClass", buf);
282             replaceString(buf, "java.DefineClass", index);
283 
284             for (int i = 0; i < iterations; i++) {
285                 try {
286                     @SuppressWarnings("unchecked")
287                     Class<DefineClass> dc = (Class<DefineClass>) cl.myDefineClass(null, buf, 0, buf.length);
288                     throw new RuntimeException("Defining a class in the 'java' package should fail!");
289                 }
290                 catch (java.lang.SecurityException jlse) {
291                     // Expected, because we're not allowed to define a class in the 'java' package
292                 }
293             }
294             // We expect to stay with one (the initial) instances of DefineClass.
295             // All the subsequent attempts to reload DefineClass into the 'java' package should have failed.
296             printClassStats(1, false);
297             System.gc();
298             System.out.println("System.gc()");
299             // At least after System.gc() the failed loading attempts should leave no instances around!
300             printClassStats(1, true);
301         }
302         else if ("defineClassParallel".equals(args[0])) {
303             MyParallelClassLoader pcl = new MyParallelClassLoader();
304             CountDownLatch stop = new CountDownLatch(1);
305 
306             Thread[] threads = new Thread[iterations];
307             for (int i = 0; i < iterations; i++) {
308                 (threads[i] = new ParallelLoadingThread(pcl, buf, stop)).start();
309             }
310             stop.countDown(); // start parallel class loading..
311             // ..and wait until all threads loaded the class
312             for (int i = 0; i < iterations; i++) {
313                 threads[i].join();
314             }
315             System.out.print("Counted " + pcl.getLinkageErrors() + " LinkageErrors ");
316             System.out.println(pcl.getLinkageErrors() == 0 ?
317                     "" : "(use -XX:+AllowParallelDefineClass to avoid this)");
318             System.gc();
319             System.out.println("System.gc()");
320             // After System.gc() we expect to remain with two instances: one is the initial version which is
321             // kept alive by this main method and another one in the parallel class loader.
322             printClassStats(2, true);
323         }
324         else if ("redefineClass".equals(args[0])) {
325             loadInstrumentationAgent(myName, buf);
326             int index = getStringIndex("AAAAAAAA", buf);
327             CountDownLatch stop = new CountDownLatch(1);
328 
329             Thread[] threads = new Thread[iterations];
330             for (int i = 0; i < iterations; i++) {
331                 buf[index] = (byte) ('A' + i + 1); // Change string constant in getID() which is legal in redefinition
332                 instrumentation.redefineClasses(new ClassDefinition(DefineClass.class, buf));
333                 DefineClass dc = DefineClass.class.newInstance();
334                 CountDownLatch start = new CountDownLatch(1);
335                 (threads[i] = new MyThread(dc, start, stop)).start();
336                 start.await(); // Wait until the new thread entered the getID() method
337             }
338             // We expect to have one instance for each redefinition because they are all kept alive by an activation
339             // plus the initial version which is kept active by this main method.
340             printClassStats(iterations + 1, false);
341             stop.countDown(); // Let all threads leave the DefineClass.getID() activation..
342             // ..and wait until really all of them returned from DefineClass.getID()
343             for (int i = 0; i < iterations; i++) {
344                 threads[i].join();
345             }
346             System.gc();
347             System.out.println("System.gc()");
348             // After System.gc() we expect to remain with two instances: one is the initial version which is
349             // kept alive by this main method and another one which is the latest redefined version.
350             printClassStats(2, true);
351         }
352         else if ("redefineClassWithError".equals(args[0])) {
353             loadInstrumentationAgent(myName, buf);
354             int index = getStringIndex("getID", buf);
355 
356             for (int i = 0; i < iterations; i++) {
357                 buf[index] = (byte) 'X'; // Change getID() to XetID() which is illegal in redefinition
358                 try {
359                     instrumentation.redefineClasses(new ClassDefinition(DefineClass.class, buf));
360                     throw new RuntimeException("Class redefinition isn't allowed to change method names!");
361                 }
362                 catch (UnsupportedOperationException uoe) {
363                     // Expected because redefinition can't change the name of methods
364                 }
365             }
366             // We expect just a single DefineClass instance because failed redefinitions should
367             // leave no garbage around.
368             printClassStats(1, false);
369             System.gc();
370             System.out.println("System.gc()");
371             // At least after a System.gc() we should definitely stay with a single instance!
372             printClassStats(1, true);
373         }
374     }
375 }
376