From c54724da143bb96da9c3534539bc789847858464 Mon Sep 17 00:00:00 2001
From: Lin Zang <lzang@openjdk.org>
Date: Thu, 25 Feb 2021 12:09:55 +0000
Subject: [PATCH] 8257234: Add gz option to SA jmap to write a gzipped heap
 dump

Reviewed-by: cjplummer, ysuenaga, sspitsyn
---
 .../sun/jvm/hotspot/CommandProcessor.java     |  96 +++++-
 .../classes/sun/jvm/hotspot/SALauncher.java   |  42 ++-
 .../classes/sun/jvm/hotspot/tools/JMap.java   |  58 +++-
 .../hotspot/utilities/HeapHprofBinWriter.java | 311 ++++++++++++++++--
 .../serviceability/sa/ClhsdbDumpheap.java     | 120 ++++++-
 test/jdk/sun/tools/jhsdb/HeapDumpTest.java    |  77 +++--
 .../lib/jdk/test/lib/hprof/parser/Reader.java |  35 +-
 7 files changed, 634 insertions(+), 105 deletions(-)

diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/CommandProcessor.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/CommandProcessor.java
index 6c486fc1b4d..6b00ce48f5c 100644
--- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/CommandProcessor.java
+++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/CommandProcessor.java
@@ -163,7 +163,6 @@ public class CommandProcessor {
 
         Tokens(String cmd) {
             input = cmd;
-
             // check for quoting
             int quote = cmd.indexOf('"');
             ArrayList<String> t = new ArrayList<>();
@@ -1787,25 +1786,60 @@ public class CommandProcessor {
                 sysProps.run();
             }
         },
-        new Command("dumpheap", "dumpheap [filename]", false) {
+        new Command("dumpheap", "dumpheap [gz=<1-9>] [filename]", false) {
             public void doit(Tokens t) {
-                if (t.countTokens() > 1) {
+                int cntTokens = t.countTokens();
+                if (cntTokens > 2) {
+                    err.println("More than 2 options specified: " + cntTokens);
                     usage();
-                } else {
-                    JMap jmap = new JMap();
-                    String filename;
-                    if (t.countTokens() == 1) {
-                        filename = t.nextToken();
+                    return;
+                }
+                JMap jmap = new JMap();
+                String filename = "heap.bin";
+                int gzlevel = 0;
+                /*
+                 * Possible command:
+                 *     dumpheap gz=1 file;
+                 *     dumpheap gz=1;
+                 *     dumpheap file;
+                 *     dumpheap
+                 *
+                 * Use default filename if cntTokens == 0.
+                 * Handle cases with cntTokens == 1 or 2.
+                 */
+                if (cntTokens == 1) { // first argument could be filename or "gz="
+                    String option = t.nextToken();
+                    if (!option.startsWith("gz=")) {
+                        filename = option;
                     } else {
-                        filename = "heap.bin";;
-                    }
-                    try {
-                        jmap.writeHeapHprofBin(filename);
-                    } catch (Exception e) {
-                        err.println("Error: " + e);
-                        if (verboseExceptions) {
-                            e.printStackTrace(err);
+                        gzlevel = parseHeapDumpCompressionLevel(option);
+                        if (gzlevel == 0) {
+                            usage();
+                            return;
                         }
+                        filename = "heap.bin.gz";
+                    }
+                }
+                if (cntTokens == 2) { // first argument is "gz=" followed by filename
+                    String option = t.nextToken();
+                    gzlevel = parseHeapDumpCompressionLevel(option);
+                    if (gzlevel == 0) {
+                        usage();
+                        return;
+                    }
+                    filename = t.nextToken();
+                    if (filename.startsWith("gz=")) {
+                        err.println("Filename should not start with \"gz=\": " + filename);
+                        usage();
+                        return;
+                    }
+                }
+                try {
+                    jmap.writeHeapHprofBin(filename, gzlevel);
+                } catch (Exception e) {
+                    err.println("Error: " + e);
+                    if (verboseExceptions) {
+                        e.printStackTrace(err);
                     }
                 }
             }
@@ -2080,4 +2114,34 @@ public class CommandProcessor {
             }
         }
     }
+
+    /* Parse compression level
+     * @return   1-9    compression level
+     *           0      compression level is illegal
+     */
+    private int parseHeapDumpCompressionLevel(String option) {
+
+        String[] keyValue = option.split("=");
+        if (!keyValue[0].equals("gz")) {
+            err.println("Expected option is \"gz=\"");
+            return 0;
+        }
+        if (keyValue.length != 2) {
+            err.println("Exactly one argument is expected for option \"gz\"");
+            return 0;
+        }
+        int gzl = 0;
+        String level = keyValue[1];
+        try {
+            gzl = Integer.parseInt(level);
+        } catch (NumberFormatException e) {
+            err.println("gz option value not an integer ("+level+")");
+            return 0;
+        }
+        if (gzl < 1 || gzl > 9) {
+            err.println("Compression level out of range (1-9): " + level);
+            return 0;
+        }
+        return gzl;
+    }
 }
diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/SALauncher.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/SALauncher.java
index 944bdbe25ef..36b99552179 100644
--- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/SALauncher.java
+++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/SALauncher.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2021, 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
@@ -126,7 +126,8 @@ public class SALauncher {
         System.out.println("    <no option>             To print same info as Solaris pmap.");
         System.out.println("    --heap                  To print java heap summary.");
         System.out.println("    --binaryheap            To dump java heap in hprof binary format.");
-        System.out.println("    --dumpfile <name>       The name of the dump file.");
+        System.out.println("    --dumpfile <name>       The name of the dump file. Only valid with --binaryheap.");
+        System.out.println("    --gz <1-9>              The compression level for gzipped dump file. Only valid with --binaryheap.");
         System.out.println("    --histo                 To print histogram of java object heap.");
         System.out.println("    --clstats               To print class loader statistics.");
         System.out.println("    --finalizerinfo         To print information on objects awaiting finalization.");
@@ -301,33 +302,40 @@ public class SALauncher {
     }
 
     private static void runJMAP(String[] oldArgs) {
-        Map<String, String> longOptsMap = Map.of("exe=", "exe",
-                                                 "core=", "core",
-                                                 "pid=", "pid",
-                                                 "connect=", "connect",
-                                                 "heap", "-heap",
-                                                 "binaryheap", "binaryheap",
-                                                 "dumpfile=", "dumpfile",
-                                                 "histo", "-histo",
-                                                 "clstats", "-clstats",
-                                                 "finalizerinfo", "-finalizerinfo");
+        Map<String, String> longOptsMap = Map.ofEntries(
+                Map.entry("exe=", "exe"),
+                Map.entry("core=", "core"),
+                Map.entry("pid=", "pid"),
+                Map.entry("connect=", "connect"),
+                Map.entry("heap", "-heap"),
+                Map.entry("binaryheap", "binaryheap"),
+                Map.entry("dumpfile=", "dumpfile"),
+                Map.entry("gz=", "gz"),
+                Map.entry("histo", "-histo"),
+                Map.entry("clstats", "-clstats"),
+                Map.entry("finalizerinfo", "-finalizerinfo"));
         Map<String, String> newArgMap = parseOptions(oldArgs, longOptsMap);
 
         boolean requestHeapdump = newArgMap.containsKey("binaryheap");
         String dumpfile = newArgMap.get("dumpfile");
+        String gzLevel = newArgMap.get("gz");
+        String command = "-heap:format=b";
         if (!requestHeapdump && (dumpfile != null)) {
             throw new IllegalArgumentException("Unexpected argument: dumpfile");
         }
         if (requestHeapdump) {
-            if (dumpfile == null) {
-                newArgMap.put("-heap:format=b", null);
-            } else {
-                newArgMap.put("-heap:format=b,file=" + dumpfile, null);
+            if (gzLevel != null) {
+                command += ",gz=" + gzLevel;
             }
+            if (dumpfile != null) {
+                command += ",file=" + dumpfile;
+            }
+            newArgMap.put(command, null);
         }
 
         newArgMap.remove("binaryheap");
         newArgMap.remove("dumpfile");
+        newArgMap.remove("gz");
         JMap.main(buildAttachArgs(newArgMap, false));
     }
 
@@ -485,6 +493,8 @@ public class SALauncher {
         } catch (SAGetoptException e) {
             System.err.println(e.getMessage());
             toolHelp(args[0]);
+            // Exit with error status
+            System.exit(1);
         }
     }
 }
diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/tools/JMap.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/tools/JMap.java
index ddb61495adc..4c80ecd6c4e 100644
--- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/tools/JMap.java
+++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/tools/JMap.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2004, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2004, 2021, 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
@@ -50,16 +50,21 @@ public class JMap extends Tool {
     }
 
     protected String getCommandFlags() {
-        return "-heap|-heap:format=b|-histo|-clstats|-finalizerinfo";
+        return "-heap|-heap:format=b[,gz=<1-9>][,file=<dumpfile>]|-heap:format=x[,file=<dumpfile>]|{-histo|-clstats|-finalizerinfo";
     }
 
     protected void printFlagsUsage() {
-        System.out.println("    <no option>\tto print same info as Solaris pmap");
-        System.out.println("    -heap\tto print java heap summary");
-        System.out.println("    -heap:format=b\tto dump java heap in hprof binary format");
-        System.out.println("    -histo\tto print histogram of java object heap");
-        System.out.println("    -clstats\tto print class loader statistics");
-        System.out.println("    -finalizerinfo\tto print information on objects awaiting finalization");
+        System.out.println("    <no option>\tTo print same info as Solaris pmap.");
+        System.out.println("    -heap\tTo print java heap summary.");
+        System.out.println("    -heap:format=b[,gz=<1-9>][,file=<dumpfile>]  \tTo dump java heap in hprof binary format.");
+        System.out.println("                                                 \tIf gz specified, the heap dump is written in gzipped format");
+        System.out.println("                                                 \tusing the given compression level.");
+        System.err.println("                                                 \t1 (recommended) is the fastest, 9 the strongest compression.");
+        System.out.println("    -heap:format=x[,file=<dumpfile>]             \tTo dump java heap in GXL format.");
+        System.out.println("                                                 \tPlease be aware that \"gz\" option is not valid for heap dump in GXL format.");
+        System.out.println("    -histo\tTo print histogram of java object heap.");
+        System.out.println("    -clstats\tTo print class loader statistics.");
+        System.out.println("    -finalizerinfo\tTo print information on objects awaiting finalization.");
         super.printFlagsUsage();
     }
 
@@ -72,6 +77,7 @@ public class JMap extends Tool {
     public static final int MODE_FINALIZERINFO = 6;
 
     private static String dumpfile = "heap.bin";
+    private static int gzLevel = 0;
 
     public void run() {
         Tool tool = null;
@@ -94,7 +100,7 @@ public class JMap extends Tool {
             break;
 
         case MODE_HEAP_GRAPH_HPROF_BIN:
-            writeHeapHprofBin(dumpfile);
+            writeHeapHprofBin(dumpfile, gzLevel);
             return;
 
         case MODE_HEAP_GRAPH_GXL:
@@ -151,6 +157,26 @@ public class JMap extends Tool {
                                 System.exit(1);
                             }
                             dumpfile = keyValue[1];
+                        } else if (keyValue[0].equals("gz")) {
+                            if (mode == MODE_HEAP_GRAPH_GXL) {
+                                System.err.println("\"gz\" option is not compatible with heap dump in GXL format");
+                                System.exit(1);
+                            }
+                            if (keyValue.length == 1) {
+                                System.err.println("Argument is expected for \"gz\"");
+                                System.exit(1);
+                            }
+                            String level = keyValue[1];
+                            try {
+                                gzLevel = Integer.parseInt(level);
+                            } catch (NumberFormatException e) {
+                                System.err.println("\"gz\" option value not an integer ("+level+")");
+                                System.exit(1);
+                            }
+                            if (gzLevel < 1 || gzLevel > 9) {
+                                System.err.println("compression level out of range (1-9): " + level);
+                                System.exit(1);
+                            }
                         } else {
                             System.err.println("unknown option:" + keyValue[0]);
 
@@ -176,9 +202,17 @@ public class JMap extends Tool {
         jmap.execute(args);
     }
 
-    public boolean writeHeapHprofBin(String fileName) {
+    public boolean writeHeapHprofBin(String fileName, int gzLevel) {
         try {
-            HeapGraphWriter hgw = new HeapHprofBinWriter();
+            HeapGraphWriter hgw;
+            if (gzLevel == 0) {
+                hgw = new HeapHprofBinWriter();
+            } else if (gzLevel >=1 && gzLevel <= 9) {
+                hgw = new HeapHprofBinWriter(gzLevel);
+            } else {
+                System.err.println("Illegal compression level: " + gzLevel);
+                return false;
+            }
             hgw.write(fileName);
             System.out.println("heap written to " + fileName);
             return true;
@@ -188,7 +222,7 @@ public class JMap extends Tool {
     }
 
     public boolean writeHeapHprofBin() {
-        return writeHeapHprofBin("heap.bin");
+        return writeHeapHprofBin("heap.bin", -1);
     }
 
     private boolean writeHeapGXL(String fileName) {
diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/utilities/HeapHprofBinWriter.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/utilities/HeapHprofBinWriter.java
index d16aacd7a64..1769447ddd4 100644
--- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/utilities/HeapHprofBinWriter.java
+++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/utilities/HeapHprofBinWriter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2004, 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2004, 2021, 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
@@ -25,8 +25,11 @@
 package sun.jvm.hotspot.utilities;
 
 import java.io.*;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.nio.channels.*;
 import java.util.*;
+import java.util.zip.*;
 import sun.jvm.hotspot.debugger.*;
 import sun.jvm.hotspot.memory.*;
 import sun.jvm.hotspot.oops.*;
@@ -386,15 +389,39 @@ public class HeapHprofBinWriter extends AbstractHeapGraphWriter {
     public HeapHprofBinWriter() {
         this.KlassMap = new ArrayList<Klass>();
         this.names = new HashSet<Symbol>();
+        this.gzLevel = 0;
+    }
+
+    public HeapHprofBinWriter(int gzLevel) {
+        this.KlassMap = new ArrayList<Klass>();
+        this.names = new HashSet<Symbol>();
+        this.gzLevel = gzLevel;
     }
 
     public synchronized void write(String fileName) throws IOException {
         VM vm = VM.getVM();
 
+        // Check whether we should dump the heap as segments
+        useSegmentedHeapDump = isCompression() ||
+                (vm.getUniverse().heap().used() > HPROF_SEGMENTED_HEAP_DUMP_THRESHOLD);
+
         // open file stream and create buffered data output stream
         fos = new FileOutputStream(fileName);
-        out = new DataOutputStream(new BufferedOutputStream(fos));
-
+        hprofBufferedOut = null;
+        OutputStream dataOut = fos;
+        if (useSegmentedHeapDump) {
+            if (isCompression()) {
+                dataOut = new GZIPOutputStream(fos) {
+                    {
+                        this.def.setLevel(gzLevel);
+                    }
+                };
+            }
+            hprofBufferedOut = new SegmentedOutputStream(dataOut);
+        } else {
+            hprofBufferedOut = new SegmentedOutputStream(fos, false /* allowSegmented */);
+        }
+        out = new DataOutputStream(hprofBufferedOut);
         dbg = vm.getDebugger();
         objectHeap = vm.getObjectHeap();
 
@@ -419,9 +446,6 @@ public class HeapHprofBinWriter extends AbstractHeapGraphWriter {
         FLOAT_SIZE = objectHeap.getFloatSize();
         DOUBLE_SIZE = objectHeap.getDoubleSize();
 
-        // Check weather we should dump the heap as segments
-        useSegmentedHeapDump = vm.getUniverse().heap().used() > HPROF_SEGMENTED_HEAP_DUMP_THRESHOLD;
-
         // hprof bin format header
         writeFileHeader();
 
@@ -447,10 +471,11 @@ public class HeapHprofBinWriter extends AbstractHeapGraphWriter {
         // flush buffer stream.
         out.flush();
 
-        // Fill in final length
-        fillInHeapRecordLength();
-
-        if (useSegmentedHeapDump) {
+        if (!useSegmentedHeapDump) {
+            // Fill in final length.
+            fillInHeapRecordLength();
+        } else {
+            hprofBufferedOut.finish();
             // Write heap segment-end record
             out.writeByte((byte) HPROF_HEAP_DUMP_END);
             out.writeInt(0);
@@ -459,19 +484,18 @@ public class HeapHprofBinWriter extends AbstractHeapGraphWriter {
 
         // flush buffer stream and throw it.
         out.flush();
+        out.close();
         out = null;
-
-        // close the file stream
-        fos.close();
+        hprofBufferedOut = null;
     }
 
     @Override
     protected void writeHeapRecordPrologue() throws IOException {
-        if (currentSegmentStart == 0) {
-            // write heap data header, depending on heap size use segmented heap
-            // format
-            out.writeByte((byte) (useSegmentedHeapDump ? HPROF_HEAP_DUMP_SEGMENT
-                    : HPROF_HEAP_DUMP));
+        if (useSegmentedHeapDump) {
+            hprofBufferedOut.enterSegmentMode();
+        } else if (currentSegmentStart == 0) {
+            // write heap data header
+            out.writeByte((byte) (HPROF_HEAP_DUMP));
             out.writeInt(0);
 
             // remember position of dump length, we will fixup
@@ -486,15 +510,12 @@ public class HeapHprofBinWriter extends AbstractHeapGraphWriter {
     @Override
     protected void writeHeapRecordEpilogue() throws IOException {
         if (useSegmentedHeapDump) {
-            out.flush();
-            if ((fos.getChannel().position() - currentSegmentStart - 4L) >= HPROF_SEGMENTED_HEAP_DUMP_SEGMENT_SIZE) {
-                fillInHeapRecordLength();
-                currentSegmentStart = 0;
-            }
+            hprofBufferedOut.exitSegmentMode();
         }
     }
 
     private void fillInHeapRecordLength() throws IOException {
+        assert !useSegmentedHeapDump : "fillInHeapRecordLength is not supported for segmented heap dump";
 
         // now get the current position to calculate length
         long dumpEnd = fos.getChannel().position();
@@ -513,13 +534,10 @@ public class HeapHprofBinWriter extends AbstractHeapGraphWriter {
         // seek the position to write length
         fos.getChannel().position(currentSegmentStart);
 
+        // write length
         int dumpLen = (int) dumpLenLong;
-
-        // write length as integer
-        fos.write((dumpLen >>> 24) & 0xFF);
-        fos.write((dumpLen >>> 16) & 0xFF);
-        fos.write((dumpLen >>> 8) & 0xFF);
-        fos.write((dumpLen >>> 0) & 0xFF);
+        byte[] lenBytes = genByteArrayFromInt(dumpLen);
+        fos.write(lenBytes);
 
         //Reset to previous current position
         fos.getChannel().position(currentPosition);
@@ -569,8 +587,10 @@ public class HeapHprofBinWriter extends AbstractHeapGraphWriter {
         long originalLengthInBytes = originalArrayLength * typeSize;
 
         // calculate the length of heap data
+        // only process when segmented heap dump is not used, since SegmentedOutputStream
+        // could create segment automatically.
         long currentRecordLength = (dumpEnd - currentSegmentStart - 4L);
-        if (currentRecordLength > 0 &&
+        if ((!useSegmentedHeapDump) && currentRecordLength > 0 &&
             (currentRecordLength + headerSize + originalLengthInBytes) > MAX_U4_VALUE) {
             fillInHeapRecordLength();
             currentSegmentStart = 0;
@@ -1226,6 +1246,18 @@ public class HeapHprofBinWriter extends AbstractHeapGraphWriter {
         return size;
     }
 
+    private boolean isCompression() {
+        return (gzLevel >= 1 && gzLevel <= 9);
+    }
+
+    // Convert integer to byte array with BIG_ENDIAN byte order.
+    private static byte[] genByteArrayFromInt(int value) {
+        ByteBuffer intBuffer = ByteBuffer.allocate(4);
+        intBuffer.order(ByteOrder.BIG_ENDIAN);
+        intBuffer.putInt(value);
+        return intBuffer.array();
+    }
+
     // We don't have allocation site info. We write a dummy
     // stack trace with this id.
     private static final int DUMMY_STACK_TRACE_ID = 1;
@@ -1233,9 +1265,11 @@ public class HeapHprofBinWriter extends AbstractHeapGraphWriter {
 
     private DataOutputStream out;
     private FileOutputStream fos;
+    private SegmentedOutputStream hprofBufferedOut;
     private Debugger dbg;
     private ObjectHeap objectHeap;
     private ArrayList<Klass> KlassMap;
+    private int gzLevel;
 
     // oopSize of the debuggee
     private int OBJ_ID_SIZE;
@@ -1274,4 +1308,221 @@ public class HeapHprofBinWriter extends AbstractHeapGraphWriter {
     }
 
     private Map<InstanceKlass, ClassData> classDataCache = new HashMap<>();
+
+    /**
+     * The class implements a buffered output stream for segmented data dump.
+     * It is used inside HeapHprofBinWritter only for heap dump.
+     * Because the current implementation of segmented heap dump needs to update
+     * the segment size at segment header, and because it is hard to modify the
+     * compressed data after they are written to file, this class first saves the
+     * uncompressed data into an internal buffer, and then writes through to the
+     * GZIPOutputStream when the whole segmented data are ready and the size is updated.
+     * If the data to be written are larger than internal buffer, or the internal buffer
+     * is full, the internal buffer will be extend to a larger one.
+     * This class defines a switch to turn on/off the segmented mode. If turned off,
+     * it behaves the same as BufferedOutputStream.
+     * */
+    private class SegmentedOutputStream extends BufferedOutputStream {
+        /**
+         * Creates a new buffered output stream to support segmented heap dump data.
+         *
+         * @param   out                 the underlying output stream.
+         * @param   allowSegmented      whether allow segmental dump.
+         */
+        public SegmentedOutputStream(OutputStream out, boolean allowSegmented) {
+            super(out, 8192);
+            segmentMode = false;
+            this.allowSegmented = allowSegmented;
+            segmentBuffer = new byte[SEGMENT_BUFFER_SIZE];
+            segmentWritten = 0;
+        }
+
+        /**
+         * Creates a new buffered output stream to support segmented heap dump data.
+         *
+         * @param   out      the underlying output stream.
+         */
+        public SegmentedOutputStream(OutputStream out) {
+            this(out, true);
+        }
+
+        /**
+         * Writes the specified byte to this buffered output stream.
+         *
+         * @param      b   the byte to be written.
+         * @throws     IOException  if an I/O error occurs.
+         */
+        @Override
+        public synchronized void write(int b) throws IOException {
+           if (segmentMode) {
+               if (segmentWritten == 0) {
+                   // At the begining of the segment.
+                   writeSegmentHeader();
+               } else if (segmentWritten == segmentBuffer.length) {
+                   // Internal buffer is full, extend a larger one.
+                   int newSize = segmentBuffer.length + SEGMENT_BUFFER_INC_SIZE;
+                   byte newBuf[] = new byte[newSize];
+                   System.arraycopy(segmentBuffer, 0, newBuf, 0, segmentWritten);
+                   segmentBuffer = newBuf;
+               }
+               segmentBuffer[segmentWritten++] = (byte)b;
+               return;
+           }
+           super.write(b);
+        }
+
+        /**
+         * Writes {@code len} bytes from the specified byte array
+         * starting at offset {@code off} to this output stream.
+         *
+         * @param      b     the data.
+         * @param      off   the start offset in the data.
+         * @param      len   the number of bytes to write.
+         * @throws     IOException  if an I/O error occurs.
+         */
+        @Override
+        public synchronized void write(byte b[], int off, int len) throws IOException {
+            if (segmentMode) {
+                if (segmentWritten == 0) {
+                    writeSegmentHeader();
+                }
+                // Data size is larger than segment buffer length, extend segment buffer.
+                if (segmentWritten + len > segmentBuffer.length) {
+                    int newSize = segmentBuffer.length + Math.max(SEGMENT_BUFFER_INC_SIZE, len);
+                    byte newBuf[] = new byte[newSize];
+                    System.arraycopy(segmentBuffer, 0, newBuf, 0, segmentWritten);
+                    segmentBuffer = newBuf;
+                }
+                System.arraycopy(b, off, segmentBuffer, segmentWritten, len);
+                segmentWritten += len;
+                return;
+            }
+            super.write(b, off, len);
+        }
+
+        /**
+         * Flushes this buffered output stream. This forces any buffered
+         * output bytes to be written out to the underlying output stream.
+         *
+         * @throws     IOException  if an I/O error occurs.
+         * @see        java.io.FilterOutputStream#out
+         */
+        @Override
+        public synchronized void flush() throws IOException {
+            if (segmentMode) {
+                // The case that nothing has been written in segment.
+                if (segmentWritten == 0) return;
+                // There must be more data than just header size written for non-empty segment.
+                assert segmentWritten > SEGMENT_HEADER_SIZE
+                        : "invalid header in segmented mode";
+
+                if (segmentWritten > (segmentBuffer.length)) {
+                    throw new RuntimeException("Heap segment size overflow.");
+                }
+
+                if (segmentWritten > SEGMENT_HEADER_SIZE) {
+                    fillSegmentSize(segmentWritten - SEGMENT_HEADER_SIZE);
+                    super.write(segmentBuffer, 0, segmentWritten);
+                    super.flush();
+                    segmentWritten = 0;
+                }
+                return;
+            }
+            super.flush();
+        }
+
+        /**
+         * Enters segmented mode, flush buffered data and set flag.
+         */
+        public void enterSegmentMode() throws IOException {
+            if (allowSegmented && !segmentMode && segmentWritten == 0) {
+                super.flush();
+                segmentMode = true;
+                segmentWritten = 0;
+            }
+        }
+
+        /**
+         * Before finish, flush all data in buffer.
+         */
+        public void finish() throws IOException {
+            if (allowSegmented && segmentMode) {
+                flush();
+                assert segmentWritten == 0;
+                segmentMode = false;
+            }
+        }
+
+        /**
+         * Exits segmented mode, flush segmented data.
+         * @param    force    flush data regardless whether the buffer is full
+         */
+        public void exitSegmentMode() throws IOException {
+            if (allowSegmented && segmentMode && shouldFlush()) {
+                flush();
+                assert segmentWritten == 0;
+                segmentMode = false;
+            }
+        }
+
+
+        /**
+         * Check whether the data should be flush based on data saved in
+         * segmentBuffer.
+         * This method is used to control the segments number and the memory usage.
+         * If segment is too small, there will be lots segments in final dump file.
+         * If it is too large, lots of memory is used in RAM.
+         */
+        private boolean shouldFlush() {
+            // return true if data in segmentBuffer has been extended.
+            return segmentWritten > SEGMENT_BUFFER_SIZE;
+        }
+
+        /**
+         * Writes the write segment header into internal buffer.
+         */
+        private void writeSegmentHeader() {
+            assert segmentWritten == 0;
+            segmentBuffer[segmentWritten++] = (byte)HPROF_HEAP_DUMP_SEGMENT;
+            writeInteger(0);
+            // segment size, write dummy length of 0 and we'll fix it later.
+            writeInteger(0);
+        }
+
+        /**
+         * Fills the segmented data size into the header.
+         */
+        private void fillSegmentSize(int size) {
+            byte[] lenBytes = genByteArrayFromInt(size);
+            System.arraycopy(lenBytes, 0, segmentBuffer, 5, 4);
+        }
+
+        /**
+         * Writes an {@code int} to the internal segment buffer
+         * {@code written} is incremented by {@code 4}.
+         */
+        private final void writeInteger(int value) {
+            byte[] intBytes = genByteArrayFromInt(value);
+            System.arraycopy(intBytes, 0, segmentBuffer, segmentWritten, 4);
+            segmentWritten += 4;
+        }
+
+        // The buffer size for segmentBuffer.
+        // Since it is hard to calculate and fill the data size of an segment in compressed
+        // data, making the segmented data stored in this buffer could help rewrite the data
+        // size before the segmented data are written to underlying GZIPOutputStream.
+        private static final int SEGMENT_BUFFER_SIZE = 1 << 20;
+        // Buffer size used to extend the segment buffer.
+        private static final int SEGMENT_BUFFER_INC_SIZE = 1 << 10;
+        // Headers:
+        //    1 byte for HPROF_HEAP_DUMP_SEGMENT
+        //    4 bytes for timestamp
+        //    4 bytes for size
+        private static final int SEGMENT_HEADER_SIZE = 9;
+        // Segment support.
+        private boolean segmentMode;
+        private boolean allowSegmented;
+        private byte segmentBuffer[];
+        private int segmentWritten;
+    }
 }
diff --git a/test/hotspot/jtreg/serviceability/sa/ClhsdbDumpheap.java b/test/hotspot/jtreg/serviceability/sa/ClhsdbDumpheap.java
index 3a1c0b23832..389a9fbfc70 100644
--- a/test/hotspot/jtreg/serviceability/sa/ClhsdbDumpheap.java
+++ b/test/hotspot/jtreg/serviceability/sa/ClhsdbDumpheap.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2021, 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
@@ -24,7 +24,13 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 
+import static jdk.test.lib.Asserts.assertTrue;
+import static jdk.test.lib.Asserts.assertFalse;
+import jdk.test.lib.hprof.HprofParser;
 import jdk.test.lib.apps.LingeredApp;
 import jdk.test.lib.hprof.parser.HprofReader;
 import jtreg.SkippedException;
@@ -39,6 +45,10 @@ import jtreg.SkippedException;
  */
 
 public class ClhsdbDumpheap {
+    // The default heap dump file name defined in JDK.
+    private static final String HEAP_DUMP_FILENAME_DEFAULT = "heap.bin";
+    private static final String HEAP_DUMP_GZIPED_FILENAME_DEFAULT = "heap.bin.gz";
+
     public static void printStackTraces(String file) {
         try {
             System.out.println("HprofReader.getStack() output:");
@@ -51,26 +61,116 @@ public class ClhsdbDumpheap {
         }
     }
 
+    private static void verifyDumpFile(File dump) throws Exception {
+        assertTrue(dump.exists() && dump.isFile(), "Could not create dump file " + dump.getAbsolutePath());
+        printStackTraces(dump.getAbsolutePath());
+    }
+
+    private static class SubTest {
+        private String cmd;
+        private String fileName;
+        private String expectedOutput;
+        boolean compression;
+        boolean needVerify;
+
+        public SubTest(String comm, String fName, boolean isComp, boolean verify, String expected) {
+            cmd = comm;
+            fileName = fName;
+            expectedOutput = expected;
+            compression = isComp;
+            needVerify = verify;
+        }
+
+        public String getCmd() { return cmd; }
+        public String getFileName() { return fileName; }
+        public String getExpectedOutput() { return expectedOutput; }
+        public boolean isCompression() { return compression; }
+        public boolean needVerify() { return needVerify; }
+    }
+
+    private static void runTest(long appPid, SubTest subtest) throws Exception {
+        ClhsdbLauncher test = new ClhsdbLauncher();
+        String fileName = subtest.getFileName();
+        String cmd = subtest.getCmd();
+        String expectedOutput = subtest.getExpectedOutput();
+        boolean compression = subtest.isCompression();
+        /* The expected generated file, used to distinguish with fileName in case fileName is blank or null */
+        String expectedFileName = fileName;
+        if (fileName == null || fileName.length() == 0) {
+            if (!compression) {
+                expectedFileName = HEAP_DUMP_FILENAME_DEFAULT;
+            } else {
+                expectedFileName = HEAP_DUMP_GZIPED_FILENAME_DEFAULT;
+            }
+        }
+        assertTrue (expectedFileName != null && expectedFileName.length() > 0,
+                "Expected generated file name must have value");
+        File file = new File(expectedFileName);
+        if (file.exists()) {
+            file.delete();
+        }
+        String command = cmd + fileName;
+        List<String> cmds = List.of(command);
+        Map<String, List<String>> expStrMap = new HashMap<>();
+        expStrMap.put(command, List.of(expectedOutput));
+        test.run(appPid, cmds, expStrMap, null);
+        if (subtest.needVerify()) {
+            verifyDumpFile(file);
+        }
+        file.delete();
+    }
+
     public static void main(String[] args) throws Exception {
         System.out.println("Starting ClhsdbDumpheap test");
 
         LingeredApp theApp = null;
         try {
+            // Use file name different with JDK's default value "heap.bin".
             String heapDumpFileName = "heapdump.bin";
-            ClhsdbLauncher test = new ClhsdbLauncher();
+            String heapDumpFileNameGz = "heapdump.bin.gz";
 
             theApp = new LingeredApp();
             LingeredApp.startApp(theApp);
             System.out.println("Started LingeredApp with pid " + theApp.getPid());
 
-            List<String> cmds = List.of("dumpheap " + heapDumpFileName);
-
-            Map<String, List<String>> expStrMap = new HashMap<>();
-            expStrMap.put("dumpheap", List.of(
-                    "heap written to " + heapDumpFileName));
-
-            test.run(theApp.getPid(), cmds, expStrMap, null);
-            printStackTraces(heapDumpFileName);
+            SubTest[] subtests = new SubTest[] {
+                    new SubTest("dumpheap ", heapDumpFileName, false/*compression*/, true,/*verify*/
+                            "heap written to " + heapDumpFileName),
+                    new SubTest("dumpheap gz=1 ", heapDumpFileNameGz, true, true,
+                            "heap written to " + heapDumpFileNameGz),
+                    new SubTest("dumpheap gz=9 ", heapDumpFileNameGz, true, true,
+                            "heap written to " + heapDumpFileNameGz),
+                    new SubTest("dumpheap gz=0 ", heapDumpFileNameGz, true, false,
+                            "Usage: dumpheap \\[gz=<1-9>\\] \\[filename\\]"),
+                    new SubTest("dumpheap gz=100 ", heapDumpFileNameGz, true, false,
+                            "Usage: dumpheap \\[gz=<1-9>\\] \\[filename\\]"),
+                    new SubTest("dumpheap gz= ", heapDumpFileNameGz, true, false,
+                            "Usage: dumpheap \\[gz=<1-9>\\] \\[filename\\]"),
+                    new SubTest("dumpheap gz ", heapDumpFileNameGz, true, false,
+                            "Usage: dumpheap \\[gz=<1-9>\\] \\[filename\\]"),
+                    new SubTest("dumpheap", "", false, true,
+                            "heap written to " + HEAP_DUMP_FILENAME_DEFAULT),
+                    new SubTest("dumpheap gz=1", "", true, true,
+                            "heap written to " + HEAP_DUMP_GZIPED_FILENAME_DEFAULT),
+                    new SubTest("dumpheap gz=9", "", true, true,
+                            "heap written to " + HEAP_DUMP_GZIPED_FILENAME_DEFAULT),
+                    new SubTest("dumpheap gz=0", "", true, false,
+                            "Usage: dumpheap \\[gz=<1-9>\\] \\[filename\\]"),
+                    new SubTest("dumpheap gz=100", "", true, false,
+                            "Usage: dumpheap \\[gz=<1-9>\\] \\[filename\\]"),
+                    // Command "dumpheap gz=".
+                    new SubTest("dumpheap ", "gz=", true, false,
+                            "Usage: dumpheap \\[gz=<1-9>\\] \\[filename\\]"),
+                    // Command "dumpheap gz".
+                    new SubTest("dumpheap ", "gz", false, true, "heap written to gz"),
+                    // Command "dump heap gz=1 gz=2".
+                    new SubTest("dumpheap gz=1", "gz=2", true, false,
+                            "Usage: dumpheap \\[gz=<1-9>\\] \\[filename\\]")
+            };
+            // Run subtests
+            for (int i = 0; i < subtests.length;i++) {
+                runTest(theApp.getPid(), subtests[i]);
+            }
         } catch (SkippedException se) {
             throw se;
         } catch (Exception ex) {
diff --git a/test/jdk/sun/tools/jhsdb/HeapDumpTest.java b/test/jdk/sun/tools/jhsdb/HeapDumpTest.java
index 69b46b684a3..970fcc8e4b2 100644
--- a/test/jdk/sun/tools/jhsdb/HeapDumpTest.java
+++ b/test/jdk/sun/tools/jhsdb/HeapDumpTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2021, 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
@@ -49,13 +49,13 @@ import jdk.test.lib.SA.SATestUtils;
 public class HeapDumpTest {
 
     private static LingeredAppWithExtendedChars theApp = null;
-
+    private static final String SUCCESS_STRING = "heap written to";
     /**
      *
      * @param vmArgs  - tool arguments to launch jhsdb
      * @return exit code of tool
      */
-    public static void launch(String expectedMessage, List<String> toolArgs)
+    public static void launch(int expectedExitValue, List<String> toolArgs)
         throws IOException {
 
         System.out.println("Starting LingeredApp");
@@ -81,9 +81,12 @@ public class HeapDumpTest {
             System.out.println(output.getStdout());
             System.out.println("stderr:");
             System.out.println(output.getStderr());
-            output.shouldContain(expectedMessage);
-            output.shouldHaveExitValue(0);
-
+            output.shouldHaveExitValue(expectedExitValue);
+            if (expectedExitValue == 0) {
+                output.shouldContain(SUCCESS_STRING);
+            } else {
+                output.stdoutShouldNotContain(SUCCESS_STRING);
+            }
         } catch (Exception ex) {
             throw new RuntimeException("Test ERROR " + ex, ex);
         } finally {
@@ -91,10 +94,9 @@ public class HeapDumpTest {
         }
     }
 
-    public static void launch(String expectedMessage, String... toolArgs)
+    public static void launch(int expectedExitValue, String... toolArgs)
         throws IOException {
-
-        launch(expectedMessage, Arrays.asList(toolArgs));
+        launch(expectedExitValue, Arrays.asList(toolArgs));
     }
 
     public static void printStackTraces(String file) throws IOException {
@@ -108,31 +110,66 @@ public class HeapDumpTest {
         }
     }
 
-    public static void testHeapDump() throws IOException {
+    public static void testHeapDump(SubTest subtest) throws IOException {
+        String gzOption = subtest.getGzOption();
+        boolean checkSuccess = subtest.needCheckSuccess();
+        int expectedExitValue = checkSuccess ? 0 : 1;
+
         File dump = new File("jhsdb.jmap.heap." +
                              System.currentTimeMillis() + ".hprof");
         if (dump.exists()) {
             dump.delete();
         }
+        if (gzOption == null || gzOption.length() == 0) {
+            launch(expectedExitValue, "jmap",
+                   "--binaryheap", "--dumpfile=" + dump.getAbsolutePath());
+        } else {
+            launch(expectedExitValue, "jmap",
+                   "--binaryheap", gzOption, "--dumpfile=" + dump.getAbsolutePath());
+        }
 
-        launch("heap written to", "jmap",
-               "--binaryheap", "--dumpfile=" + dump.getAbsolutePath());
+        if (checkSuccess) {
+            assertTrue(dump.exists() && dump.isFile(),
+                       "Could not create dump file " + dump.getAbsolutePath());
 
-        assertTrue(dump.exists() && dump.isFile(),
-                   "Could not create dump file " + dump.getAbsolutePath());
-
-        printStackTraces(dump.getAbsolutePath());
-
-        dump.delete();
+            printStackTraces(dump.getAbsolutePath());
+            dump.delete();
+        } else {
+            assertTrue(!dump.exists(), "Unexpected file created: " + dump.getAbsolutePath());
+        }
     }
 
     public static void main(String[] args) throws Exception {
         SATestUtils.skipIfCannotAttach(); // throws SkippedException if attach not expected to work.
-        testHeapDump();
-
+        SubTest[] subtests = new SubTest[] {
+                new SubTest("", true/*checkSuccess*/),
+                new SubTest("--gz=1", true),
+                new SubTest("--gz=9", true),
+                new SubTest("--gz=0", false),
+                new SubTest("--gz=100", false),
+                new SubTest("--gz=", false),
+                new SubTest("--gz", false),
+        };
+        // Run subtests
+        for (int i = 0; i < subtests.length;i++) {
+            testHeapDump(subtests[i]);
+        }
         // The test throws RuntimeException on error.
         // IOException is thrown if LingeredApp can't start because of some bad
         // environment condition
         System.out.println("Test PASSED");
     }
+
+    private static class SubTest {
+        private String gzOption;
+        boolean needCheckSuccess;
+
+        public SubTest(String gzOpt, boolean checkSuccess) {
+            gzOption = gzOpt;
+            needCheckSuccess = checkSuccess;
+        }
+
+        public String getGzOption() { return gzOption; }
+        public boolean needCheckSuccess() { return needCheckSuccess; }
+    }
 }
diff --git a/test/lib/jdk/test/lib/hprof/parser/Reader.java b/test/lib/jdk/test/lib/hprof/parser/Reader.java
index d881b435f3f..da18dca1219 100644
--- a/test/lib/jdk/test/lib/hprof/parser/Reader.java
+++ b/test/lib/jdk/test/lib/hprof/parser/Reader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2021, 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
@@ -33,6 +33,7 @@
 package jdk.test.lib.hprof.parser;
 
 import java.io.*;
+import java.util.zip.GZIPInputStream;
 import jdk.test.lib.hprof.model.*;
 
 /**
@@ -45,6 +46,8 @@ import jdk.test.lib.hprof.model.*;
 
 public abstract class Reader {
     protected PositionDataInputStream in;
+    // Magic number of gzip dump file header.
+    private static final int GZIP_HEADER_MAGIC = 0x1f8b08;
 
     protected Reader(PositionDataInputStream in) {
         this.in = in;
@@ -142,9 +145,39 @@ public abstract class Reader {
                                       true, debugLevel);
                 r.read();
                 return r.printStackTraces();
+            } else if ((i >>> 8) == GZIP_HEADER_MAGIC) {
+                // Possible gziped file, try decompress it and get the stack trace.
+                in.close();
+                String deCompressedFile = "heapdump" + System.currentTimeMillis() + ".hprof";
+                File out = new File(deCompressedFile);
+                // Decompress to get dump file.
+                try {
+                    GZIPInputStream gis = new GZIPInputStream(new FileInputStream(heapFile));
+                    FileOutputStream fos = new FileOutputStream(out);
+                    byte[] buffer = new byte[1024 * 1024];
+                    int len = 0;
+                    while ((len = gis.read(buffer)) > 0) {
+                        fos.write(buffer, 0, len);
+                    }
+                    // Check dump data header and print stack trace.
+                    PositionDataInputStream in2 = new PositionDataInputStream(
+                        new BufferedInputStream(new FileInputStream(out)));
+                    i = in2.readInt();
+                    if (i == HprofReader.MAGIC_NUMBER) {
+                        HprofReader r
+                            = new HprofReader(deCompressedFile, in2, dumpNumber,
+                                              true, debugLevel);
+                        r.read();
+                        return r.printStackTraces();
+                    }
+                } catch (Exception e) {
+                    throw new IOException("Can not decompress the compressed hprof file", e);
+                }
+                out.delete();
             } else {
                 throw new IOException("Unrecognized magic number: " + i);
             }
         }
+        return null;
     }
 }