liuhao163.github.io

杂七杂八


  • Home

  • Categories

  • Tags

  • Archives

  • Sitemap

JAVA中的管程

Posted on 2019-03-18 | Edited on 2022-09-21 | In java , 并发

什么是管程

java在1.5之前只提供了synchronized,wait,notfiy,notfiyAll3种实现并发的源语,他们实际上是管程的组成部分。

对应的英文是Monitor,他表示管理共享变量以及操作共享变量,使其支持并发的过程。

java的实现–MESA模式

在管程的发展史上出现过3中管程模型,Hasen、Hoare、MESA。java采用的是MESA模型

在并发变成领域,有俩大核心问题:互斥–一个共享资源同一时刻只能被一个线程访问;同步–即线程之间的通信、协作;

java的管程如何解决互斥问题

如果:

avator

管程将资源以及方法都封装起来,只允许一个线程通过管程哦方法调用该方法。

java的管程如何解决同步问题

利用之前提到的消息-通知机制来让线程之间协作见下面的一个BlockQueue的代码,请忽略业务代码:P

  1. enq的时候while判断队列是否满了,如果满了,notFull.await()阻塞当前线程;
  2. enq如果没满,添加对象,并且用notEmpty.single()通知deque停止阻塞;
  3. deq可以顺利执行出队列的操作;
  4. deq的时候while判断队列是否为空,如果为空,notEmpty.await()阻塞当前线程;
  5. deq如果不为空,poll对象,并且用notFull.single()通知enq停止阻塞;
  6. enq可以顺利执行队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class BlockQueue {
ReentrantLock lock = new ReentrantLock();

Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

private Queue queue = new LinkedList();
private int queSize = 10;

public BlockQueue(int queSize) {
this.queSize = queSize;
}

public void enq(Object o) {
lock.lock();
try {
//如果为慢noFull阻塞线程
while (queue.size() == queSize) {
notFull.await();
}

queue.add(o);
//添加成功通知deq停止阻塞
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public Object deque() {
lock.lock();
Object ret = null;
try {
//如果为空notEmpty阻塞线程
while (queue.size() == 0) {
notEmpty.await();
}
return queue.poll();
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
//出队列成功通知队列未满可以入队列
notFull.signal();
lock.unlock();
}
}
}

notify使用场景

上面用了notify类似的逻辑,之前提到过能用notifyAll尽量用notifyAll。那么什么时候用notify呢。需要同时满足下面3个条件

  1. 所有线程的等待条件一样
  2. 所有线程满足等待条件后的操作一样
  3. 只需要唤醒一个线程

结合上面的场景,都是队列queue;notify之后的操作都是queue入队或者出队;只需要唤醒一个线程入/出队

管程3中模型的区别

  1. Hasen:T1在wait之后,T2notify,notify必须在T2最后一条语句,执行完后T2终止,以此保证系统只有一个线程在运行。
  2. Hoare:T1在wait之后,T2notify,notify后T2堵塞,T1开始运行,T1运行完后唤醒T2,新能略差,因为多了一次T2的唤醒。
  3. MESA:T1在wait之后,T2notify,T1值是获取了可执行的可能,需要竞争获取锁才能执行,JAVA采用此种模型

所以在MESA中wait的标准写法如下,因为被noyify通知之后线程只是可能执行,有可能在执行时候线程又会不满足条件

1
2
3
4
5
while( 条件){
synchronized(objcet){
object.wait();
}
}

JAVA并发编程主要面临的几个问题:安全性,活跃性,性能

Posted on 2019-03-18 | Edited on 2022-09-21 | In java , 并发

概述

java并发编程的问题主要集中在安全性、活跃性、性能3个问题

安全性

主要防止竞态问题(race condition),如下代码,add10k中的set(get()+1) 是不安全的,因为他要依赖get方法的结果在set。虽然set和get加了锁但是+1这步操作不是原子性的操作。(有可能get俩个一样的值+1,在set回去,影响了预期结果),这种后面的操作依赖前面操作的结果的操作被称为竞态操作。

解决方法

  1. 加锁synchronized(lock){}解决
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
private long count = 0;
synchronized long get(){
return count;
}
synchronized void set(long v){
count = v;
}
void add10K() {
int idx = 0;
while(idx++ < 10000) {
set(get()+1)
}
}
}

活跃性

要注意活锁和饿死的关系。

  • 饿死只一个线程被阻塞住或者锁住,但是一直没有得到资源,导致程序一直无法执行下去
  • 活锁:活锁是指线程一直尝试获取资源但是因为条件没法满足所以一直没法获取资源。和死锁相比,死锁是线程阻塞程序无法运行,活锁的线程的状态一直没有被锁住,一直在尝试获取资源,很快会耗光cpu的资源。

解决方法:

  • 饿死可以采用公平锁,用fifo队列来解决锁争抢
  • 活锁:可以采用不同线程阻塞一个随机时间。减少竞争。

性能问题

因为并发变成要对数据加锁,可能会影响系统的并发度,某些操作会变为串行,可以考虑用jdk里的并发包里提供的工具类解决,里面有许多无锁的工具。

JAVA中用等待、通知机制优化循环等待

Posted on 2019-03-18 | Edited on 2022-09-21 | In java , 并发

为什么要采用循环等待机制

之前的文章在解决死锁中的打破:占有且等待中的代码中,采用了死循环的方式来处理竞争失败的情况。

1
2
//返回true
while (!Allocator.getInstance().apply(this, target)) {}

当线程之间竞争不激烈,或者apply方法耗时较短时候这个方案还好,反之,可能有时需要循环几万次才能获取锁这种情况就会大量的浪费cpu计算资源。

java中的等待通知机制的使用

java中为我们提供了wait(),notify(),notifyAll()。方法配置synchronized完成通知等待。我们在不满足条件时候可以用lock.wait()来释放资源,进入等待状态,当满足提交过后,在调用notify()重新获取资源。

wait()的说明

wait是Object对象的方法,实际上是和“锁”对象对应使用的,

  1. wait必须在synchronized代码块中,否则会报异常:IllegalMonitorStateException
  2. wait()方法必须和synchronized对应起来,即synchronized(c){},那么一定是c.wait()

notify()和notifyAll()方法的使用注意事项

  • 当我们notfiy()之后,线程重新获取被保护资源。一定要在次判断锁条件,因为notify之后到资源重新被获取,这段时间有可能再次不满足条件。
  • 没有特殊情况尽量用notifyAll()方法。因为notify只会“唤醒”一个线程,有可能这个线程不是需要唤醒的线程导致执行效率低下。而notfiyAll会唤醒所有在等待的线程,并且同时竞争资源,保证会唤醒需要的唤醒的线程。

之前的代码改造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Allocator {
private static Allocator instance = new Allocator();
public static Allocator getInstance() {
return instance;
}

private Allocator() {
}

private Set<Account> set = new HashSet<>();
public synchronized boolean apply(Account from, Account to) {

while (set.contains(from) || set.contains(to)) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

set.add(from);
set.add(to);
return true;
}

public synchronized void free(Account from, Account to) {
set.remove(from);
set.remove(to);
this.notifyAll();
}
}

public class Account {
private int balance;

public void transfer(Account target, int amt) {
try {
Allocator.getInstance().apply(this, target);

synchronized (this) {
synchronized (target) {
if (balance >= amt) {
balance -= amt;
target.setBalance(target.getBalance() + amt);
}
}
}

} finally {
Allocator.getInstance().free(this, target);
}
}
}

思考

wait()和sleep()的区别

wait

  1. wait是Object中的方法,和锁对应;wait要在Synchronized代码块中执行
  2. wait会释放被锁资源。

sleep

  1. sleep是Thread中的静态方法,让当前线程休眠一段时间
  2. sleep是让出当前线程的执行权限,让优先级低的线程执行,但是不会释放锁资源,也就是说其他线程遇到synchronized代码快会阻塞住

wait的原理

wait方法被调用后,当前线程会去掉锁标记,并且会将当前线程对象放入到对象资源池,知道notify或者notifyAll被调用。
当notify或者notfiyAll被调用时候,会将对象资源池中的一个线程对象或者所有线程移入到锁标记资源池,在锁标记资源池的线程对象会争抢锁,获取资源,获取到资源的开始执行

wait有哪些被唤醒的方法

lock.notify、lock.notifyAll

wait(1000L),时间到后

thread.interrupt 别的线程调用线程的interrupt方法

JAVA中的排他锁--synchroized

Posted on 2019-03-17 | Edited on 2022-09-21 | In java , 并发

锁的作用,解决原子性问题

例如:32位处理器在并发修改一个long类型的数据为什么是不安全的,原因是long类型是64位的数据在32位处理器中分为高32位和低32位,当2个线程同时修改一个long类型的高32位和低32位时候可能就会引起数据不一致的情况。解决方案是,给这个数据加锁,同时只允许一个线程对他进行修改。java中就是利用synchroized关键字。锁模型如下

avator

synchroized关键字的几种用法

synchroized有3种使用方法分别是锁static方法,锁实例方法,锁方法块,如下:

分别表示:

  1. 修饰静态方法锁类的class对象,当这个静态方法被调用时候,该类所有的加锁的静态方法都会被锁住。
  2. 修饰实例方法,锁当前方法实例化的对象,其他线程对该对象的所有synchronized方法都会被锁住,以及该对象涉及锁住的代码块
  3. 代码块锁对象,该对象修饰的其他的代码块、以及该对象的synchronized都会被锁住。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class X {
// 修饰非静态方法
synchronized void foo() {
// 临界区
}
// 修饰静态方法
synchronized static void bar() {
// 临界区
}
// 修饰代码块
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
}

锁和被锁资源的关系

被锁资源和锁的关系应该是N:1关系,即一个资源只能对应一个锁。如下就是错误的且实际编程会经常发生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SafeCalc {
static long value = 0L;

//锁是SafeCalc的实例
synchronized long get() {
return value;
}

//锁是SafeCalc.class
synchronized static void addOne() {
value += 1;
}

//addOne和get方法对应的不是同一把锁,所以在实际中锁是无效的
}

注意HB原则要切实锁住对象

下图的方法是错误的,因为hb原则没有规定get和addOne不适用于任何一条hb原则,所以当并发开始后,有可能get会出现数据不一致情况。解决方法:

  1. get加synchronized关键字:当并发发生后,解锁hb于加锁,所以后面的方法对前面方法一定可见
  2. 将value设置为volatile:并发发生volitile写hb于读,所以addOne之后get一定可见
1
2
3
4
5
6
7
8
9
class SafeCalc {
volitile long value = 0L;
long get() {
return value;
}
synchronized void addOne() {
value += 1;
}
}

如何一把锁保护多个资源

因为被锁的资源和锁之间关系是N:1的关系,那么如何用一把锁保护多个资源呢

如何保护没有关联关系的锁

如下,对于账户里面的余额和密码,我们采用了俩把锁balLock和pwLock分别保护俩个资源,同时也可用一把锁同时保护俩个资源,即给所有的方法加synchroinzed关键字,但是这样锁的粒度较大性能不如前一种好,因为一个修改余额的操作可能会影响到密码相关的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Account {
// 锁:保护账户余额
private final Object balLock
= new Object();
// 账户余额
private Integer balance;
// 锁:保护账户密码
private final Object pwLock
= new Object();
// 账户密码
private String password;

// 取款
void withdraw(Integer amt) {
synchronized(balLock) {
if (this.balance > amt){
this.balance -= amt;
}
}
}
// 查看余额
Integer getBalance() {
synchronized(balLock) {
return balance;
}
}

// 更改密码
void updatePassword(String pw){
synchronized(pwLock) {
this.password = pw;
}
}

// 查看密码
String getPassword() {
synchronized(pwLock) {
return password;
}
}
}

如何保护有关联关系的资源

例如上面提到的Account对象要提供转账方法,很直观的想法是给transfer方法加锁。如下所示,但是这个方法只是看似正确。因为synchronized方法修饰的调用方法的实例即this对象,并不能锁住target对象。
因为在高并发情况下。如果target也向对方转账,是无法保证taget数据的一致性的。如:a–>b 100元,同时另一个线程b–>c100元.如图:

avator

1
2
3
4
5
6
7
8
9
10
11
class Account {
private int balance;
// 转账
synchronized void transfer(
Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}

解决方案是transfer中的代码块用同一把锁锁住,最好的方法是用Account.class,因为jvm虚拟机所有线程只会加载一个Account对象

注意不能将不可变对象置为锁

因为不可变对象的值一旦发生改变,锁就会失效

锁的优化以及如何规避死锁问题

如上所述,对于转账这个场景给Account.class加锁,虽然并发问题是解决了,但是系统中所有的转账都会变成串行的操作,在现实系统中该方法完全不可用。我们可以采用细粒度的方法对程序进行优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Account {
private int balance;
// 转账
synchronized void transfer(
Account target, int amt){
synchronized(this){
synchronized(target){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
}

这个方法是同时锁住转入账户和转出账户,并发问题解决了但是引入了另一个问题,即并发编程最臭名昭著的“死锁”,为什么会发生死锁呢。设想如下场景:
线程T1里a要向b转账,同时线程T2里b也要向a转账,这时候T1拿到了a的锁synchronized(this),同时T2拿到了b的锁synchronized(this),下一步T1和T2因为synchronized(target)同时等待对方释放锁。这时候就进入死了死锁。那么死锁的条件是什么呢?

  1. 互斥:即代码中的target和this俩个线程互斥。
  2. 占有且等待:即T1synchronized(this)占有资源且等待target。
  3. 不释放:T1不会主动释放锁的资源。
  4. 循环等待:上文中说的互相等待。

    那么如何避免死锁呢打破上述的任何一个条件即可。互斥我们是无法打破的我们看下剩下3个条件如何打破。

打破:占有且等待

我们针对“等待”俩个字,让锁一下获得所有的资源。具体方式,我们可以创建一个Allocator方法提供apply申请资源和free释放资源的方法。注意Allocator一定是单例,在锁保证全局唯一,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Allocator {
private static Allocator instance = new Allocator();
public static Allocator getInstance() {
return instance;
}
private Allocator() {
}

private Set<Account> set = new HashSet<>();
public synchronized boolean apply(Account from, Account to) {
if (set.contains(from) || set.contains(to)) {
return false;
}
set.add(from);
set.add(to);
return true;
}

public synchronized void free(Account from, Account to) {
set.remove(from);
set.remove(to);
}
}

public class Account {
int balance;

public void transfer(Account target, int amt) {
try {
while (!Allocator.getInstance().apply(this, target)) {}
synchronized (this) {
synchronized (target) {
if (balance >= amt) {
balance -= amt;
target.setBalance(target.getBalance() + amt);
}
}
}
} finally {
Allocator.getInstance().free(this, target);
}
}
}

打破:不释放

即主动释放资源,但是java的synchronized关键字因为在出现竞争时候线程会进入阻塞状态,本身是没有这个功能可以用Lock替代

打破:循环等待

这个其实很简单,我们给每个账号上分配一个id,对这个id排序,加锁按照id的顺序加锁,保证每个线程的加锁顺序一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Account {
long id;
int balance;

public void transfer(Account target, int amt) {
Account first = this;
Account second = target;
//按照id排序
if (first.id < second.id) {
first = target;
second = this;
}
synchronized (first) {
synchronized (second) {
if (balance >= amt) {
balance -= amt;
target.balance += amt;
}
}
}
}

}

AtomicReferenceFieldUpdater

Posted on 2019-03-11 | Edited on 2022-09-21 | In java , 源码

AtomicReferenceFieldUpdater

JAVA原子更新器,用于原子性的更新对象的字段

用newUpdater来初始化AtomicReferenceFieldUpdater对象。
1.有3个参数第一个参数包含该字段类
2.有修改字段的类
3.字段名

1
AtomicReferenceFieldUpdater updater=AtomicReferenceFieldUpdater.newUpdater(Object.class,String.class,"fieldName");

修改方法,失败会返回false

1
boolean res=updater.compareAndSet(obj,"srcValue","destValue");

Unsafe-java7之后的CAS

Posted on 2019-03-11 | Edited on 2022-09-21 | In java , 源码

Unsafe

java7之后在并发中大量使用Unsafe来保证并发状态,字段修改的唯一性

代码例子

例如在AbstractQueuedSynchronizer中的CAS中

1
2
3
4
5
6
7
8
9
//创建unsafe对象
private static final Unsafe unsafe = Unsafe.getUnsafe();

//计算字段的偏移量
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));

//CAS修改方法,第二个至是计算出的偏移量,第3个值是原有值,第4个参数是修改之后的值,success-true,faild-false
unsafe.compareAndSwapObject(this, tailOffset, expect, update);

阻塞线程park和unpark

park和unpark与object.wait() object.notify一样都是可以阻塞和唤醒线程但是俩这有如下区别:

  1. 面向的主体不同,LockSupport的park, unpark面向的是线程,而Object.wait, nofify面向的是对象
  2. 底层实现机制不同,可以看到Object的wait, notify方法也是native方法,Unsafe的park和unpark方法也是native方法,底层实现不同,Object.notify不能唤醒Unsafe park的线程。

JAVA Nio的学习

Posted on 2019-03-11 | Edited on 2022-09-21 | In java , io

JAVA Nio的学习

Selector 的3种选择方式

Selector.select()

Selector.select(timeout)

Selector.selectNow()

selector.wakeup()

当采用Selector.select()或者select(timeout)时候,会阻塞线程,wakeup()会唤醒被select阻塞的线程。使用时候可以参考netty中的cas方式提高性能。

interestOps

JAVA并发基础-内存模型

Posted on 2019-03-11 | Edited on 2022-09-21 | In java , 并发

引起并发bug的3个主因

缓存导致的可见性

由于cpu拥有自己独立的缓存空间,多线程+多核CPU并发修改同一个变量的流程是:

  1. 每个线程对应的CPU都加载内存中的变量到自己的缓存中;
  2. 修改变量,存入到CPU中缓存中;
  3. 数据从CPU的缓存输入到,内存中;

由于cpu的缓存是独立的对其他线程不可见,所以就会出现在并发情况下内存中的变量相互覆盖。如图:

avator

线程切换的原子性

1个cpu可以“同时”执行多个进程,这里的“指的是,cpu在执行进程时候是通过在进程直接切换,每个进程执行一小段时候实现的。但是进程之间内存不共享,它们的切换需要修改内存地址,开销比较大。所以操作系统一般改为在一个进程替换切换线程来实现并发,因为线程共享内存,切换不需要修改内存地址。但是线程之间的切换往往会引起并发上的bug。
如:我们在一个进程中的俩个线程都对一个变量做累加1000,往往结果不会等于2000,这就是在一个cpu下,俩个线程在执行上来回切换执行导致。如流程如下图下图:

avator

编译优化的指令顺序

现代的编译器为了性能往往会对指令重排序,这就会导致一些奇怪的bug,如下双重检查的单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
  public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}

线程A在执行这段代码的instance = new Singleton();在cpu的指令是3条命令,我们期望的步骤是:

  1. 初始化地址&M
  2. 在&M地址上new Singleton()
  3. &M赋值给instance

而实际上是

  1. 初始化地址&M
  2. &M赋值给instance
  3. 在&M地址上new Singleton()

这样线程B和A同时调用getInstance()的时候,有可能线程B执行第一个判断时候,由于编译器指令优化A已经instance=&M,所以B判断instance不为空直接返回,但是这时候&M还没有new Singleton()导致这空指针异常。

java的内存模型

由于java线程间通信采用的是共享内存的方式,所以遇到上述的指令重排序往往会引起一些bug,在此基础上,java采用了synchronized、volatile、final关键字以及6个Happens-Before规则来约束指令重排序。

happens-before原则

同一线程,前面的操作happens-before于后面的操作

参考代码1,x=42一定happens-before v=true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 以下代码来源于【参考 1】
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里 x 会是多少呢?
}
}
}

volatile关键字修饰的变量,写happens-before读操作

结合代码1:如果线程a、b同时执行,线程a调用writer,线程b调用reader,那么v一定等于true

happens-before传递性 a–>b b–>c 则 a–>c

结合这条规则和1、2看,x一定=42。因为x=42–>v=true v=true–>v==true 所以 x=42

synchronized关键字,不同线程 解锁happens-before加锁

线程b,和线程a同时执行下面代码,线程a获取锁之后执行完成并且释放锁,这时候b执行,x的值一定是12。即线程a操作hanppens-before b

1
2
3
4
5
6
synchronized (this) { // 此处自动加锁
// x 是共享变量, 初始值 =10
if (this.x < 12) {
this.x = 12;
}
} // 此处自动解锁

线程strat()

线程a调用了线程b.start(),那么a的变量和操作,线程b可见

线程的join()

线程b在线程a中调用了b.join(),那么b里的共享变量和共享操作,a可见

1
2
3
4
5
6
7
8
9
10
11
12
Thread B = new Thread(()->{
// 此处对共享变量 var 修改
var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程 B 可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用 B.join() 之后皆可见
// 此例中,var==66

final的应用

说明该变量初始化后即不变,其他线程访问时候也一定是一个完全初始化的变量,这就可以解决前面提到的双重检查单例线程不安全的问题。但是要小心溢出问题。

1
2
3
4
5
6
7
8
9
// 以下代码来源于【参考 1】
final int x;
// 错误的构造函数
public FinalFieldExample() {
x = 3;
y = 4;
// 此处就是讲 this 逸出,
global.obj = this;
}

JVM调试的常用命令

Posted on 2019-03-10 | Edited on 2022-09-21 | In java , jvm优化

java性能调优的主要几个工具

通过jstack打印堆栈信息,得知线程的使用情况
通过jstat打印堆中gc以及对象的情况
通过jmap详细分析堆中内存的个数大小,必要时可以dump出来结合jhat查看

jstack

打印JAVA的堆栈信息

1
2

jstack <pid>

jstat

统计gc、JVM的情况

1
2
3
4

统计gc的情况
jstat -gcutil <pid> 2000 30
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT

jmap

统计jvm的内存中类的使用情况

1
2
3
4
5
6
7
8

按数量排序
jmap -histo:live <pid>|grep xxxx|sort -k 2 -g -r|head

按大小排序
jmap -histo:live <pid>|grep xxxx|sort -k 3 -g -r|head

`

配合jhat使用

1
2
3

jmap -dump:live,file=xxx.log pid
jhat xxx.log

排查java异常进程的方法

Posted on 2019-03-10 | Edited on 2022-09-21 | In java , jvm优化

定位JAVA进程中异常线程的方法

日常使用中我们往往会遇到JAVA进程内存飙高,程序hang住,cpu的load过高等情况。这时候往往需要定位到JAVA的实例看下具体占用CPU、内存过高的线程都在做什么?
下面总结了下如何定位JAVA问题线程的方法。

找到有问题的JVM进程

linux命令:top–找到有问题的JAVA进程
shift+p 按照cpu排序
shift+m按照内存排序

查看有问题JAVA进程中的线程

linux命令:top -H -p pid 查看pid进程下所有有问题的线程

  1. 将十进制数换成16进制:printf “%x\n” tid
  2. 查看进程下的线程正在执行的方法: jstack pid |grep nid=0x【tid】 -A 30

jstack查看出来的线程信息梳理

线程的几种状态

  1. RUNNABLE,线程处于执行中
  2. BLOCKED,线程被阻塞
  3. WAITING,线程正在等待

对于锁的竞争,可参照下面的思路排查问题:

1
2
3
4
线程1获取到锁,处于RUNNABLE状态,线程2处于BLOCK状态;  
1、locked <0x000000076bf62208>说明线程1对地址为0x000000076bf62208对象进行了加锁;
2、waiting to lock <0x000000076bf62208> 说明线程2在等待地址为0x000000076bf62208对象上的锁;
3、waiting for monitor entry [0x000000001e21f000]说明线程1是通过synchronized关键字进入了监视器的临界区,并处于"Entry Set"队列,等待monitor;

jmap查看JVM系统信息的命令

查找JVM中系统的大小和个数

1
2
3
4
5
按照个数统计  
jmap -histo:live pid|grep keyword|sort -k 2 -g -r|head -10

按照占用大小统计
jmap -histo:live pid|grep keyword|sort -k 2 -g -r|head -10

查看JVM中Heap的情况

1
jmap -heap pid

情况如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Attaching to process ID Pid, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.65-b01

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 4294967296 (4096.0MB)
NewSize = 858980352 (819.1875MB)
MaxNewSize = 858980352 (819.1875MB)
OldSize = 3435986944 (3276.8125MB)
NewRatio = 4
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 = 773128192 (737.3125MB)
used = 9882512 (9.424697875976562MB)
free = 763245680 (727.8878021240234MB)
1.2782501145683225% used
Eden Space: (Edn区)
capacity = 687276032 (655.4375MB)
used = 9882512 (9.424697875976562MB)
free = 677393520 (646.0128021240234MB)
1.4379247260000476% used
From Space: (Survivor中的From)
capacity = 85852160 (81.875MB)
used = 0 (0.0MB)
free = 85852160 (81.875MB)
0.0% used
To Space: (Survivor中的To)
capacity = 85852160 (81.875MB)
used = 0 (0.0MB)
free = 85852160 (81.875MB)
0.0% used
concurrent mark-sweep generation: (老年代)
capacity = 3435986944 (3276.8125MB)
used = 258117296 (246.1598358154297MB)
free = 3177869648 (3030.6526641845703MB)
7.512173364067357% used
1…181920…23

Liu hao

励志当好厨子的程序员

229 posts
54 categories
81 tags
RSS
GitHub E-Mail
© 2018 – 2023 Liu hao
Powered by Hexo v3.9.0
|
Theme – NexT.Pisces v7.0.0