从类状态看Java多线程安全并发

图片来自pixabay.com的razvansubscribe-316951会员

对于Java开发人员来说,i++的并发不安全是人所共知,但是它真的有那么不安全么?

在开发Java代码时,如何能够避免多线程并发出现的安全问题?这是所有Java程序员都会面临的问题。本文讲述了在开发Java代码时安全并发设计所需要考虑的点,文中以一张图展开,围绕着Java类状态,讨论各种情况下的并发安全问题。当理解了Java类的各种变量状态在并发情况下的表现,在写Java多线程代码时就可以做到心中有数,游刃有余,写出更加安全、健壮、灵活的多线程并发代码。

1. 多线程并发简介

在现代操作系统中,CPU的调度都是以线程为基本单位,各个线程各自独立获取到CPU时间片并执行代码指令,这就是多线程并发。于此同时,同一进程中的所有线程将共享当前进程的内存地址空间,这些线程可以访问当前内存地址空间上的同一个变量。若一个线程在使用某个变量时,另一个线程对这个变量进行修改,将造成不可预测的结果,这也是多线程并发问题。

一个简单的例子是,当一个线程循环读取一个数组时,另外一个线程对这个数组内对象进行删除,则前面一个线程可能读取失败或读取的是脏数据。

在多线程并发中,若一段代码的执行不能按预期正确地进行,或者执行的最终结果不可预测,则我们说这段代码并发不安全。换句话说,若线程之间能够按照预期执行代码,操作数据并获取到期望的结果,则实现了安全的并发。

2. 从类状态看Java安全并发

类状态是指类中所声明的变量,无论是公有变量、私有变量,亦或static和final修饰的变量,都是不同形式的类状态。按照Java语法,类变量有如下各种形式,

  • 公有变量(public)、私有变量(private)、保护变量(protect)
  • 静态变量(static)
  • 不可变变量(final)
  • 外部变量、内部变量、局部变量

这些类变量在运行时刻,映射到JVM内存中各种对象。Java安全并发设计,其核心在于如何处理这些变量在并发中的表现,掌握它们的特性是Java安全并发设计的关键。

下图从类状态出发,简要的说明了Java类变量的各种状态形式,及其相关的并发安全性,

Java安全并发设计

其中,

  • 绿色方块说明多线程并发安全。
  • 桔红色方块说明多线程并发不安全,会出现问题。
  • 图中的Java类是指完全依据面向对象设计,即:类成员变量被声明为私有,类方法只对类内部成员变量进行操作。
  • 有状态是指Java类中有成员变量声明,无论是公有、私有还是保护变量,亦或static和final;无状态则指类中无任何成员变量声明。
  • 私有状态是指类成员变量通过ThreadLocal进行了线程隔离,实现了按线程进行变量的分配;而共享状态则指类变量可以被多线程访问。
  • 不可变状态是指类成员变量被声明为final,是一种常量状态。
  • 静态状态是指类成员变量被声明为static。
  • 阻塞是指线程在执行代码前,必须获取锁,这个锁只有一个,通过锁实现了代码的多线程串行执行。

需要注意的是,该图是以Java语言为例来说明如何设计并发安全的对象类,但实践中,图中所涉及的状态、私有状态、不可变状态、非阻塞和阻塞访问,这些概念也应该适用于更多面对对象的编程语言。

下面将对上图中各个类状态进行一一讲解,介绍各个状态下并发设计的要点。

3. Java安全并发分解

3.1 无状态类

一个无状态类是指其没有任何声明的成员变量,例如,

public class StatelessClass {

    public void increment() {
        int i = 0;
        String msg = String.valueOf(i++);
        log.info(msg);
    }

}

无状态类是线程安全的。上述类中的increment()方法中,有两个本地变量i和msg,这两个本地变量都在方法栈空间上分配,由于栈内存空间是按线程各自独立的,相互隔离,因此栈空间上的变量是线程安全的。

由此还可以知道,在方法调用中分配的变量和对象,若在栈退出后变量或对象引用被JVM释放(不会被外部再访问到),则这个变量和对象也是线程安全的。关于本地变量和JVM栈空间的更多介绍,可以参考这篇文章

3.2 有状态类

和无状态相反,有状态类是指类中有声明的成员变量,例如

public class StatefulClass {

    private int i=0;

    public void increment() {
        i++;
    }

}

上面的类声明了一个int i的类变量,并初始化为0。大多数情况下Java类都是属于有状态类。

有状态是导致线程不安全的必要条件,但它不是充分条件,请继续看下文。

3.3 私有状态类

若Java类的状态通过ThreadLocal等方法,使得状态被隔离在各个线程中,相互不干扰,例如,

public class PrivateStateClass {

    private ThreadLocal<Integer> i = new ThreadLocal<>();

    public void set(int i) {
        i.set(i);
    }

    public void increment() {
        Integer value = i.get();
        i.set(value + 1);
    }

}

上面的类声明了一个ThreadLocal i的变量,这个类状态按各个线程进行了隔离,为一种私有状态,在执行increment()方法时可以被多线程安全访问。

3.4 共享状态类

正常的Java成员变量是线程共享的,即多个线程通过Java类提供的类方法访问类对象时,类对象中的成员变量可以被共享访问到,这是大多数情况下的应用场景。

共享状态在多线程并发时,不一定就是不安全,其又可以分为常量状态和可变状态两种情况来讨论,请见下文。

3.5 不可变状态类(常量状态)

下面的Java类中,有一个Integer PI变量被声明为final,这说明这个变量是一个常量对象,初始化之后不再改变。

public class FinalStateClass {

    private final Integer PI = 3.14;

    public double calculate(double radius) {
        return PI*radius*radius;
    }

}

多线程访问上述的calculate()方法是线程安全的。

final声明使得变量变为常量状态,多线程在访问时不能更改状态,在一定程度上实现了只读,从而是线程安全的。

3.6 可变状态类

对于可变的共享状态,当多线程访问时,必然出现协同操作和同步问题,若代码设计不当,则很容易出现线程不安全问题。

对于可变共享状态的访问,是多线程并发设计时的考虑重点。为了实现线程安全,一般通过下面两种方法,

  • 非阻塞设计(多线程并行执行,通过算法实现线程安全)
  • 阻塞设计(加锁,使得多线程实现串行执行)

下面是这两种方法的简单比较,

非阻塞设计 阻塞设计
多线程执行 并行执行 串行执行
安全实现方法 通过算法设计 通过锁
吞吐性能
优点 无死锁,线程不会被阻塞挂起 通过锁可以实现可控的线程调度
缺点 算法实现复杂,在高度竞争情况下,吞吐性能会低于锁 线程的挂起和上下文切换、死锁

更详细的讨论见下文。

3.7 非阻塞设计

下面的Java类通过原子变量AtomicInteger实现非阻塞的自增算法。

public class AtomicStateClass {

    private AtomicInteger i = new AtomicInteger(0);

    public void increment() {
        i.incrementAndGet();
    }

}

可以看到increment()方法没有添加任何锁,但是它可以实现多线程的安全自增操作。AtomicInteger其原理是通过CAS算法,即compareAndSet()方法,先查看变量是否变化,若没有变化则设置值,若有变化,则重新尝试,在绝大数情况下,值的设置在第一次尝试就成功。

更多非阻塞算法设计,比如非阻塞的栈、非阻塞的链表插入操作,见这里

3.8 阻塞设计

阻塞是指通过锁来控制线程对类状态的访问,使得当前状态只能由一个线程访问,其它访问线程则挂起等待,一直等到锁被释放后,所有的等待线程竞争锁,获得下一次访问权。

锁的设计,使得线程各自之间实现同步,串行执行代码指令,避免了竞争状态。但是于此同时,它也带来了死锁的困扰。若两个线程之间相互持有对方需要的资源或锁,则进入死锁状态。

JVM在解决死锁上没有提供较好的办法机制,更多的是提供监控工具来查看。对于死锁问题,最终解决方案是依赖开发者实现的代码,增加更多的资源,减少锁的碰撞,实现锁的有序持有和不定时释放,都是避免死锁的有效方案。

3.8.1 资源死锁(resource deadlock)

资源死锁是一种广泛的死锁定义,简单例子是,一个打印任务需要获得打印机和文件对象,若一个线程获得了打印机,而另外一个线程获得了文件对象,相互都不释放获得的资源,则出现资源死锁情况。

增加更多的资源,是解决此类死锁的有效方案。

3.8.2 锁顺序死锁(lock-ordering deadlock)

下面是一个锁顺序死锁的演示代码,

public class LockOrderingDeadLock {

    public void transferMoney(Account from, Account to, Integer amount) {
        synchronized (from) {
            synchronized (to) {
                from.debit(amount);
                to.credit(amount);
            }
        }
    }

}

若同时启动两个线程,分别执行下面两个操作,

  • 线程1:transferMoney(accountA, accountB, 100)
  • 线程2:transferMoney(accountB, accountA, 100)

则很有可能出现死锁状态,因为线程1在握有accountA对象锁的同时,线程2也握有accountB的锁。下面是对transferMoney方法测试过程中,通过JConsole观察到的死锁情况,

死锁2
图1:pool-1-thread-5握有account@120f74e3的锁,等待account@3e9369b9的锁

死锁1
图2:pool-1-thread-8握有account@3e9369b9的锁,等待account@120f74e3的锁

解决办法之一,是实现锁的按序持有,即对于任何两个对象锁A和B,先进行排序(排序算法必须是稳定有序),无论是哪个线程,都必须按照锁的排序,依次获取,从而避免相互持有对方需要的锁。

3.8.3 状态公开

状态公开是指类成员变量被公开,在一定程度上破坏了面向对象设计的数据封装性。对类方法再好的阻塞设计,一旦状态被公开,其并发安全性都会功亏一篑。

见下面的例子,类中定义了一个personList的对象,方法insert()和iterate()通过synchronized进行了阻塞加锁,其只能运行一个线程进入类方法执行操作。

public class PublicStateClass {

    public ArrayList<String> personList = new ArrayList<>();

    public synchronized void insert(String person) {
        personList.add(person);
    }

    public synchronized void iterate() {
        Integer size = personList.size();
        for (int i = 0; i < size; i++) {
            System.out.println(personList.get(i));
        }
    }

}

但多线程访问insert()和iterate()方法时,并不一定线程安全,主要原因是personList被声明了公开对象,使得类之外的线程可以轻易地访问到personList变量,从而导致personList的状态不一致,在iterate整个person列表时,可能列表中的对象已被删除。

这是类状态公开导致的线程安全问题,究其原因,还要归结于没有做好类的面对对象设计,对外部没有隐藏好数据。

下面的getList方法返回也会导致同样的问题,

public class PublicStateClass {

    private ArrayList<String> personList = new ArrayList<>();

    public List getList() {
        return personList;
    }

}

对于这样的问题,推荐的做法是,成员变量声明为私有,在执行读操作时,对外克隆一份数据副本,从而保证类内部数据对象不被泄露,

public class PublicStateClass {

    private ArrayList<String> personList = new ArrayList<>();

    public List getList() {
        return (List) personList.clone();
    }

}

4. 类的静态状态

类的静态状态是指类中被static声明的成员变量,这个状态会在类初次加载时初始化,被所有的类对象所共享。Java程序员对这个static关键字应该不会陌生,其使用的场景还是非常广泛,比如一些常量数据,由于没有必要在每个Java对象中存储一份,为了节省内存空间,很多时候声明为static变量。

但static变量并发不安全,从面向对象设计来说,一旦变量声明为静态,则作用空间扩大到整个类域,若被声明为公共变量,则成为全局性的变量,static的变量声明大大破坏了类的状态封装。

为了使静态变量变得多线程并发安全,final声明是它的“咖啡伴侣”。在阿里巴巴的编码规范中,其中一条是,若是static成员变量,必须考虑是否为final。

5. 类外部状态和多线程安全并发

上文在讲并发设计时,都是针对类内部状态,即类内部成员变量被声明为私有,类方法只对类内部变量进行操作,这是一种简化的应用场景,针对的是依据完全面向对象设计的Java类。一种更常见的情况是,类方法需要对外部传入的对象进行操作。这个时候,类的并发设计则和外部状态息息相关。

例如,

public class StatelessClass {

    public void iterate(List<Person> personList) {
        Integer size = personList.size();
        for (int i = 0; i < size; i++) {
            System.out.println(personList.get(i));
        }
    }

}

上面的类是一个无状态类,里面没有任何声明的变量。但是iterate方法接受一个personList的列表对象,由外部传入,personList是一个外部状态。

外部状态类似上文中内部状态公开,无论在类方法上做如何的参数定义(使用ThreadLocal/final进行声明定义),做如何并发安全措施(加锁,使用非阻塞设计),类方法其对状态的操作都是不安全的。外部状态的安全性取决于外部的并发设计。

一个简单的处理方法,在调用类方法的地方,传入一个外部状态的副本,隔离内外部数据的关联性。

6. 小结

类状态的并发,本质上是内存共享数据对象的多线程访问问题。只有对代码中各个Java对象变量的状态特性掌握透彻,写起并发代码时将事倍功半。

下面的类中,整个hasPosition()方法被synchronized修饰,

public class UserLocator {

    private final Map<String, String> userLocations = new HashMap<>();

    public synchronized boolean hasPositioned(String name, String position) {
        String key = String.format("%s.location", name);
        String location = userLocations.get(key);
        return location != null && position.equals(location);
    }

}

但仔细查看可以知道外部变量name和position、内部变量key和location都是并发安全,只有userLocations这个变量存在并发风险,需要加锁保护。因此,将上面的方法进行如下调整,将减少锁的粒度,有效提高并发效率。

public class UserLocator {

    private final Map<String, String> userLocations = new HashMap<>();

    public boolean hasPositioned(String name, String position) {
        String key = String.format("%s.location", name);
        String location;
        synchronized (this) {
            location = userLocations.get(key);
        }
        return location != null && position.equals(location);
    }
}

由此可见,了解类中各个变量特性对写好并发安全代码的重要性。在这个基础上,优化锁的作用范围,减少锁的粒度,实现锁分段,都可以做到信手拈来,游刃有余。

关于类状态,说了这么多,最后给一个全文性总结:面向对象进行类设计,隐藏好数据,控制好类的状态,从严控制变量的访问范围,能private尽量private,能final尽量final,这些都将有助于提高代码的并发健壮性。

7. 演示代码

所有的演示代码在如下的代码仓库中,

8. 参考资料

  1. 《Java并发编程实战》 [美] Brian Goetz 等 著,童云兰 等 译,ISBN:9787111370048。
  2. IBM DeveloperWorks:非阻塞算法简介

JVM内存数据模型

图片来自pixabay.com的Gipfelsturm69-2191891会员

本文将对JVM内存数据模型进行介绍,并给出一个简单的Java应用程序,描述其内存分配过程。在编写代码中,只有对类、各个变量和Java对象做到心中有数,才能“下笔”(敲代码)如有神。

1. JVM内存数据模型

如下图所示,

JVM内存数据模型

根据JVM规范,在运行时刻JVM内存数据分为如下6种,

  1. PC Register 程序计数器: 一个JVM中支持多个线程的执行,每个线程拥有各自独立的程序计数器,程序计数器指向线程执行的当前方法地址。
  2. JVM Stacks 栈区:每个线程拥有各自独立的JVM栈,一个栈存储着frames列表,每个frame对应着一个方法调用,其保存着方法调用所使用的本地变量和Java对象的引用,方法返回的值和异常。Frame按照后入先出的原则,执行并返回调用结果。这个数据区会发生如下两种内存溢出错误,
    • StackOverflowError 栈超过允许的调用深度
    • OutofMemoryError 栈超过允许的可用内存大小
  3. Heap 堆区,这个区的数据被所有线程所共享,是类对象创建时分配内存的地方。这个区的内存被JVM管理,实现对象的自动回收,也就是GC。这个数据区发生如下的内存溢出错误,
    • OutofMemoryError创建的对象超过可分配的内存大小
  4. Method Area方法区:这个区的数据被所有线程所共享,里面加载着类的定义,包括常量池,变量和方法数据等。这个数据区发生如下的内存溢出错误,
    • OutofMemoryError加载的类超过可分配的内存大小
  5. Run-Time Contant Pool 常量池,一个类文件中所定义的常量,一般会存储在方法区中。
  6. Native Method Stacks原生方法栈,Java内核代码中含有很多对操作系统原生方法的调用,这里存储着对原生方法调用的信息。其只对Java内核代码有意义,对于Java程序员来说,可以忽略这个区。

2. 一个简单应用程序的JVM内存数据

下面以一个简单的Java程序,描述下JVM的内存分配过程。

public class Demo {

    private static String CONSTANT = "hello,world";

    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.print(CONSTANT);

        int i = 0;
        String s = String.valueOf(i);
        demo.print(s);
    }

    private String print(String s) {
        System.out.println(s);
        return s;
    }

}

上述程序定义了一个Demo的类,里面包含了一个main主程序,和一个print()方法调用。

整个程序在运行过程中,JVM将会执行如下动作,

  1. JVM根据启动参数,初始化各个内存区。
  2. JVM加载Demo类到方法区,加载各个变量和方法定义,加载常量定义,其中字符串常量从堆区分配。
  3. 启动一个main线程,执行main主程序,线程的执行进度记录在程序计数器中。同时,在栈区初始化当前线程的方法调用栈。
  4. 进入main()方法调用,创建frame1,初始化如下变量
    • String args 输入参数,引用指向堆区所创建的args对象
    • Demo demo 引用,指向堆区所创建的demo对象
    • int i = 0 分配一个整型i,初始化值为0
    • String s 引用,指向堆区所创建的s对象
  5. 进入print()方法调用,创建frame2
    • String s 输入参数,引用指向堆区已创建的s对象
    • 返回s,指向堆区的s对象。
  6. print()方法调用结束,栈回到frame1。
  7. main()方法调用结束,方法调用栈清空。
  8. main线程执行结束。

上述在内存的分配可以描述为下图,

一个简单的Java程序内存分配.PNG

3. 堆区和元空间

JVM Heap是最大的内存分配区域,所有的Java对象都从这里获得内存存储空间,这里也是JVM自动内存回收(GC)的地方。

要想了解GC的工作机制,首先需要了解堆区中Java对象按代进行存储的机制。整个堆区分为如下几个区域,

  1. Eden 伊甸园区:这里是创建对象时最先分配内存的地方,名副其实的创世区
  2. Survivor 存活区:在发生Young GC时,会将Eden区中大量不再使用的对象删除,留下来的放入Survivor区。注意的是,Survivor区一般有两个,每次YGC时,将会S0和S1交换着来保存存活下来的对象。也就是说,S0和S1总有一个是处于清空状态。
  3. Tenured年老代:在GC多次过后,有些对象存活时间比较长,将会移入到年老代。

至于对象的存活与否,如何回收,这个将涉及到对象引用计数的概念,以及各个GC算法实现,这里不再扩展。

下图描述了堆区的示意图,

JVM堆区

图中还有一个元空间(MetaSpace),在JDK7之前其是一个永久代(PermGen)的内存空间,里面存放类定义等数据。在JDK8之后,永久代被元空间取代,两者的区别之一在于空间地址,永久代位于JVM Heap Memory中,而元空间移到了native memory中,这里的native memory是相对于JVM里面的heap memory而言,是位于JVM所运行的内存空间。

一个查看Java进程的堆区内存使用情况,命令如下(请使用JDK8的jmap工具),

$ jmap -heap 14120
Attaching to process ID 14120, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.111-b14

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4208984064 (4014.0MB)
   NewSize                  = 88080384 (84.0MB)
   MaxNewSize               = 1402994688 (1338.0MB)
   OldSize                  = 176160768 (168.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 31981568 (30.5MB)
   used     = 21396720 (20.405502319335938MB)
   free     = 10584848 (10.094497680664062MB)
   66.90328629290471% used
From Space:
   capacity = 1048576 (1.0MB)
   used     = 524288 (0.5MB)
   free     = 524288 (0.5MB)
   50.0% used
To Space:
   capacity = 1048576 (1.0MB)
   used     = 0 (0.0MB)
   free     = 1048576 (1.0MB)
   0.0% used
PS Old Generation
   capacity = 176160768 (168.0MB)
   used     = 68010784 (64.86013793945312MB)
   free     = 108149984 (103.13986206054688MB)
   38.60722496396019% used

6179 interned Strings occupying 524264 bytes.

4. Java主要启动参数

在了解了JVM内存数据模型之后,下面就可以看看Java 的各种启动参数配置,来了解如何配置JVM的内存空间。

可以通过java -X命令获取java的启动参数列表,或者查看文档。

参数 描述 默认值
-server 服务器模式
-Xms 堆初始化容量
-Xmx 堆最大可分配容量 建议根据可用物理内存设置
-Xmn 年轻代堆初始化容量(且为最大容量) 建议不配置,根据NewRatio动态调整
-Xss 栈大小 320KB-1MB
-XX:MetaspaceSize 元空间初始化容量
-XX:MaxMetaspaceSize 元空间最大可分配容量
-XX:NewSize 同-Xmn
-XX:NewRatio 年老代和年轻代的容量比例 2
-XX:SurvivorRatio Eden和单个Survivor的容量比例 8
-XX:+UseAdaptiveSizePolicy 允许JVM动态调整年老代和年轻代的容量比例 enabled
-XX:+PrintGC 每次GC时输出相关信息 disabled
-XX:+PrintGCDateStamps GC日志中输出日期时间
-Xloggc:./gc.log GC日志文件位置
-XX:+HeapDumpOnOutOfMemoryError 在OOM时输出堆区内存情况 disabled
-XX:HeapDumpPath=path 输出堆区内存到指定文件
-XX:+UseSerialGC 串行GC disabled
-XX:+UseParallelGC 并行GC JDK8中服务器模式下默认GC选项
-XX:+UseG1GC G1 GC JDK9中服务器模式下默认GC选项

5. 参考资料

  1. 官方文档:The Java Virtual Machine Specification
  2. Java Heap Space vs Stack – Memory Allocation in Java
  3. IBM Developer Works - Understanding how the JVM uses native memory on Windows and Linux
  4. 官方文档:JDK tool - java
  5. JDK9:JEP 248: Make G1 the Default Garbage Collector

JDK Tools简介

图片来自pixabay.com的StockSnap-894430会员

1. 简介

Oracle JDK在发布的SDK包中,除了核心类库、编译、运行和调试工具,还带了大量的监视查看工具。这些监视工具可以辅助开发者查看Java程序的运行状态,包括环境配置、内存、CPU、线程、堆栈、类加载等各种信息。本文将对这些监视工具进行介绍。

本文的工具介绍将主要参考JDK 8的官方文档,并以jdk 8为运行环境演示命令输出。

2. 官方文档

  1. JDK 10 tools
  2. JDK 9 tools
  3. JDK 8 tools
  4. JDK 5 tools

2. 各个工具简介

2.1 基本工具

  1. java 启动java应用程序命令
  2. javac 编译工具,将java源代码编译为java bytecode文件
  3. jar 打包归档工具,也可以对包进行解压
  4. javadoc 生成java api说明和使用文档工具

    基本工具的使用方法可参考这篇文章

2.1 常用的监控分析工具

  1. jps 查看java进程状态工具
  2. jstat 内存监视工具
  3. jmap 内存查看工具,获取内存快照
  4. jinfo 获取java进程的环境配置信息
  5. jstack 进程中各个线程的调用栈查看工具
  6. jconsole 一个java进程的监视和管理控制台,可以查看进程的CPU、内存、线程运行情况,还可以辅助检测死锁,查看类加载详细情况
  7. jvisualvm.exe 一个可视化的java进程管理工具,可以查看进程的CPU、内存、线程运行情况。

    上述监控分析工具的使用方法见下文。

3. 工具使用方法

3.1 jps

  • 简单的命令,输出进程id:jps
  • 输出进程的id,main程序全package路径名,启动参数:jps -mlvV
  • 还可以配合jstat server/RMI registry,实现查看远程服务器上的java进程列表:jps -l remote.domain

一个命令输出样例如下,

$ jps -mlvV
16552 jdk.jcmd/sun.tools.jps.Jps -mlvV -Dapplication.home=G:\local\java\jdk-9.0.1 -Xms8m -Djdk.module.main=jdk.jcmd

3.2 jstat

检查进程15644的内存使用(garbage collected heap)情况

$ jstat -gc 17152
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
10752.0 10752.0  0.0    0.0   64512.0   2580.5   172032.0     0.0     4480.0 770.2  384.0   75.9       0    0.000   0      0.000    0.000

每隔5秒检查进程15644的内存使用情况,输出中添加timestamp,每隔5行添加header,

$ jstat -gcutil -t -h5 15644 500
Timestamp         S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
         2926.3   0.00  89.58  32.70   1.42  94.99  90.00      7    0.025     1    0.012    0.037
         2926.8   0.00  89.58  33.13   1.42  94.99  90.00      7    0.025     1    0.012    0.037
         2927.3   0.00  89.58  33.13   1.42  94.99  90.00      7    0.025     1    0.012    0.037
         2927.8   0.00  89.58  33.13   1.42  94.99  90.00      7    0.025     1    0.012    0.037
         2928.3   0.00  89.58  33.13   1.42  94.99  90.00      7    0.025     1    0.012    0.037
Timestamp         S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
         2928.8   0.00  89.58  33.13   1.42  94.99  90.00      7    0.025     1    0.012    0.037
         2929.3   0.00  89.58  33.13   1.42  94.99  90.00      7    0.025     1    0.012    0.037
         2929.8   0.00  89.58  33.13   1.42  94.99  90.00      7    0.025     1    0.012    0.037
         2930.3   0.00  89.58  33.93   1.42  94.99  90.00      7    0.025     1    0.012    0.037
         2930.8   0.00  89.58  34.16   1.42  94.99  90.00      7    0.025     1    0.012    0.037

在使用-gc选项来检查内存使用情况时,各个缩写字母的含义为,
- S0C: Current survivor space 0 capacity (KB). 年轻代第一个survivor空间容量
- S1C: Current survivor space 1 capacity (KB). 年轻代第二个survivor空间容量
- S0U: Survivor space 0 utilization (KB). 年轻代第一个survivor空间使用量
- S1U: Survivor space 1 utilization (KB). 年轻代第二个survivor空间使用量
- EC: Current eden space capacity (KB). 年轻代Eden空间容量
- EU: Eden space utilization (KB). 年轻代Eden空间使用量
- OC: Current old space capacity (KB). 年老代空间容量
- OU: Old space utilization (KB). 年老代空间使用量
- MC: Metaspace capacity (KB). 元空间容量
- MU: Metaspace utilization (KB). 元空间使用量
- CCSC: Compressed class space capacity (KB).
- CCSU: Compressed class space used (KB).
- YGC: Number of young generation garbage collection (GC) events. 年轻代空间GC次数
- YGCT: Young generation garbage collection time. 年轻代空间GC时间开销
- FGC: Number of full GC events. 整个堆区FULL GC次数
- FGCT: Full garbage collection time. 整个堆区FULL GC时间开销
- GCT: Total garbage collection time. 总GC时间开销

其它内存检查时所看到的缩写字母类似,更详细可以查看官方文档。

JVM的内存空间模型详细介绍见这里

3.3 jmap

简单命令,输出进程14120的内存使用情况,

$ jmap -heap 14120
Attaching to process ID 14120, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.111-b14

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4208984064 (4014.0MB)
   NewSize                  = 88080384 (84.0MB)
   MaxNewSize               = 1402994688 (1338.0MB)
   OldSize                  = 176160768 (168.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 31981568 (30.5MB)
   used     = 21396720 (20.405502319335938MB)
   free     = 10584848 (10.094497680664062MB)
   66.90328629290471% used
From Space:
   capacity = 1048576 (1.0MB)
   used     = 524288 (0.5MB)
   free     = 524288 (0.5MB)
   50.0% used
To Space:
   capacity = 1048576 (1.0MB)
   used     = 0 (0.0MB)
   free     = 1048576 (1.0MB)
   0.0% used
PS Old Generation
   capacity = 176160768 (168.0MB)
   used     = 68010784 (64.86013793945312MB)
   free     = 108149984 (103.13986206054688MB)
   38.60722496396019% used

6179 interned Strings occupying 524264 bytes.

导出进程14120的内存使用情况到文件dump.tmp中,
jmap -dump:format=b,file=dump.tmp 14120
然后就可以使用 eclipse memory analyzer 进行分析,查看是否有内存泄露。

3.4 jinfo

获取进程14120的环境配置信息

$ jinfo 14120
Attaching to process ID 14120, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.111-b14
Java System Properties:

java.runtime.name = Java(TM) SE Runtime Environment
java.vm.version = 25.111-b14
sun.boot.library.path = C:\Program Files\Java\jdk1.8.0_111\jre\bin
...
java.vm.name = Java HotSpot(TM) 64-Bit Server VM
java.runtime.version = 1.8.0_111-b14
java.library.path = C:\Program Files\Java\jdk1.8.0_111\bin;
java.class.version = 52.0
...
os.name = Windows 7
sun.cpu.isalist = amd64

VM Flags:
Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=264241152 -XX:MaxHeapSize=4208984064 -XX:MaxNewSize=1402994688 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=88080384 -XX:OldSize=176160768 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
Command line:

3.5 jstack

获取进程14120的线程调用堆栈信息,通过查看线程堆栈,可以了解是否有死锁情况在发生(可以使用jconsole来辅助查看)。

$ jstack -l 14120
2018-08-22 17:49:07
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.111-b14 mixed mode):

"RMI TCP Connection(35)-172.20.16.89" #47 daemon prio=5 os_prio=0 tid=0x000000001d685000 nid=0x50f0 runnable [0x0000000021aee000]
   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:170)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
        at java.io.BufferedInputStream.read(BufferedInputStream.java:265)
        - locked <0x00000006c9442578> (a java.io.BufferedInputStream)
        at java.io.FilterInputStream.read(FilterInputStream.java:83)
        at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:550)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:826)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:683)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1/1179263329.run(Unknown Source)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:682)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at java.lang.Thread.run(Thread.java:745)
   Locked ownable synchronizers:
        - <0x000000076cd9db08> (a java.util.concurrent.ThreadPoolExecutor$Worker)
...
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001bfa9000 nid=0x468c in Object.wait() [0x000000001d3be000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
        - locked <0x00000006c54ddeb0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
...
"main" #1 prio=5 os_prio=0 tid=0x000000000253f000 nid=0x45d0 waiting on condition [0x000000000295f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at com.pphh.demo.Application.main(Application.java:23)
   Locked ownable synchronizers:
        - None

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x000000000238c800 nid=0x4404 runnable
...

JNI global references: 252