Skip to main content

如何排查Java内存泄露

转发
java内存泄露典型特征

  • 现象一: 堆/Perm 区不断增长, 没有下降趋势(回收速度赶不上增长速度), 最后不断触发FullGC, 甚至crash(如下**两张图是同一个应用的GC和Perm数据, GC触发原因确认是Perm不足**)
. 一般是现象二的晚期表现.

e1ddc292bde8c43925699d6ba1ab2c25755e75f9d359f01b651f3519aa0caedf

  • 现象二:每次FullGC后, 堆/Perm 区在慢慢的增长, 最后不断触发FullGC, 甚至crash(如下图: 示意图)

b6edaed289de823016c3a6684c06ed5f

java内存泄露场景—PermGen space

 

  • 原因: 说明Perm不足. Perm存放class,method相关对象,以及运行时常量对象. 如果一个应用加载了大量的class, 那么Perm区存储的信息一般会比较大.另外大量的intern String对象也会导致Perm区不断增长。 此区域大小由-XX:MaxPermSize参数进行设置. (jdk8相关参数已经改变, 这里不讨论)
  • 案例: Groovy动态编译class, xstream String.intern
  • 本质原因: ClassLoader.defineClass和java.lang.String.intern在大量不适宜的场景被调用.
  • 解决方案 

方案1:使用btrace相关工具输出调用ClassLoader.defineClass栈信息, 从栈信息来追溯问题. (代码如下图).

  • _2016_09_29_12_59_38
  • 方案2:用JProfiler来trace String.intern方法栈

  • _2016_09_29_1_46_59
  • 方案3: 增加-XX:+TraceClassLoading和-XX:+TraceClassUnloading, 看看哪些class加载了,哪些class卸载了. 如果一些特殊的class一直被加载而没有被卸载说明也是有问题的。(如下图)


  • _2016_09_29_1_06_46
  • 方案4:执行jmap -permgen(jstat -gcutil 可以查看内存增长速度和区域)命令看看Perm区中的内容, 初步确定是否存在问题 (如下图)

  • _2016_09_29_3_07_42_2016_09_29_1_45_46
  • 转发,如果涉及到版权,请详细我, 本人会主动删除。谢谢!

    D0

    说说并发编程 volatile

    Java volatile关键字是用来保证变量的线程可见性。到底什么是线程的可见性呢?准确地讲,每次读取volatile变量要从主内从中读取,而不是从CPU cache中读取;写一个volatile变量,要直接写到主内存,而不仅仅是CPU cache。

    Java 5 引入了volatile,就保证了读写都要从主内存中取值。下边详细介绍一下:

    线程可见性
    在多线程应用程序中, 当线程操作非volatile变量,每个线程需要复制变量从主内存到 CPU缓存中,出于性能方面的设计。如果你的计算机包含多个 CPU,每个线程可能会在不同的 CPU 上运行。这意味着,每个线程可能会将变量复制到不同的 Cpu 的 CPU 缓存。如下所示 ︰
    字节码

    非volatile变量不保证从主内存中立即读取数据或将数据从CPU 缓存中直接写入到主内存。如果多线程操作一个共享变量的话,就会出现如下的问题:

    [java]
    public class SharedObject {

    public volatile int counter = 0;

    }

    [/java]

    想象一下,如果只有线程1更新counter变量,但是线程1和线程2却可能拿到的不是最新的值。如果没用申明volatile,JVM并不保证counter变量的值马上写回到主内存中,那就意味着CPU cache中的值并不是最新的值。如下图所示:
    字节码

    这就是线程可见性的问题。线程2并没有实时看到最新的值,因为JVM会先从CPU cache中读取值。

    如果申明为volatile,则线程读取值,必须从主内存中读取,写值,必须写到主内存中,这就保证了所有的值对于线程是可见的。但是这不能说明它是线程安全的。这是两个不同的概念。

    例子
    [java]
    package com.learn.core.ch02;

    public class VolatileTest {

    private int count = 0;

    public static void main(String[] args) throws InterruptedException {
    while (true) {
    VolatileTest test = new VolatileTest();
    test.setCount(0);
    Thread[] ts = new Thread[2];
    for (int i = 0; i < 2; i++) {
    ts[i] = new Thread(new MyRun(test, i));
    ts[i].start();
    }

    for (Thread t : ts) {
    t.join();
    }
    Thread.sleep(2);

    }
    }

    public static class MyRun implements Runnable {

    private VolatileTest test;
    private int index;

    public MyRun(VolatileTest test2, int index) {
    this.test = test2;
    this.index = index;
    }

    @Override
    public void run() {
    if (index == 0) {
    test.increment();
    } else {
    try {
    Thread.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    if (test.getCount() == 0) {
    System.out.println(Thread.currentThread().getName() + "—–" + test.getCount());
    }
    }
    }

    public int getCount() {
    return count;
    }

    public void increment() {
    count++;
    }

    public void setCount(int count) {
    this.count = count;
    }

    }

    [/java]
    Thread-10181—–0
    Thread-10957—–0
    Thread-10959—–0
    Thread-11643—–0
    Thread-12177—–0
    Thread-12269—–0
    Thread-12345—–0
    Thread-12929—–0
    Thread-13221—–0
    加上volatile关键字,就可以保证永远拿到最新的值,前提是写的线程先执行。

    说说并发编程 ThreadLocal

    类ThreadLocal 提供线程局部变量。这些局部变量不同于正常的变量,每个线程都有自己独立初始化的副本,可以通过threadLocal的set/get方法去改变它的。一般ThreadLocal 常用在类变量上。比如DateFormat类不是线程安全的,没有必要所有的方法都加上同步,这是利用ThreadLocal就可以达到效果。

    创建ThreadLocal
    [java]
    private ThreadLocal myThreadLocal = new ThreadLocal();
    [/java]

    访问ThreadLocal
    [java]
    //设置变量
    myThreadLocal.set("A thread local value");
    //读取local变量
    String threadLocalValue = (String) myThreadLocal.get();
    [/java]

    初始化ThreadLocal 变量

    [java]
    private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
    @Override protected String initialValue() {
    return "This is the initial value";
    }
    };
    [/java]

    例子
    [java]
    package com.learn.core.ch02;

    import java.util.concurrent.atomic.AtomicInteger;

    public class ThreadId {

    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread’s ID
    private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {

    @Override
    protected Integer initialValue() {
    return nextId.getAndIncrement();
    }
    };

    // Returns the current thread’s unique ID, assigning it if necessary
    public static int get() {
    return threadId.get();
    }

    public static void main(String[] args) {
    for (int i = 0; i < 2; i++) {
    new Thread(new MyRunnable()).start();
    }
    }

    public static class MyRunnable implements Runnable {

    @Override
    public void run() {
    for (int i = 0; i < 10; i++) {
    System.out.println(threadId.get());
    }

    }
    }

    }
    [/java]

    总结:以下2中情况,可以考虑使用

  • 当每个线程仅仅想拥有自己的一些变量,不想所有的线程共享
  • 一些非线程安全的类变量或者工具类,为了保证线程安全,可以通过ThreadLocal作为线程局部变量达到线程安全的效果
  • 说说并发编程 synchronized

    并发编程是码农一个绕不开的话题,更是区分高手和普通的一块雷区。面试中,更是屡试不爽。那今天就说说并发编程中synchronized,这是java自带的关键字,是原生保证线程安全的机制。

    本文的目录是:

  • 什么是线程安全
  • 怎么解决线程安全
  • synchronized用法和原理介绍
  • 1. 什么是线程安全

    想理解线程安全,就必须知道什么是线程。线程是相对进程而言的。下边是线程和进程的简单介绍,如果需要更深的了解,可以查询操作系统的原理。

    进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
    线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
    一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
    相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
    进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。相对并发编程,线程的并发控制要比进程的并发控制要容易,进程之间的并发安全需要借助第三方媒介,比如文件,操作系统,而线程则可以通过指令,内部变量实现。

    说完线程,那就说说线程安全。一般对共享的资源,当有多个线程同事访问的时候,就会有线程安全,因为每个线程是个独立的执行体,可以理解为一个人,资源是有自己的数据和行为的,当多个线程(人)去执行同一资源的行为,就会导致资源内在数据的变化,谁先执行,谁后执行,就会引发线程安全的问题。比如你的银行账号,当同时几个人取钱,如何保证账户的钱是对的,对银行来说是很重要的问题。

    2.怎么解决线程安全

    线程安全的原因就是对共享资源或者叫临界区的访问,所以保证安全,就是确保同一时刻,只有一个线程可以改变该共享资源的内在行为。一般java里有2中策略: (1) Lock (2) Synchronized

    3.synchronized用法和原理介绍

    java里支持3中方法定义同步块:

  • 同步类方法
  • [java]
    package com.learn.core.ch02;

    public class InsertData {

    public static synchronized void method1() {
    System.out.println("Method 1 start");
    try {
    System.out.println("Method 1 execute");
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Method 1 end");
    }

    public static synchronized void method2() {
    System.out.println("Method 2 start");
    try {
    System.out.println("Method 2 execute");
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
    final InsertData test = new InsertData();
    final InsertData test2 = new InsertData();

    new Thread(new Runnable() {

    @Override
    public void run() {
    test.method1();
    }
    }).start();

    new Thread(new Runnable() {

    @Override
    public void run() {
    test2.method2();
    }
    }).start();
    }
    }

    [/java]

    类方法同步
    Method 1 start
    Method 1 execute
    Method 1 end
    Method 2 start
    Method 2 execute
    Method 2 end
    实例方法同步(去掉上边的static)
    Method 1 start
    Method 1 execute
    Method 2 start
    Method 2 execute
    Method 2 end
    Method 1 end

    字节码
    从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。只不过,实例方法同步和类方法同步,monitor一个是监测对象,一个是监测类

  • 同步实例方法
  • [java]
    class class_name {
    synchronized type method_name() {
    statement block
    }
    }
    [/java]

  • synchronized代码块
  • [java]
    class class_name {
    type method_name() {
    synchronized (object) {
    statement block
    }
    }
    }
    [/java]

    线程执行到这个方法时,对先尝试得到对象(object)的锁,如果拿不到锁,就会等待,直到锁释放,拿到锁。 对象可以是当前对象this,也可以某个变量。比如下面2个例子原理是一样的:
    [java]
    public class InsertData {

    private ArrayList<Integer> arrayList = new ArrayList<Integer>();

    public void insert(Thread thread) {
    synchronized (this) {
    for (int i = 0; i < 100; i++) {
    System.out.println(thread.getName() + "insert data #" + i);
    arrayList.add(i);
    }
    }
    }
    }

    public class InsertData {

    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Object object = new Object();

    public void insert(Thread thread) {
    synchronized (object) {
    for (int i = 0; i < 100; i++) {
    System.out.println(thread.getName() + "insert data #" + i);
    arrayList.add(i);
    }
    }
    }
    }
    [/java]

    这2个例子为啥效果一样呢?大家可以仔细想想。那同步块,是如何保证安全的?其实它是通过指令来完成的,看下边的字节码截图:
    字节码

    关于这两条指令的作用,我们直接参考JVM规范中描述:

    monitorenter :

    Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
    • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
    • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
    • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

    这段话的大概意思为:

    每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
    1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
    2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
    3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

    monitorexit:

    The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
    The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

    执行monitorexit的线程必须是objectref所对应的monitor的所有者。
    指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

      通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
    更详细的资料,请参考JVM指令介绍:monitorenter/monitorexit

    参考:
    http://www.importnew.com/18523.html

    java final 关键字

    说到final关键字,搞过java的都不陌生。基本上每个码农对final的基本用法都清楚,但是经常看到一些奇怪的用法和理解,那今天就好好梳理下。

    1.final关键字的基本用法
    在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)

    1.1修饰类
    当final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。默认final类中的方法都是final,成员变量可以设为final也可以不是。

    [java]
    package com.learn.core.ch02;

    public final class Country {

    private String name;

    public void display() {

    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public Country(String name) {
    this.name = name;
    }
    }

    public class CountrySub extends Country{
    //The type CountrySub cannot subclass the final class Country
    }

    [/java]

    1.2 修饰方法
    下面这段话摘自《Java编程思想》第四版第143页:

    “使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“

    因此,如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。
    注:类的private方法会隐式地被指定为final方法。

    1.3 修饰变量(成员变量或者局部变量)
    对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。注意如果是引用类型的变量,只是说引用不能再变,但是引用所代表的对象还是可以改变该对象内在的行为。
    成员变量:必须初始化或者在构造器中初始化

    [java]
    package com.learn.core.ch02;

    public final class Country {

    private String name;

    public Country(String name) {
    this.name = name;
    }

    public static void main(String[] args) {
    final Country country = new Country("china");
    country.display();

    // update to us
    country.setName("US");
    country.display();

    // output
    // china
    // US

    // country = new Country("GE");
    // error The final local variable country cannot be assigned. It must be blank and not using a compound
    // assignment
    }

    public void display() {
    System.out.println(name);
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    }

    [/java]

    2. 深究final关键字
    2.1 类的final变量和普通变量有什么区别?
    当用final作用于类的成员变量时,成员变量(注意是类的成员变量,局部变量只需要保证在使用之前被初始化赋值即可)必须在定义时或者构造器中进行初始化赋值,而且final变量一旦被初始化赋值之后,就不能再被赋值了
    2.2 final的引用变量指向的对象内容可变吗?
    只要不改变引用,引用所指向的对象是可以改变内在行为或者外在行为的,就是可以调各种方法。可以参考上边的例子。
    2.3 匿名内部类中使用的外部局部变量为什么只能是final变量?
    关于这个,可以参考另外一篇文章。
    2.4 static 和final一起使用,可以定义不可变的常量
    2.5 所有的局部变量定义成final,除了匿名内部类可以使用的有点,还有其他优势吗?

    [java]
    public void display() {
    System.out.println(name);
    String title = "Hello";
    Date date = new Date();
    System.out.println(title + "," + date);
    }
    [/java]
    001.png
    局部变量

    [java]
    public void display() {
    System.out.println(name);
    final String title = "Hello";
    final Date date = new Date();
    System.out.println(title + "," + date);
    }
    [/java]

    final局部变量

    通过查看字节码,发现final修饰的局部变量在编译阶段就会被替换掉,有点类似C语言的宏定义,如果没有final修饰的局部变量,会在方法运行栈中多用2个指令(aload_)去存储和加载局部变量所指向的值

    12: astore_1
    13: getstatic #31 // Field java/lang/System.out:Ljava/io/PrintStream;
    16: aload_1

    所以final局部变量确实可以减少指令,提升程序的性能,但是我认为如果所有的局部变量都用final修饰,会影响代码的整洁。至于选择性能还是代码的可阅读性,要看自己的习惯了!

    欢迎大家拍砖纠正!

    参考资料
    深入理解Java中的final关键字
    浅析Java中的final关键字