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