MemoryLeakUtils.java

/*
 * Copyright (c) 2015, 2022 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.test.memleak.common;

import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.management.MBeanServer;

/**
 * Utility class for memory leak test infrastructure.
 *
 * @author Stepan Vavra
 */
public class MemoryLeakUtils {

    /**
     * The log file where the output (stdout/stderr) of container is located.
     * <p/>
     * For instance, this file is used for detection of {@link OutOfMemoryError} exception records.
     */
    public static final String JERSEY_CONFIG_TEST_CONTAINER_LOGFILE = "jersey.config.test.container.logfile";

    /**
     * The memory leak timeout denotes successful end of the memory leak test. That is, if the memory leak didn't occur during the
     * specified timeout, the test successfully finishes.
     */
    public static final String JERSEY_CONFIG_TEST_MEMLEAK_TIMEOUT = "jersey.config.test.memleak.timeout";

    /**
     * The context root where the deployed application will be accessible.
     */
    public static final String JERSEY_CONFIG_TEST_CONTAINER_CONTEXT_ROOT = "jersey.config.test.container.contextRoot";

    /**
     * The path where to create heap dump files.
     */
    public static final String JERSEY_CONFIG_TEST_MEMLEAK_HEAP_DUMP_PATH = "jersey.config.test.memleak.heapDumpPath";

    private MemoryLeakUtils() {
    }

    private static final Pattern PATTERN = Pattern.compile(".*java\\.lang\\.OutOfMemoryError.*");

    /**
     * Scans the file denoted by {@link #JERSEY_CONFIG_TEST_CONTAINER_LOGFILE} for {@link OutOfMemoryError} records.
     *
     * @throws IOException           In case of I/O error.
     * @throws IllegalStateException In case the {@link OutOfMemoryError} record was found.
     */
    public static void verifyNoOutOfMemoryOccurred() throws IOException {

        final String logFileName = System.getProperty(JERSEY_CONFIG_TEST_CONTAINER_LOGFILE);
        System.out.println("Verifying whether OutOfMemoryError occurred in log file: " + logFileName);

        if (logFileName == null) {
            return;
        }
        final File logFile = new File(logFileName);
        if (!logFile.exists()) {
            return;
        }

        final List<String> lines = Files.lines(logFile.toPath(), Charset.defaultCharset())
                .filter(line -> PATTERN.matcher(line).matches()).collect(Collectors.toList());

        if (lines.size() > 0) {
            throw new IllegalStateException(
                    "OutOfMemoryError detected in '" + logFileName + "': " + Arrays.toString(lines.toArray()));
        }
    }

    /**
     * The name of the HotSpot Diagnostic MXBean
     */
    private static final String HOTSPOT_BEAN_NAME = "com.sun.management:type=HotSpotDiagnostic";

    /**
     * The class name of HotSpot Diagnostic MXBean
     */
    private static final String HOT_SPOT_DIAGNOSTIC_MXBEAN_CLASSNAME = "com.sun.management.HotSpotDiagnosticMXBean";

    /**
     * Hotspot diagnostic MBean singleton
     */
    private static volatile Object hotSpotDiagnosticMBean;

    private static volatile Method dumpHeapMethod;

    /**
     * Create a heap dump into a given file.
     *
     * @param fileName name of the heap dump file
     * @param live     whether to dump only the live objects
     */
    static void dumpHeap(String fileName, boolean live)
            throws InvocationTargetException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, IOException {
        conditionallyInitHotSpotDiagnosticMXBean();
        try {
            java.nio.file.Files.deleteIfExists(Paths.get(fileName));
        } catch (IOException e) {
            // do nothing and try to go further
        }
        dumpHeapMethod.invoke(hotSpotDiagnosticMBean, fileName, live);
    }

    /**
     * Initialize the HotSpot diagnostic MBean
     */
    private static void conditionallyInitHotSpotDiagnosticMXBean()
            throws IOException, ClassNotFoundException, NoSuchMethodException {
        if (hotSpotDiagnosticMBean == null) {
            synchronized (MemoryLeakUtils.class) {
                if (hotSpotDiagnosticMBean == null) {
                    Class clazz = Class.forName(HOT_SPOT_DIAGNOSTIC_MXBEAN_CLASSNAME);
                    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
                    hotSpotDiagnosticMBean = ManagementFactory.newPlatformMXBeanProxy(server, HOTSPOT_BEAN_NAME, clazz);
                    dumpHeapMethod = clazz.getMethod("dumpHeap", String.class, boolean.class);
                }
            }
        }
    }
}