深入理解JVM——第五章、JVM性能优化学习笔记

一、内存溢出

内存溢出的原因:程序在申请内存时,没有足够的内存空间

1、栈溢出

方法死循环递归调用(StackOverflowError)、不断建立线程(OutOfMemoryError)

由于在Hotspot虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于Hotspot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定,关于虚拟机栈和本地方法栈可以出现以下两种异常: 

第一种、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常

第二种、如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

关于这两种情况存在相互重叠的地方 ,当栈空间无法分配时是线程请求深度过大?还是无法申请足够的内存空间?本质是同一事件两种描述而已。

下面这个例子,将实验范围限制于单线程中的操作,尝试了下面两种方法均无法让需积极产生OutOfMemoryError异常,尝试的结果都是获得StackOverflowError异常

第一种方法:使用-Xss 参数减少栈内存容量,结果:抛出StackOverflowError异常,异常出现时输出的栈的深度相应缩小 。

第二种方法:定义了大量的本地变量,增大此方法帧中本地变量表的长度。结果:抛出StackOverflowError异常时输出的堆栈深度相应减小。

1.1、测试方法:

// VM Args: -Xss128k
public class StackOverFlow {
    private int stackLength = 1;
    public void stackLeak(){
        stackLength++;
        stackLeak(); // 死递归
    }
    public static void main(String[] args)throws Throwable {
        StackOverFlow javaStack = new StackOverFlow();
        javaStack.stackLeak();
        System.out.println("stack length: "+ javaStack.stackLength);
    }
}

1.2、运行抛出的异常为:

Exception in thread "main" java.lang.StackOverflowError

at com.chj.chap05.oom.StackOverFlow.stackLeak(StackOverFlow.java:10)

at com.chj.chap05.oom.StackOverFlow.stackLeak(StackOverFlow.java:11)

1.3、虚拟机栈和本地方法栈溢出分析:

实验结果表明:在单个线程下,无论是由于栈帧太大还是虚拟机容量太小,当内存无法分配的时候虚拟机都抛出的是StackOverflowError。

如果测试不限于单线程,通过不断的建立线程的方式倒是可以产生内存溢出异常,但是这样产生的内存溢出与栈空间是否够大不存在任何联系,或者说,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

原因是,操作系统分配给每个线程的内存是有限的,譬如32位Windows限制为2GB。虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为2G(操作系统内存)减去Xmx(堆最大容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗的内存很小,可以忽略。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。每个线程分配到栈容量越大,可以建立的线程数自然越少,建立线程时越容易把剩余的内存耗尽。

这一点需要在开发多线程的应用时特别注意,出现StackOverflowError异常时有错误堆栈可以阅读,相对来说,比较容易找到问题的所在。而且,如果使用虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的,所以只能说在大多数情况下)达到1000~2000完全没有问题,对于正常的方法调用(包括递归),这个深度应该完全够用了。但是,如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程了。如果没有这方面的处理经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。

2、堆溢出

Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制来清除这些对象,那么对象数量到达最大堆容量限制后就会产生内存溢出异常。

2.1、先创建测试类:

public static void main(String[] args){
    //String[] strings = new String[100000000];  //100m的数组(堆)
    List list = new LinkedList<>(); //在方法执行的过程中,它是GCRoots
    int i =0;
    while(true){
        i++;
        if(i%10000==0) System.out.println("i="+i);
        list.add(new Object());
    }
} 
  

2.2、配置jvm大小

-Xms20m -Xmx20m -XX:+PrintGCDetails    堆的大小30M

-XX:+HeapDumpOnOutOfMemoryError,可以让虚拟机在出现内存溢出时Dump出当前的内存转储快照以便事后进行分析

2.3、运行抛出的异常为:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) [PSYoungGen: 5631K->0K(6144K)] [ParOldGen: 13557K->646K(13824K)] 19189K->646K(19968K), [Metaspace: 3636K->3636K(1056768K)], 0.0033870 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
at java.util.LinkedList.linkLast(LinkedList.java:142)
at java.util.LinkedList.add(LinkedList.java:338)
at com.chj.chap05.oom.HeapOom.main(HeapOom.java:19)
Heap
 PSYoungGen      total 6144K, used 154K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
  eden space 5632K, 2% used [0x00000000ff980000,0x00000000ff9a6a10,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)

 ParOldGen       total 13824K, used 646K [0x00000000fec00000, 0x00000000ff980000, 0x00000000ff980000)
  object space 13824K, 4% used [0x00000000fec00000,0x00000000feca19c0,0x00000000ff980000)
 Metaspace       used 3643K, capacity 4540K, committed 4864K, reserved 1056768K
  class space    used 401K, capacity 428K, committed 512K, reserved 1048576K

2.4、分析:

java堆内存的OOM异常时实际应用中常见的内存溢出异常情况。当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。

要解决这个区域的异常,一般的手段是先通过内存映像工具如(Eclipse MemoryAnalyzer)对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分析到底是出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow)。

如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链,于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾回收器无法自动回收它们的。掌握了泄露对象的类型信息及GC Roots引用链的信息,就可以比较容易确定发生泄露的代码位置。 

如果不存在内存泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

3、直接内存

DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样,下面的代码越过了DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回示例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。因为虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。

设置jvm虚拟机大小:

VM Args : -Xmx20M -XX:MaxDirectMemorySize=10M

3.1、编写测试代码:

/**
 * VM Args:-XX:MaxDirectMemorySize=100m
 * 限制最大直接内存大小100m 直接内存溢出
 */
public class DirectMenoryOOM {
    public static void main(String[] args) {
        //直接分配128M的直接内存(100M)
        ByteBuffer bb = ByteBuffer.allocateDirect(128*1024*1204);
    }
}

3.2 运行结果异常:

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:693)
at java.nio.DirectByteBuffer.(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.chj.chap05.oom.DirectMenoryOOM.main(DirectMenoryOOM.java:12)

由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后文件很小,而程序中有直接或简介使用了NIO,那就可以考虑一下是不是这方面的原因。

4、方法区和运行时常量池溢出

在经常动态生产大量Class的应用中,CGLIb字节码增强,动态语言,大量JSP(JSP第一次运行需要编译成Java类),基于OSGi的应用(同一个类,被不同的加载器加载也会设为不同的类)。由于运行时常量池是方法去的一部分,因此这两个区域的溢出测试可以放在一起进行。

String.intern()方法是一个native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的string对象;否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。

在JDK1.7及之前的版本中,由于常量池分配在永久代中,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,注意JDK1.8已经使用元空间 来代替永久区,所以在1.8中,这两个参数将被忽略,而改使用-XX:MetaspaceSize -XX:MaxMetaspaceSize 来控制元空间,从而间接限制其中常量池的容量,

4.1、代码如下:

/**
 *  JDK1.7 以下运行,VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 *  JDK1.8  VM Arges: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M -XX:+PrintGCDetails
 */
public class JavaMethodAreaOOM18 {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }
    static class OOMObject {
    }
}

4.2、JVM参数配置

在JDK1.8中,需要将参数修改:

-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M

参数注意大小写;和-XX:PermSize=10M -XX:MaxPermSize=10M 在1.8之前是相同效果

4.3、运行结果:

Metaspace(元空间)内存溢出了。
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
... 8 more    
Heap
 PSYoungGen      total 861696K, used 11711K [0x000000076b380000, 0x00000007a0780000, 0x00000007c0000000)
  eden space 861184K, 1% used [0x000000076b380000,0x000000076beefd80,0x000000079fc80000)
  from space 512K, 0% used [0x00000007a0700000,0x00000007a0700000,0x00000007a0780000)
  to   space 1536K, 0% used [0x00000007a0480000,0x00000007a0480000,0x00000007a0600000)

 ParOldGen       total 2778624K, used 2303K [0x00000006c1a00000, 0x000000076b380000, 0x000000076b380000)
  object space 2778624K, 0% used [0x00000006c1a00000,0x00000006c1c3ffe8,0x000000076b380000)
 Metaspace       used 9672K, capacity 10186K, committed 10240K, reserved 1058816K
  class space    used 858K, capacity 881K, committed 896K, reserved 1048576K

二、内存泄漏

程序在申请内存后,无法释放已申请的内存空间。

1、长生命周期的对象持有短生命周期对象的引用

例如将ArrayList设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏

2、连接未关闭

如数据库连接、网络连接和IO连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。

3、变量作用域不合理

例如,1.一个变量的定义的作用范围大于其使用范围,2.如果没有及时地把对象设置为null。

4、内部类持有外部类

Java的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏。

如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏(你认为垃圾回收器会回收掉外部类的实例,但由于内部类持有外部类的引用,导致垃圾回收器不能正常工作)

解决方法:你可以在内部类的内部显示持有一个外部类的软引用(或弱引用),并通过构造方法的方式传递进来,在内部类的使用过程中,先判断一下外部类是否被回收;

public class NoStaticInternal {
    public  int k=13;
    private static String string="King";
    protected float j=1.5f;
    public static void show(){
        System.out.println("show");
    }
    private void add(){
        System.out.println("add");
    }
    public static void main(String[] args) {
        NoStaticInternal m=new NoStaticInternal();
        //非静态内部类的构造方式
        Child c=m.new Child();
//        Child c= new Child(); //静态内部类的构造方式
        c.test();
    }
    //内部类Child --静态的,防止内存泄漏
//    static  class Child{
    class Child{ // 非静态,导致内存泄漏
        public int i;
        public void test(){
            System.out.println("k=:"+k);
            System.out.println("string:"+string);
            add();
            System.out.println("j=:"+j);
            show();
        }
    }
}

5、Hash值改变

在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露。

public class Node {
    private int x;
    private int y;
    public Node(int x, int y) {
        super();
        this.x = x;
        this.y = y;
    }
    //重写HashCode的方法
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        result = prime * result + y;
        return result;
    }
    //改变y的值:同时改变hashcode
    public void setY(int y) {
        this.y = y;
    }
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        Node nod1 = new Node(1, 3);
        Node nod2 = new Node(3, 5);
        hashSet.add(nod1);
        hashSet.add(nod2);
        nod2.setY(7);//nod2的Hash值改变
        hashSet.remove(nod2);//删掉nod2节点
        System.out.println(hashSet.size());
    }
}

打印结果为2,说明删掉nod2节点并未成功,原因是他的hash值改变了,无法删除。

6、单例模式

不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏。

7、内存泄漏和内存溢出辨析

相同与不同:

  • 内存溢出:实实在在的内存空间不足导致;
  • 内存泄漏:该释放的对象没有释放,多见于自己使用容器保存元素的情况下。

如何避免:

  • 内存溢出:检查代码以及设置足够的空间
  • 内存泄漏:一定是代码有问题

往往很多情况下,内存溢出往往是内存泄漏造成的。

三、分析工具MAT

1、浅堆和深堆

  • 浅堆 :(Shallow Heap)是指一个对象所消耗的内存。例如,在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节。
  • 深堆:这个对象被GC回收后,可以真实释放的内存大小,也就是只能通过对象被直接间接访问到的所有对象的集合。通俗地说,就是指仅被对象所持有的对象的集合。深堆是指对象的保留集中所有的对象的浅堆大小之和。

举例:对象A引用了C和D,对象B引用了E。那么对象A的浅堆大小只是A本身,而如果A被回收,那么C和D都会被回收(可达性分析算法),所以A的深堆大小为A+C+D之和,同时由于对象E还可以通过对象B访问到,因此不在对象A的深堆范围内。

深入理解JVM——第五章、JVM性能优化学习笔记_第1张图片

1.1、示例代码

/**
 * 堆内存溢出 导出(可以通过mat工具进行分析)
 * VM Args:-Xms30m -Xmx30m  -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
 */
public class DumpOom {
    public static void main(String[] args) {
        List list = new LinkedList<>(); //在方法执行的过程中,它是GCRoots
        int i =0;
        while(true){
            i++;
            if(i%10000==0) System.out.println("i="+i);
            list.add(new Object()); //node  40- 24 =16
        }
    }
} 
  

1.2、运行结果

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.LinkedList.linkLast(LinkedList.java:142)
at java.util.LinkedList.add(LinkedList.java:338)
at com.chj.chap05.DumpOom.main(DumpOom.java:17)
Heap

 PSYoungGen      total 9216K, used 286K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 3% used [0x00000000ff600000,0x00000000ff647a20,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)

 ParOldGen       total 20480K, used 676K [0x00000000fe200000, 0x00000000ff600000, 0x00000000ff600000)
  object space 20480K, 3% used [0x00000000fe200000,0x00000000fe2a9378,0x00000000ff600000)
 Metaspace       used 3662K, capacity 4540K, committed 4864K, reserved 1056768K
  class space    used 402K, capacity 428K, committed 512K, reserved 1048576K

1.3、结果分析

Problem Suspect 1

The thread java.lang.Thread @ 0xfefbf3b8 main keeps local variables with total size 28,576,944 (98.04%) bytes.

The memory is accumulated in one instance of "java.lang.Thread" loaded by "".

深入理解JVM——第五章、JVM性能优化学习笔记_第2张图片

list.add(new Object());  // node  深堆40- 浅堆24 =16字节

incoming和outgoing

 

四、JDK工具

jdk 的bin 目录是jdk的工具目录,这些命令行工具大多数是 jdk/lib/tools.jar 类库的一层薄包装而已,它们主要的功能代码是在 tools 类库中实现的;介绍一下Sun JDK 监控和故障处理工具如下列表:

深入理解JVM——第五章、JVM性能优化学习笔记_第3张图片

1、命令行工具

1.1、jps命令

列出当前机器上正在运行的虚拟机进程,JPS从操作系统的临时目录上去找。

-q:仅仅显示进程,

-m:输出主函数传入的参数. 下的hello 就是在执行程序时从命令行输入的参数

-l: 输出应用程序主类完整package名称或jar完整名称.

-v: 列出jvm参数, -Xms20m -Xmx50m是启动程序指定的jvm参数

-V 输出通过标记的文件传递给JVM的参数(.hotspotrc文件,或者是通过参数-XX:Flags=指定的文件)。

-J 用于传递jvm选项到由javac调用的java加载器中,例如,“-J-Xms48m”将把启动内存设置为48M,使用-J选项可以非常方便的向基于Java的开发的底层虚拟机应用程序传递参数。下面样例均在linux的jdk1.7下测试。

1.2、jstat虚拟机统计信息监视工具

是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。

Jstat用于监控基于HotSpot的JVM,对其堆的使用情况进行实时的命令行的统计,使用jstat我们可以对指定的JVM做如下监控:

  • 类的加载及卸载情况
  • 查看新生代、老生代及持久代的容量及使用情况
  • 查看新生代、老生代及持久代的垃圾收集情况,包括垃圾回收的次数及垃圾回收所占用的时间
  • 查看新生代中Eden区及Survior区中容量及分配情况等

假设需要每250毫秒查询一次进程13616垃圾收集状况,一共查询10次,那命令应当是:jstat-gc 13616  250 10

常用参数:

-class (类加载器)
-compiler (JIT)
-gc (GC堆状态)
-gccapacity (各区大小)
-gccause (最近一次GC统计和原因)
-gcnew (新区统计)
-gcnewcapacity (新区大小)
-gcold (老区统计)
-gcoldcapacity (老区大小)
-gcpermcapacity (永久区大小)
-gcutil (GC统计汇总)
-printcompilation (HotSpot编译统计)

命令格式:

jstat [ option vmid [interval] [s|ms] [count]]

对于命令格式中的 VMID和 LVMID。

如过是本地虚拟机进程,VMID和LVMID是一致的,

如果是远程虚拟机,那VMID的格式应当是:

[protocol:] [//] lvmid[@hostname[:port]rvername]

参数interval 和count分别表示查询的间隔和次数,如果省略这两个参数,说明只查询一次。

如需要每250ms 查询一次进程 2764 垃圾收集状况,一共查询20次,命名应该是:

jstat -gc 2764 250 20

选项option代表着用户希望查询的虚拟机信息,主要分为三类:类装载,垃圾收集、运行期编译状况

深入理解JVM——第五章、JVM性能优化学习笔记_第4张图片新java 堆分为:生代和老年代,新生代一般划分为三块区域,Eden + From Survivor + To Survivor,Eden 和 Survivor 的内存比为8:1,每次只使用一个Eden 和一个 Survivor 区域,另一个 Survivor 用于复制收集算

详细介绍:

S0C、S1C、S0U、S1U:young代的Survivor 0/1区容量(Capacity)和使用量(Used)。0是FromSurvivor,1是ToSurvivor。
EC、EU:Eden区容量和使用量
OC、OU:年老代容量和使用量
MC、MU:元数据区(Metaspace)已经committed的内存空间和使用量
CCSC、CCSU:压缩Class(Compressed class space)committed的内存空间和使用量。
YGC、YGT:young代GC次数和GC耗时
FGC、FGCT:Full GC次数和Full GC耗时
GCT:GC总耗时

1.3、jinfo java配置信息工具

jinfo可以输出并修改运行时的java 进程的opts。用处比较简单用于输出JAVA系统参数及命令行参数。

1)jinfo的作用(function):

  • 实时地查看和调整虚拟机各项参数。使用jps命令的-v 参数可以查看jvm启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,就只能使用 jinfo的-flag 选项进行查询了;
  • jinfo还可以使用 -sysprops 选项把jvm 进程的 System.getProperties()的内容打印出来;
  • jinfo加入了运行期修改参数的能力,可以使用 -flag[+|-] name 或 -flag name=value 修改一部分运行期可写的jvm 参数值;(jinfo 在 windows平台的功能有较大限制,只提供了最基本的-flag选项)

2)命令格式:

jinfo [option] pid

使用样例:jinfo [option] pid //pid 通过 jps 来查看

  1. 参数介绍:

深入理解JVM——第五章、JVM性能优化学习笔记_第5张图片

jinfo –sysprops 可以查看由System.getProperties()取得的参数

jinfo –flag 未被显式指定的参数的系统默认值

jinfo –flags(注意s)显示虚拟机的参数

jinfo –flag +-[参数] 可以增加、删除参数,

但是仅限于由java -XX:+PrintFlagsFinal –version查询出来且为manageable的参数

深入理解JVM——第五章、JVM性能优化学习笔记_第6张图片

jinfo –flag -[参数] 可以去除参数

Thread.getAllStackTraces();

案例:JinfoTest类

1)程序运行时只打印简单GC

public static void main(String[] args) {
    while (true){
        byte[]b=null;
        for(int i=0;i<10;i++){
            b=new byte[1*1024*1024];
        }
        try{
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
  1. 通过jinfo修改 参数,打印GC详情:

修改命令:jinfo -flag +PrintGCDetails 18844

深入理解JVM——第五章、JVM性能优化学习笔记_第7张图片

 

1.4、jmapjava内存映像工具(memory map for java):

用于生成堆转储快照(一般称为heapdump或dump文件)。jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。和jinfo命令一样,jmap有不少功能在Windows平台下都是受限的,除了生成dump文件的-dump选项和用于查看每个类的实例、空间占用统计的-histo选项在所有操作系统都提供之外,其余选项都只能在Linux/Solaris下使用。

当然也可其他方法比如加参数-XX:+HeapDumpOnOutOfMemoryError参数,在虚拟机OOM异常的之后自动生成dump文件,也可以通过-XX:+HeapDumpOnCtrlBreak参数则可以使用Ctrl+Break键让虚拟机生成dump文件。在前文测试中就有生成。dump文件生成后可借助jha、MAT( Eclipse Memory Analyzer tool)、IBM HeapAnalyzer来对dump分析。

命令格式

jmap [ option ] pid

option的合法值和具体含义如下表:

jmap -dump:live,format=b,file=heap.bin

深入理解JVM——第五章、JVM性能优化学习笔记_第8张图片

Sun JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。

深入理解JVM——第五章、JVM性能优化学习笔记_第9张图片

测试案例1:

jmap -dump:live,format=b,file=heap.bin 1617

Dumping heap to /usr/local/jdk1.8.0_181/bin/heap.bin ...

Heap dump file created

生成快照结果:

深入理解JVM——第五章、JVM性能优化学习笔记_第10张图片

结果分析:

178 instances of "net.sf.ehcache.store.NotifyingMemoryStore", 
loaded by "org.apache.catalina.loader.ParallelWebappClassLoader @ 0xeb6d53b8" 
occupy 192,425,568 (59.01%) bytes. These instances are referenced from one instance of 
"java.util.concurrent.ConcurrentHashMap$Node[]", loaded by ""

Keywords
java.util.concurrent.ConcurrentHashMap$Node[]
net.sf.ehcache.store.NotifyingMemoryStore
org.apache.catalina.loader.ParallelWebappClassLoader @ 0xeb6d53b8

深入理解JVM——第五章、JVM性能优化学习笔记_第11张图片

测试案例2

命令:jmap -heap 1617

[root@hzcms-rest ~]# jmap -heap 1617
Attaching to process ID 1617, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.181-b13
using thread-local object allocation.
Mark Sweep Compact GC
Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 526385152 (502.0MB)
   NewSize                  = 11141120 (10.625MB)
   MaxNewSize               = 175439872 (167.3125MB)
   OldSize                  = 22413312 (21.375MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)
Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 157941760 (150.625MB)
   used     = 86512360 (82.5046157836914MB)
   free     = 71429400 (68.1203842163086MB)
   54.774848653073136% used

Eden Space:
   capacity = 140443648 (133.9375MB)
   used     = 84498168 (80.58373260498047MB)
   free     = 55945480 (53.35376739501953MB)
   60.165175999985415% used
From Space:
   capacity = 17498112 (16.6875MB)
   used     = 2014192 (1.9208831787109375MB)
   free     = 15483920 (14.766616821289062MB)
   11.510910434222847% used
To Space:
   capacity = 17498112 (16.6875MB)
   used     = 0 (0.0MB)
   free     = 17498112 (16.6875MB)
   0.0% used

tenured generation:
   capacity = 350945280 (334.6875MB)
   used     = 339725584 (323.98756408691406MB)
   free     = 11219696 (10.699935913085938MB)
   96.80300701009571% used
58465 interned Strings occupying 6068304 bytes.

1.5、jhat虚拟机堆转储快照分析工具尽量少用

jhat dump文件名

后屏幕显示“Server is ready.”的提示后,用户在浏览器中键入http://localhost:7000/就可以访问详情

深入理解JVM——第五章、JVM性能优化学习笔记_第12张图片

使用jhat可以在服务器上生成堆转储文件分析(一般不推荐,毕竟占用服务器的资源,比如一个文件就有1个G)。

1.6、jstackJava堆栈跟踪工具Stack Trace for Java

jstack(Stack Trace for Java)命令用于生产虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当虚拟机内每一条线程正在执行的方法堆栈集合,生产线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者等待着什么资源。

jstack命令格式:jstack [option] vmid

option选择的合法值域具有含有请看下表:

深入理解JVM——第五章、JVM性能优化学习笔记_第13张图片

 

在代码中可以用java.lang.Thread类的getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码就完成jstack的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈。

jstack执行样例:

jstack -l 19212

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x00000000151d2000 nid=0x5408 runnable [0x000000001593e000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:171)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)

        - locked <0x00000000fecc1180> (a java.io.InputStreamReader)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)

        - locked <0x00000000fecc1180> (a java.io.InputStreamReader)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
   Locked ownable synchronizers:
        - None

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001515c000 nid=0xd64 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   Locked ownable synchronizers:
        - None

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x0000000015109000 nid=0x4d9c runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   Locked ownable synchronizers:
        - None

深入理解JVM——第五章、JVM性能优化学习笔记_第14张图片

HSDIS:JIT生成代码反汇编(略)

2、可视化工具

JDK中除了提供大量的命令行工具外,还有两个功能强大的可视化工具:JConsole和VisualVM,这两个工具是JDK的正式成员。

2.1、JConsole:Java监视与管理控制台

JConsole(Java Monitoring and Management Console)是一种基于JMX的可视化监视、管理工具,也是一个为应用程序、设备、系统等植入管理功能的框架。它管理部分的功能是针对JMX Mbean进行管理。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。

知识扩展:JMX(Java Management Extensions)即ava管理扩展,MBean(managed beans)即被管理的Beans。一个MBean是一个被管理的Java对象,有点类似于JavaBean,一个设备、一个应用或者任何资源都可以被表示为MBean,MBean会暴露一个接口对外,这个接口可以读取或者写入一些对象中的属性。

启动:如下图所示:

深入理解JVM——第五章、JVM性能优化学习笔记_第15张图片

通过JDK_HOME/bin目录下的“jconsole.exe”启动JConsole后,讲自动搜索出本机运行的所有虚拟机进程,不需要用户自己再用JPS来查询了。也可以使用下面的“远程进程”功能来连接远程服务器,对远程虚拟机进行监控,若连接远程虚拟机,需要开启JMX服务才能连接。

深入理解JVM——第五章、JVM性能优化学习笔记_第16张图片

 

概述”页签显示的是整个虚拟机主要运行数据的概览,其中包括“堆内存使用情况”、“线程”、“类”、“CPU使用情况”4种信息的曲线图,这些曲线图是后面“内存”、“线程”、“类”页签的信息汇总。其它详细的页签都不加以说明,自行学习吧,或者后期有充足的时间在详解一下。

管理远程进程需要在远程程序的启动参数中增加:

-Djava.rmi.server.hostname=…..

-Dcom.sun.management.jmxremote

-Dcom.sun.management.jmxremote.port=8888

-Dcom.sun.management.jmxremote.authenticate=false

-Dcom.sun.management.jmxremote.ssl=false

2.2 VisualVM:多合一故障处理工具

VisualVM(ALL-in-One Java Troubleshooting Tool)是到目前为止随JDK发布的功能最强大的运行监控和故障处理程序,并且可以预见在未来的一段时间内都是官方主力发展的虚拟机故障处理工具。VisuaVM是基于NetBean平台开发的,因此它一开始就具备了插件扩展功能的特性,通过插件扩展支持,VisualVM几乎可以做到所欲命令行工具的功能和其它Plugins的无限可能性。

作用(functions)

1)显示虚拟机进程以及进程的配置,环境信息(jps,jinfo);

2)监视应用程序的CPU, GC, 堆, 方法区以及线程的信息(jstat、jstack);

3)dump 以及分析堆转储快照(jmap,jhat);

4)方法级的程序运行性能分析,找出被调用最多,运行时间最长的方法;

5)离线程序快照,收集程序的运行时配置,线程dump,内存dump 等信息建立一个快照,可以将快照发送开发者处进行bug 反馈;

6)其他plugins 的无限可能性;(在对应用程序进行检测时,还需要加载相应的插件)。

下图为进入VisualVM的主页:

深入理解JVM——第五章、JVM性能优化学习笔记_第17张图片

首次启动VisualVM后,读者先不必着急找应用程序进行监控,因为现在VisualVM还没有加载任何插件,虽然基本的监控、线程面板的功能主程序都以默认插件的形式提供了,但是不给VisualVM装任何扩展插件,就相当于放弃了它最精华的功能,和没有安装任何应用软件的操作系统差不多。

插件可以进行手工安装,在相关网站上下载*.nbm包后,点击“工具—插件—已下载”菜单,然后在弹出的对话框中指定nbm包枯井变可以进行安装,插件安装后存放在JDK_HOME/lib/visualvm/visualvm中。当然同样可以在有网络连接的环境下选择自动安装,点击“工具—插件—可用插件”弹出如下图所示的插件页签中选择合适的插件安装即可。

深入理解JVM——第五章、JVM性能优化学习笔记_第18张图片

从VisualVM主页的左菜单栏可以看到,显示的虚拟机进程跟JConsole显示的是一样的,还有一个远程虚拟机进程。当我点击进入一个虚拟机进程后的进程主页如以下所示(不同版本可以会有所差异):

深入理解JVM——第五章、JVM性能优化学习笔记_第19张图片

虚拟机进程主页包含了“概述”、“监视”、“抽样器”、“Visual GC”页签,其中“Visual GC”是我自己安装的插件,更详细的VisualVM使用说明就不多说了。后期学习在继续详细解释。

深入理解JVM——第五章、JVM性能优化学习笔记_第20张图片

 

深入理解JVM——第五章、JVM性能优化学习笔记_第21张图片

3、总结:

jdk提供的vm故障处理工具都比较实用,常用的jps,jstat,jmap,jstack以及可视化工具visualvm,当然根据个人实际实用情况,可能还选用第三方的工具进行dump分析,如eclipse的MAT(Memory Analyzer Tool)等。灵活实用这些工具,可以给处理问题带来很大的便利,要知道出现什么问题了考虑使用什么工具,其它的就看你的经验吧。

你可能感兴趣的:(深入理解JVM)