Fork me on GitHub
Blog


  • 首页

  • 标签

  • 分类

  • 归档

  • 关于

【Java多线程】JUC锁 06. ReentrantReadWriteLock

发表于 2018-07-30 | 分类于 Java多线程 , JUC锁

ReentrantReadWriteLock

1. 前言

  • 读写锁,它维护了一对相关的锁 :“读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作
  • 默认不公平模式,支持公平模式,不会强加对锁的读取或写入优先访问顺序
  • 写入锁可以降级为读取锁,读取锁不能升级为写入锁

2. 源码解析

2.1 数据结构

  • ReentrantReadWriteLock包含一对读锁ReadLock和写锁WriteLock,Sync对象;读锁ReadLock和写锁WriteLock继承自Lock接口,内部的Sync对象和ReentrantReadWriteLock的Sync对象是一样的
  • ReentrantReadWriteLock的公平锁和非公平锁由Sync对象实例决定

2.2 内部类

  • ReadLock
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
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;

protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync; // 同ReentrantReadWriteLock Sync一样
}

//获取锁
public void lock() {
sync.acquireShared(1);
}

//获取锁,除非当前线程被中断
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}

//尝试获取锁,立即返回
public boolean tryLock() {
return sync.tryReadLock();
}

//获取读锁,除非在指定时间内写锁被占用或者当前线程中断
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

//释放锁
public void unlock() {
sync.releaseShared(1);
}

//读锁不支持Condition条件
public Condition newCondition() {
throw new UnsupportedOperationException();
}

public String toString() {
int r = sync.getReadLockCount();
return super.toString() +
"[Read locks = " + r + "]";
}
}
  • WriteLock
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
54
55
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;

protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync; //sync 同ReentrantReadWriteLock.sync
}

//获取锁
public void lock() {
sync.acquire(1);
}

//获取锁除非当前线程中断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}

//尝试获取锁
public boolean tryLock( ) {
return sync.tryWriteLock();
}

//指定时间尝试获取锁,除非线程中断
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

//释放锁
public void unlock() {
sync.release(1);
}

public Condition newCondition() {
return sync.newCondition();
}

public String toString() {
Thread o = sync.getOwner();
return super.toString() + ((o == null) ?
"[Unlocked]" :
"[Locked by thread " + o.getName() + "]");
}

//查询锁是否被当前线程占据
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}

//当前线程重入锁数
public int getHoldCount() {
return sync.getWriteHoldCount();
}
}

2.3 共享锁

2.3.1 获取锁

获取共享锁ReadLock思想:首先通过tryAcquireShared()尝试获取;失败则通过doAcquireShared()不断的循环并尝试获取锁,若有需要,则阻塞等待。

  • lock()

在ReadLock中获取锁

1
2
3
public void lock() {
sync.acquireShared(1);
}
1
2
3
4
5
//AQS中实现,获取共享锁
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
  • tryAcquireShared

在ReentrantReadWriteLock内部类Sync实现tryAcquireShared()

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
//尝试获取共享锁
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState(); //锁状态
// 如果“锁”是“互斥锁”,并且获取锁的线程不是current线程;则返回-1。
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 获取“读取锁”的共享计数
int r = sharedCount(c);
// 如果“不需要阻塞等待”,并且“读取锁”的共享计数小于MAX_COUNT;
// 则通过CAS函数更新“锁的状态”,将“读取锁”的共享计数+1。
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
//HoldCounter是用来统计该线程获取“读取锁”的次数。
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}

其中判断是否需要阻塞readerShouldBlock与锁模式有关:

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
//FairSync:
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
//判断当前线程前是否有线程在等待
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

//NonfairSync:
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
//AQS中实现,等待队列首线程不是独占锁
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
  • fullTryAcquireShared
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
54
55
56
57
58
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
// 获取“锁”的状态
int c = getState();
// 如果“锁”是“互斥锁”,并且获取锁的线程不是current线程;则返回-1。
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// 如果“需要阻塞等待”。
// (01) 当“需要阻塞等待”的线程是第1个获取锁的线程的话,则继续往下执行。
// (02) 当“需要阻塞等待”的线程获取锁的次数=0时,则返回-1。
} else if (readerShouldBlock()) {
// 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程
if (firstReader == current) {
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId()) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
// 如果当前线程获取锁的计数=0,则返回-1。
if (rh.count == 0)
return -1;
}
}
// 如果“不需要阻塞等待”,则获取“读取锁”的共享统计数;
// 如果共享统计数超过MAX_COUNT,则抛出异常。
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 将线程获取“读取锁”的次数+1。
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 如果是第1次获取“读取锁”,则更新firstReader和firstReaderHoldCount。
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程,
// 则将firstReaderHoldCount+1。
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
// 更新线程的获取“读取锁”的共享计数
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
  • doAcquireShared

doAcquireShared()的作用是在CLH队列中自旋获取共享锁。doAcquireShared()在每一次尝试获取锁时,是通过tryAcquireShared()来执行的!

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
private void doAcquireShared(int arg) {
// 创建“当前线程”对应的Shared节点,并将该线程添加到CLH队列中。
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取“node”的前一节点
final Node p = node.predecessor();
// 如果“当前线程”是CLH队列的表头,则尝试获取共享锁。
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 如果“当前线程”不是CLH队列的表头,则通过shouldParkAfterFailedAcquire()判断是否需要等待,
// 需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。若阻塞等待过程中,线程被中断过,则设置interrupted为true。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

2.3.2 释放锁

释放共享锁的思想,是先通过tryReleaseShared()尝试释放共享锁。尝试成功的话,则通过doReleaseShared()唤醒“其他等待获取共享锁的线程”,并返回true;否则的话,返回flase。

  • unlock()
1
2
3
public void unlock() {
sync.releaseShared(1);
}
  • releaseShared()
1
2
3
4
5
6
7
8
//AQS中实现
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
  • tryReleaseShared()
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
protected final boolean tryReleaseShared(int unused) {
// 获取当前线程,即释放共享锁的线程。
Thread current = Thread.currentThread();
// 如果想要释放锁的线程(current)是第1个获取锁(firstReader)的线程,
// 并且“第1个获取锁的线程获取锁的次数”=1,则设置firstReader为null;
// 否则,将“第1个获取锁的线程的获取次数”-1。
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
// 获取rh对象,并更新“当前线程获取锁的信息”。
} else {

HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
// 获取锁的状态
int c = getState();
// 将锁的获取次数-1。
int nextc = c - SHARED_UNIT;
// 通过CAS更新锁的状态。
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
  • doReleaseShared()
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
//AQS中实现,会释放“共享锁”。从前往后的遍历CLH队列,依次“唤醒”然后“执行”队列中每个节点对应的线程;最终的目的是让这些线程释放它们所持有的锁。
private void doReleaseShared() {
for (;;) {
// 获取CLH队列的头节点
Node h = head;
// 如果头节点不为null,并且头节点不等于tail节点。
if (h != null && h != tail) {
// 获取头节点对应的线程的状态
int ws = h.waitStatus;
// 如果头节点对应的线程是SIGNAL状态,则意味着“头节点的下一个节点所对应的线程”需要被unpark唤醒。
if (ws == Node.SIGNAL) {
// 设置“头节点对应的线程状态”为空状态。失败的话,则继续循环。
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒“头节点的下一个节点所对应的线程”。
unparkSuccessor(h);
}
// 如果头节点对应的线程是空状态,则设置“文件点对应的线程所拥有的共享锁”为其它线程获取锁的空状态。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果头节点发生变化,则继续循环。否则,退出循环。
if (h == head) // loop if head changed
break;
}
}

3. 参考

http://www.cnblogs.com/skywang12345/p/3505809.html

【JVM】Java类加载机制

发表于 2018-07-30 | 分类于 JVM

Java类加载机制

1. 生命周期

类从加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期如下:

1532939355060

2. 加载过程

2.1 加载

在加载阶段,虚拟机需要完成以下3件事情

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。(这个Class对象并没有规定是在Java堆内存中,对于HotSpot虚拟机而言,它比较特殊,虽为对象,但存放在方法区中)

2.2 验证

这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理

这阶段的验证是基于二进制字节流的,只有通过验证,字节流才会进入内存的方法区进行存储,后面的验证阶段是基于方法区的存储结构进行的

  • 元数据验证

对字节码描述的信息进行语义分析,保证描述的信息符合Java语言规范

  • 字节码验证

最复杂的阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的

  • 符号引用验证

最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用时候,确保解析动作能正常进行。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验

2.3 准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。

通常情况下,初始值为零值,比如一个类变量定义为:

1
public static int v = 8080;

实际上变量v在准备阶段过后的初始值为0而不是8080,将v赋值为8080的putstatic指令是程序被编译后,存放于类构造器方法之中,将v赋值为8080的动作将在初始化阶段才会进行。

如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段v会初始化ConstantValue属性为所指定的值

1
public static final int v = 8080;

在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080。

2.4 解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程

  • 符号引用:与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
  • 直接引用:与虚拟机实现的布局相关,可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

2.5 初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。

初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。

注意以下几种情况不会执行类初始化:

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化。
  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  • 通过类名获取Class对象,不会触发类的初始化。
  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

3. 参考

《深入理解Java虚拟机》

【JVM】Class类文件结构

发表于 2018-07-30 | 分类于 JVM

Class类文件结构

1. 前言

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有任何分隔符,这使得Class文件中存储的内容几乎全部都是程序运行的必要数据

Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表

  • 无符号数

属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,以u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节

  • 表

由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。

用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的。具体的顺序定义如下:

1532919736753

几个概念:

  • 全限定名

将类全名中的“.”替换为“/”,为了保证多个连续的全限定名之间不产生混淆,在最后加上“;”表示全限定名结束。例如:”com.test.Test”类的全限定名为”com/test/Test;”

  • 简单名称

没有类型和参数修饰的方法或字段名称。例如:”public void add(int a,int b){…}”该方法的简单名称为”add”,”int a = 123;”该字段的简单名称为”a”

  • 描述符

描述字段的数据类型、方法的参数列表(包括数量、类型和顺序)和返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符表示,而对象类型则用字符L加对象全限定名表示

标识字符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 特殊类型void
L 对象类型,如:Ljava/lang/Object;

对于数组类型,每一维将使用一个前置的“[”字符来描述,如:”int[]”将被记录为”[I”,”String[][]”将被记录为”[[Ljava/lang/String;”

用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组”()”之内,如:方法”String getAll(int id,String name)”的描述符为”(I,Ljava/lang/String;)Ljava/lang/String;”

2. Class文件结构

2.1 魔数

  • 每个Class文件的头4个字节称为魔数(Magic Number)
  • 唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。
  • Class文件魔数的值为0xCAFEBABE

2.2 版本号

紧接着魔数的4个字节是Class文件版本号,如果Class文件的版本号超过虚拟机版本,将被拒绝执行 。版本号分为:

  • 次版本号(minor_version): 前2字节用于表示次版本号
  • 主版本号(major_version): 后2字节用于表示主版本号。

2.3 常量池

紧接着魔数与版本号之后的是常量池入口,常量池简单理解为class文件的资源从库

  • Class文件结构中与其它项目关联最多的数据类型、
  • 占用Class文件空间最大的数据项目之一
  • 在文件中第一个出现的表类型数据项目

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。从1开始计数,第0项空出来是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。

常量池之中主要存放两大类常量:

  • 字面量: 比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
  • 符号引用: 属于编译原理方面的概念,包括了下面三类常量:
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池中每一项都是一种表,共14种,表开始的第一位都是一个u1类型的标志位,代表这个常量属于哪种属于哪种常量类型,常量类型如下所示:

1532920845018

1
2
3
4
5
// 表示类或接口
CONSTANT_Class_info {
u1 tag; //标志位
u2 name_index; //指向常量池的有效索引,常量池在该索引处为CONSTANT_Utf8_info
}
1
2
3
4
5
6
// 表示字符串常量的值
CONSTANT_Utf8_info {
u1 tag;
u2 length; //指明了 bytes[]数组的长度
u1 bytes[length]; //表示字符串值的byte数组
}

2.4 访问标志

access_flags: 常量池之后两个字节,用于识别一些类或接口层次的访问信息

1532921992330

2.5 类索引、父类索引和接口索引集合

  • 类索引(this_class),用于确定这个类的全限定名,占2字节
  • 父类索引(super_class),用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了java.lang.Object类之外所有类都有父类,故除了java.lang.Object类之外,所有类该字段值都不为0),占2字节
  • 接口索引计数器(interfaces_count),占2字节。如果该类没有实现任何接口,则该计数器值为0,并且后面的接口的索引集合将不占用任何字节
  • 接口索引集合(interfaces),一组u2类型数据的集合。用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果该类本身为接口,则为extends语句)后的接口顺序从左至右排列在接口的索引集合

this_class、super_class与interfaces按顺序排列在访问标志之后,它们中保存的索引值均指向常量池中一个CONSTANT_Class_info类型的常量,通过这个常量中保存的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串

2.6 字段表集合

字段表结构为:

1
2
3
4
5
6
7
field_info {
u2 access_flags;//定义字段被访问权限和基础属性
u2 name_index; //表示一个有效的字段的非全限定名,是对常量池的一个有效索引
u2 descriptor_index; //表示一个有效的字段的描述符,是对常量池的一个有效索引
u2 attributes_count; //表示当前字段的附加属性的数量
attribute_info attributes[attributes_count];//属性表集合
}

access_flags 用于定义字段被访问权限和基础属性的掩码标志,取值如下:

名称 标志值 含义
ACC_PUBLIC 0x0001 public,表示字段可以从任何包访问。
ACC_PRIVATE 0x0002 private,表示字段仅能该类自身调用。
ACC_PROTECTED 0x0004 protected,表示字段可以被子类调用。
ACC_STATIC 0x0008 static,表示静态字段。
ACC_FINAL 0x0010 final, 表示字段定义后值无法修改。
ACC_VOLATILE 0x0040 volatile, 表示字段是易变的。
ACC_TRANSIENT 0x0080 transient, 表示字段不会被序列化。
ACC_SYNTHETIC 0x1000 表示字段由编译器自动产生。
ACC_ENUM 0x4000 enum, 表示字段为枚举类型。

2.7 方法表集合

定义所有方法,包括实例初始化方法和类初始化方法,方法表结构如下:

1
2
3
4
5
6
7
method_info {
u2 access_flags; //定义当前方法的访问权限和基本属性的掩码标志
u2 name_index; //表示一个方法的有效的非全限定名
u2 descriptor_index; //表示一个有效的方法的描述符
u2 attributes_count; //表示方法的附加属性的数量
attribute_info attributes[attributes_count]; //方法属性
}

access_flags 项的值是用于定义当前方法的访问权限和基本属性的掩码标志, 取值如下:

标记名 值 说明
ACC_PUBLIC 0x0001 public, 方法可以从包外访问
ACC_PRIVATE 0x0002 private, 方法只能本类中访问
ACC_PROTECTED 0x0004 protected, 方法在自身和子类可以访问
ACC_STATIC 0x0008 static, 静态方法
ACC_FINAL 0x0010 final, 方法不能被重写(覆盖)
ACC_SYNCHRONIZED 0x0020 synchronized, 方法由管程同步
ACC_BRIDGE 0x0040 bridge, 方法由编译器产生
ACC_VARARGS 0x0080 表示方法带有变长参数
ACC_NATIVE 0x0100 native, 方法引用非 java 语言的本地方法
ACC_ABSTRACT 0x0400 abstract, 方法没有具体实现
ACC_STRICT 0x0800 strictfp, 方法使用 FP-strict 浮点格式
ACC_SYNTHETIC 0x1000 方法在源文件中不出现,由编译器产生

方法中的Java代码,经编译器编译成字节码指令后,保存在方法属性表集合中的”Code”属性中

2.8 属性表集合

在CLass文件,字段表,方法表都可携带自己的属性表集合,以用于描述某些场景专有的信息,属性表的结构如下:

1
2
3
4
5
attribute_info {
u2 attribute_name_index; //指向常量池的无符号索引。常量池在该索引处的项必须是 CONSTANT_Utf8_info
u4 attribute_length; //info[attribute_length] 长度
u1 info[attribute_length]; //属性值
}

与Class文件中其它数据项对长度、顺序、格式的严格要求不同,属性表集合不要求其中包含的属性表具有严格的顺序,并且只要属性的名称不与已有的属性名称重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息。虚拟机在运行时会忽略不能识别的属性,为了能正确解析Class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性(预定义属性已经增加到21项):

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类文件、字段表、方法表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTale Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述(局部变量作用域)
SourceFile 类文件 源文件名称
Synthetic 类文件、方法表、字段表 标识方法或字段是由编译器自动生成的
  • Code属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack; //当前方法的操作数栈在运行执行的任何时间点的最大深度
u2 max_locals; //分配在当前方法引用的局部变量表中的局部变量个数,包括调用此方法时用于传递参数的局部变量;long 和 double 型的局部变量的最大索引是 max_locals-2,其它类型的局部变量的最大索引是 max_locals-1
u4 code_length; //当前方法的 code[]数组的字节数
u1 code[code_length]; //实现当前方法的 Java 虚拟机字节码
u2 exception_table_length;
{ u2 start_pc; //start_pc 和 end_pc 两项的值表明了异常处理器在 code[]数组中的有效范围
u2 end_pc;
u2 handler_pc; //表示一个异常处理器的起点
u2 catch_type; //指向常量池的一个有效索引, CONSTANT_Class_info类型
} exception_table[exception_table_length]; //表示 code[]数组中的一个异常处理器
u2 attributes_count;
attribute_info attributes[attributes_count]; //属性表的每个成员的值必须是 attribute 结构
}
  • LocalVariableTable 属性

LocalVariableTable 是可选变长属性,位于 Code属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息。在 Code 属性的属性表中, LocalVariableTable 属性可以按照任意顺序出现。 Code 属性中的每个局部变量最多只能有一 个 LocalVariableTable 属性。

1
2
3
4
5
6
7
8
9
10
11
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc; //局部变量的索引都在范围[start_pc, start_pc+length)中
u2 length;
u2 name_index;//表示一个局部变量的有效的非全限定名, 常量池的一个有效索引,CONSTANT_Utf8_info结构
u2 descriptor_index;//表示源程序中局部变量类型的字段描述符, CONSTANT_Utf8_info结构
u2 index;//局部变量在当前栈帧的局部变量表中的索引
} local_variable_table[local_variable_table_length];
}

参考

《深入理解Java虚拟机》

《Java虚拟机规范》

https://www.cnblogs.com/wade-luffy/p/5929325.html

【Java多线程】JUC锁 05. ReentrantLock

发表于 2018-07-25 | 分类于 Java多线程 , JUC锁

ReentrantLock

1. 前言

  • ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。
  • ReentrantLock分为“公平锁”和“非公平锁”。它们的区别体现在获取锁的机制上是否公平。
  • ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁
  • 最多支持同一线程的2147483647个递归锁,超过将抛出Error
  • ReentrantLock 和 synchronized 区别:

    • ReentrantLock 非阻塞,synchronized 阻塞
    • ReentrantLock 获取锁时候可以中断,可以设置超时时间

    • ReentrantLock 灵活性更高,允许获取多个锁,以不同顺序释放锁;允许在不同范围释放锁

    • 都具备线程重入特性;ReentrantLock 表现为API层面的互斥锁, synchronized 表现为原生语法层面的互斥锁

    • ReentrantLock 具备以下高级功能:
      • 等待可中断
      • 可实现公平锁:必须按照申请锁的时间顺序来依次获得锁
      • 锁可以绑定多个条件

2. 源码解析

2.1 数据结构

  • ReentrantLock与sync是组合关系。ReentrantLock中,包含了Sync对象,Sync是AQS的子类;
  • Sync有两个子类FairSync(公平锁)和NonFairSync(非公平锁)。ReentrantLock是一个独占锁,它是公平锁还是非公平锁由Sync对象实例决定。

2.2函数列表

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
public class ReentrantLock implements Lock, java.io.Serializable {

private final Sync sync; //内部类,AQS子类,基于AQS实现
//构造方法,默认是“非公平锁”
public ReentrantLock() {
sync = new NonfairSync();
}
//fair为true表示是公平锁,fair为false表示是非公平锁。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

// 查询当前线程保持此锁的次数。
int getHoldCount()
// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
protected Thread getOwner()
// 返回一个 collection,它包含可能正等待获取此锁的线程。
protected Collection<Thread> getQueuedThreads()
// 返回正等待获取此锁的线程估计数。
int getQueueLength()
// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition)
// 返回等待与此锁相关的给定条件的线程估计数。
int getWaitQueueLength(Condition condition)
// 查询给定线程是否正在等待获取此锁。
boolean hasQueuedThread(Thread thread)
// 查询是否有些线程正在等待获取此锁。
boolean hasQueuedThreads()
// 查询是否有些线程正在等待与此锁有关的给定条件。
boolean hasWaiters(Condition condition)
// 如果是“公平锁”返回true,否则返回false。
boolean isFair()
// 查询当前线程是否保持此锁。
boolean isHeldByCurrentThread()
// 查询此锁是否由任意线程保持。
boolean isLocked()
// 获取锁。
void lock()
// 如果当前线程未被中断,则获取锁。
void lockInterruptibly()
// 返回用来与此 Lock 实例一起使用的 Condition 实例。
Condition newCondition()
// 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
boolean tryLock()
// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
boolean tryLock(long timeout, TimeUnit unit)
// 试图释放此锁。
void unlock()
}

2.3 公平锁和非公平锁

2.3.1 公平锁

  • lock()

公平锁在FairSync类实现:

1
2
3
final void lock() {
acquire(1); //设置状态,锁可获取时候,为0;锁被线程获取,状态值为1;可重入锁,状态值state+1
}

acquire 在AQS中实现。

1
2
3
4
5
public final void acquire(int arg) {
if (!tryAcquire(arg) && //获取锁成功直接返回,失败则进入等待队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//独占模式
selfInterrupt();//自己产生一个中断。在acquireQueued()中,即使是线程在阻塞状态被中断唤醒而获取到cpu执行权利;但是,如果该线程的前面还有其它等待锁的线程,根据公平性原则,该线程依然无法获取到锁。它会再次阻塞! 该线程再次阻塞,直到该线程被它的前面等待锁的线程锁唤醒;线程才会获取锁,然后“真正执行起来”!
}

过程如下:

  • 首先通过tryAcquire()尝试获取锁,获取成功直接返回;
  • 当前线程tryAcquire()获取失败,通过addWaiter(Node.EXCLUSIVE)方法将当前线程添加入CLH等待队列末尾
  • 加入CLH队列后,通过acquireQueued()方法获取锁。“当前线程”在执行acquireQueued()时,会进入到CLH队列中休眠等待,直到获取锁了才返回!如果“当前线程”在休眠等待过程中被中断过,acquireQueued()会返回true,此时”当前线程”会调用selfInterrupt()来自己给自己产生一个中断
  1. tryAcquire方法在FairSync实现,尝试获取锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//尝试获取锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { //可获取锁,锁没有被任何线程拥有
if (!hasQueuedPredecessors() && //当前线程是CLH队列中第一个线程或者CLH为空
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
1
2
3
4
5
6
7
8
9
10
11
//CLH队列为空或者队首为当前线程返回false
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
  1. addWaiter加入CLH等待队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//AQS中实现,加入等待队列尾节点
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); //自旋加入节点
return node;
}
  1. acquireQueued获取锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//AQS中实现
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//p == head 公平性原则,线程可能由于中断而唤醒影响公平,需要当前线程被其他线程unpark
if (p == head && tryAcquire(arg)) { //当前继节点是CLH队列的头节点,并且它释放锁之后;就轮到当前节点获取锁了。然后,当前节点通过tryAcquire()获取锁;获取成功的话,通过setHead(node)设置当前节点为头节点,并返回。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && //判断“当前线程是否应该阻塞”
parkAndCheckInterrupt()) //阻塞线程
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 判断“当前线程是否应该阻塞”
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 如果前继节点是SIGNAL状态,则意味这当前线程需要被unpark唤醒。此时,返回true。
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
// 如果前继节点是“取消”状态,则设置 “当前节点”的 “当前前继节点” 为 “‘原前继节点’的前继节点”。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果前继节点为“0”或者“共享锁”状态,则设置前继节点为SIGNAL状态。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
  • unlock()
1
2
3
public void unlock() {
sync.release(1); //每次释放,锁的状态 -1
}

release释放锁在AQS中实现

1
2
3
4
5
6
7
8
9
10
//尝试释放当前线程锁持有的锁。成功的话,则唤醒后继等待线程,并返回true。否则,直接返回false。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//Sync中实现,尝试释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {// 如果“锁”已经被当前线程彻底释放,则设置“锁”的持有者为null,即锁是可获取状态。
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//唤醒当前线程的后继线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0) //设置状态为0, 可获取锁
compareAndSetWaitStatus(node, ws, 0);
//获取当前节点的“有效的后继节点”,无效的话,则通过for循环进行获取。
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}

2.3.2 非公平锁

非公平锁和公平锁主要在获取锁上有差别。

公平锁在尝试获取锁时,即使“锁”没有被任何线程锁持有,它也会判断自己是不是CLH等待队列的表头;是的话,才获取锁。
而非公平锁在尝试获取锁时,如果“锁”没有被任何线程持有,则不管它在CLH队列的何处,它都直接获取锁。

  • lock()
1
2
3
4
5
6
7
//NonfairSync类中实现,会先通过CAS直接获取锁;失败调用acquire(1)获取锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
  • tryAcquire()
1
2
3
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Sync类中实现,非公平尝试获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {//不需要判断在CLH首节点
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

3. 参考

http://www.cnblogs.com/skywang12345/p/3496147.html

http://www.cnblogs.com/skywang12345/p/3496147.html

【JVM】jdk命令行工具

发表于 2018-07-24 | 分类于 JVM

JDK 命令行工具

命令名称 全称 用途
jps JVM Process Status Tool 显示指定系统内所有的HotSpot虚拟机进程
jstat JVM Statistics Monitoring Tool 用于收集Hotspot虚拟机各方面的运行数据
jinfo Configuration Info for Java 显示虚拟机配置信息
jmap JVM Memory Map 生成虚拟机的内存转储快照,生成heapdump文件
jhat JVM Heap Dump Browser 用于分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户在浏览器上查看分析结果
jstack JVM Stack Trace 显示虚拟机的线程快照

1. jps:虚拟机进程状况工具

可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)的名称,以及这些进程的本地虚拟机的唯一ID(LVMID,Local Virtual Machine Identifier)

jps命令格式:

​ jps [option][hostid]

jps可以通过RMI协议查询开启了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的主机名。

选项 作用
-q 只输出LVMID,省略主类的名称
-m 输出虚拟机进程启动时传递给主类的main()函数的参数
-l 输出主类的全名,如果进程执行的是jar包,输出jar路径
-v 输出虚拟机进程启动时JVM参数

2. jstat:虚拟机统计信息监控工具

是用于监控虚拟机各种运行状态信息的命令行工具.它可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

jstat命令格式:

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

– VMID与LVMID需要特别说明下:如果是本地虚拟机进程,VMID和LVMID是一致的,如果是远程虚拟机进程,那VMID的格式应当是:[protocol:][//] lvmid [@hostname[:port]/servername]

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

选项option代表这用户希望查询的虚拟机信息,主要分为3类:类装载、垃圾收集和运行期编译状况,具体选项及租用参见下表:

选项 作用
-class 监视类装载、卸载数量、总空间及类装载所耗费的时间
-gc 监视Java堆状况,包括Eden区、2个Survivor区、老年代、永久代等的容量
-gccapacity 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大和最小空间
-gcutil 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause 与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因
-gcnew 监视新生代GC的状况
-gcnewcapacity 监视内容与-gcnew基本相同,输出主要关注使用到的最大和最小空间
-gcold 监视老年代GC的状况
-gcoldcapacity 监视内容与——gcold基本相同,输出主要关注使用到的最大和最小空间
-gcpermcapacity 输出永久代使用到的最大和最小空间
-compiler 输出JIT编译器编译过的方法、耗时等信息
-printcompilation 输出已经被JIT编译的方法

3. jinfo: Java配置信息工具

实时地查看和调整虚拟机的各项参数

jinfo命令格式:

​ jinfo [option] pid

可以用jinfo -flags来查询线程的参数

4. jmap: Java内存映像工具

用于生产堆转储快照(一般称为heapdump或dump文件),且可以查询finalize执行队列,Java堆与永久代的一些信息

jmap命令格式:

​ jmap [option] vmid

选项 作用
-dump 生成Java堆转储快照。格式为:-dump:[live,]format=b,file=,其中live子参数说明是否只dump出存活的对象
-finalizerinfo 显示在F-Queue中等待Finalizer线程执行finalize()方法的对象。只在Linux/Solaris平台下有效
-heap 显示Java堆详细信息,如使用哪种回收器、参数配置、分代状况等。只在Linux/Solaris平台下有效
-histo 显示堆中对象统计信息,包括类、实例数量和合计容量
-permstat 以ClassLoader为统计口径显示永久代内存状态。只在Linux/Solaris平台下有效
-F 当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照。只在Linux/Solaris平台下有效

5. jhat:虚拟机堆转储快照分析工具

分析jmap生成的堆转储快照

jhat命令格式:

​ jhat heapdumpFileName

6. jstack: Java堆栈跟踪工具

命令用于生成虚拟机当前时刻的线程快照

jstack命令格式:

​ jstack [option] vmid

option选项的合法值与具体意义如下:

选项 作用
-F 当正常输出的请求不被响应时,强制输出线程堆栈
-l 除堆栈外,显示关于锁的附加信息
-m 如果调用到本地方法的话,可以显示C/C++的堆栈

可视化工具:JConsole, VisualVM

【JVM】垃圾回收机制

发表于 2018-07-23 | 分类于 JVM

垃圾回收机制

1. 对象存活判定算法

1.1 引用计数法

算法思路:给对象添加一个引用计数器,当有一个地方引用它时,计数器+1;当引用失效时,计数器 -1;任何时刻计数器为0的对象是不能再被使用的

无法解决相互引用的问题

1.2 可达性分析算法

算法思路:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,称为这个对象是不可用的。

GC Roots对象:

  • 虚拟机栈(栈帧中本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • JNI中引用的对象

1.3 引用类别

  • 强引用:我们常见的普通对象引用,类似"Object obj = new Object()"这类引用,只要还有强引用指向一个对象,就表明该对象还活着,GC永不会回收该对象
  • 软引用(SoftReference):相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当JVM认为内存不足时,才会去试图回收软引用指向的对象。软引用通常用来实现内存敏感的缓存。
  • 弱引用(WeakReference):也是用来描述非必须对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下次垃圾收集发生之前。无论当前内存是否足够,都会回收掉只被弱引用关联的对象。同样是很多缓存实现的选择
  • 虚引用(PhantomReference):有时也叫幻象引用,最弱的一种引用关系,不能通过它访问对象。虚引用仅仅提供一种确保对象被finalize以后,做某些事情的机制,如Post-Mortem清理机制、监控对象的创建和销毁、Java平台自身的Cleaner机制。

1.4 方法区垃圾回收

  • 废弃常量:系统中没有任何地方引用该字面量
  • 无用的类
    • 该类所有实例都已被回收
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法

2. 垃圾收集算法

2.1 标记-清除算法

算法分标记和清除两个阶段,首先标记处所有需要被回收的对象,在标记完成之后统一回收所有被标记的对象

不足:

  • 效率问题,标记和清除两个过程的效率都不高
  • 空间问题,产生大量不连续的内存碎片

2.2 复制算法

  • 将内存按容量分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上,然后将已使用过的内存空间一次清理掉
  • 新生代垃圾回收算法,HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,可用参数 -XX:SurvivorRatio设置

2.3 标记-整理算法

标记过程同”标记-清除”算法,整理过程为让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

3. 内存分配回收策略

  • 对象优先在Eden区分配

    • Eden区没有足够空间进行分配,将发起一次Minor GC
    • -Xmn 参数设置新生代大小
  • 大对象直接进入老年代

大对象指需要大量连续内存空间的Java对象,典型的为很长的字符串和大数组

  • 长期存活的对象将进入老年代

可通过参数 -XX:MaxTenuringThreshold设置年龄阈值,默认15

  • 动态对象年龄判定

如果在survivor区相同年龄的所有对象大小超过空间的一半,不必等到MaxTenuringThreshold中要求的年龄,超过或等于该年龄的对象直接进入老年代

  • 空间分配担保

Minor GC 时,Survivor空间内存不足,无法容纳的对象将直接进入老年代,需确保老年代的剩余空间大于晋升对象容量经验值(每次回收晋升到老年代对象容量的平均大小值),否则进行Full GC来让老年代腾出更多空间

【Java多线程】JUC锁 04. AQS

发表于 2018-07-14 | 分类于 Java多线程 , JUC锁

AbstractQueuedSynchronizer

1. 前言

  • AQS是java中管理“锁”的抽象类,依赖于FIFO等待队列,锁的许多公共方法都是在这个类中实现
  • AQS锁的类别 – 分为“独占锁”和“共享锁”两种。
    • 独占锁 – 锁在一个时间点只能被一个线程锁占有。根据锁的获取机制,它又划分为“公平锁”和“非公平锁”。公平锁,是按照通过CLH等待线程按照先来先得的规则,公平的获取锁;而非公平锁,则当线程要获取锁时,它会无视CLH等待队列而直接获取锁。独占锁的典型实例子是ReentrantLock,此外,ReentrantReadWriteLock.WriteLock也是独占锁。
    • 共享锁 – 能被多个线程同时拥有,能被共享的锁。JUC包中的ReentrantReadWriteLock.ReadLock,CyclicBarrier, CountDownLatch和Semaphore都是共享锁。
  • 使用AQS作为基础同步器,需要通过更改State状态重写tryAcquire, tryRelease, tryAcquireShared, tryReleaseShared, isHeldExclusively这些方法

2. 源码解析

2.1 数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
// 版本号
private static final long serialVersionUID = 7373984972572414691L;
// 头结点
private transient volatile Node head;
// 尾结点
private transient volatile Node tail;
// 状态
private volatile int state;
// 自旋时间
static final long spinForTimeoutThreshold = 1000L;
}

AQS基于FIFO队列,底层数据结构为双向链表,依据head,tail指针来调度锁的获取和释放。Condition queue 是单向链表,不是必须的,当使用Condition才有此单向链表;可能有多个Condition queue

2.2 内部类

  • Node类
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
54
55
56
57
58
59
static final class Node {
// 模式,分为共享与独占
// 共享模式
static final Node SHARED = new Node();
// 独占模式
static final Node EXCLUSIVE = null;
// 结点状态
// CANCELLED,值为1,表示当前的线程被取消
// SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark
// CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中
// PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行
// 值为0,表示当前节点在sync队列中,等待着获取锁
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;

// 结点状态
volatile int waitStatus;
// 前驱结点
volatile Node prev;
// 后继结点
volatile Node next;
// 结点所对应的线程
volatile Thread thread;
// 通过nextWaiter来区分线程是“独占锁”线程还是“共享锁”线程。如果是“独占锁”线程,则nextWaiter的值为EXCLUSIVE;如果是“共享锁”线程,则nextWaiter的值是SHARED。
Node nextWaiter;

// 结点是否在共享模式下等待
final boolean isShared() {
return nextWaiter == SHARED;
}

// 获取前驱结点,若前驱结点为空,抛出异常
final Node predecessor() throws NullPointerException {
// 保存前驱结点
Node p = prev;
if (p == null) // 前驱结点为空,抛出异常
throw new NullPointerException();
else // 前驱结点不为空,返回
return p;
}

// 无参构造函数
Node() { // Used to establish initial head or SHARED marker
}

// 构造函数
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}

// 构造函数
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
  • ConditionObject
1
2
3
4
5
6
7
8
9
10
11
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
/** Mode meaning to reinterrupt on exit from wait */
private static final int REINTERRUPT = 1; //中断模式 - 可重新中断
/** Mode meaning to throw InterruptedException on exit from wait */
private static final int THROW_IE = -1; //中断模式 - 抛出异常
}

2.3 独占锁和共享锁

2.3.1 独占锁

具体见ReentrantLock获取锁流程

  • acquire() 获取锁

独占模式获取锁,忽略中断

1
2
3
4
5
6
// 获取锁
public final void acquire(int arg) {
if (!tryAcquire(arg) && //尝试获取
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //加入CLH队列,在队列中阻塞获取
selfInterrupt();
}
  • addWaiter()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 加入等待队列尾节点
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 自旋加入CLH末尾,若CLH为空,则新建一个CLH表头
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

其中,compareAndSetHead和compareAndSetTail 通过CAS方法操作当前线程

1
2
3
4
5
6
7
8
//实际调用的UnSafe类
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
  • acquireQueued()

acquireQueued()的作用就是“当前线程”会根据公平性原则进行阻塞等待,直到获取锁为止;并且返回当前线程在等待过程中有没有并中断过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//获取锁
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取上一个等待的线程
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { //前继节点是否为头节点,公平性原则
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 判断“当前线程是否应该阻塞”
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 如果前继节点是SIGNAL状态,则意味这当前线程需要被unpark唤醒。此时,返回true。
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
// 状态为CANCELLED = 1,则设置 “当前节点”的 “当前前继节点” 为 “‘原前继节点’的前继节点”。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 状态为“0”或者“共享锁-3”状态,则设置前继节点为SIGNAL状态。为-2 则在condition queue中
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
1
2
3
4
5
6
private final boolean parkAndCheckInterrupt() {
// 通过LockSupport的park()阻塞“当前线程”。
LockSupport.park(this);
// 返回线程的中断状态。
return Thread.interrupted();
}

线程被阻塞之后唤醒,一般有2种情况:

  1. unpark()唤醒。“前继节点对应的线程”使用完锁之后,通过unpark()方式唤醒当前线程。
  2. 中断唤醒。其它线程通过interrupt()中断当前线程。
  • cancelAcquire()
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
// 取消继续获取(资源)
private void cancelAcquire(Node node) {
if (node == null)
return;
// 设置node结点的thread为空
node.thread = null;

// 保存node的前驱结点
Node pred = node.prev;
while (pred.waitStatus > 0) // 找到node前驱结点中第一个状态小于0的结点,即不为CANCELLED状态的结点
node.prev = pred = pred.prev;

// 获取pred结点的下一个结点
Node predNext = pred.next;

// 设置node结点的状态为CANCELLED
node.waitStatus = Node.CANCELLED;

// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) { // node结点为尾结点,则设置尾结点为pred结点
// 比较并设置pred结点的next节点为null
compareAndSetNext(pred, predNext, null);
} else { // node结点不为尾结点,或者比较设置不成功
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) { // (pred结点不为头结点,并且pred结点的状态为SIGNAL)或者 pred结点状态小于等于0,并且比较并设置等待状态为SIGNAL成功,并且pred结点所封装的线程不为空
// 保存结点的后继
Node next = node.next;
if (next != null && next.waitStatus <= 0) // 后继不为空并且后继的状态小于等于0
compareAndSetNext(pred, predNext, next); // 比较并设置pred.next = next;
} else {
unparkSuccessor(node); // 释放node的前一个结点
}
node.next = node; // help GC
}
}
  • release() 释放锁
1
2
3
4
5
6
7
8
9
10
//尝试释放当前线程锁持有的锁。成功的话,则唤醒后继等待线程,并返回true。否则,直接返回false。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//唤醒当前线程的后继线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0) //设置状态为0, 可获取锁
compareAndSetWaitStatus(node, ws, 0);
//获取当前节点的“有效的后继节点”,无效的话,则通过for循环进行获取。
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}

2.3.2 共享锁

具体见ReentrantReadWriteLock.ReadLock

  • acquireShared() 获取共享锁
1
2
3
4
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) //尝试获取共享锁
doAcquireShared(arg);
}
  • doAcquireShared()

doAcquireShared()的作用是在CLH队列中自旋获取共享锁。doAcquireShared()在每一次尝试获取锁时,是通过tryAcquireShared()来执行的!

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
private void doAcquireShared(int arg) {
// 创建“当前线程”对应的Shared节点,并将该线程添加到CLH队列中。
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取“node”的前一节点
final Node p = node.predecessor();
// 如果“当前线程”是CLH队列的表头,则尝试获取共享锁。
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 如果“当前线程”不是CLH队列的表头,则通过shouldParkAfterFailedAcquire()判断是否需要等待,
// 需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。若阻塞等待过程中,线程被中断过,则设置interrupted为true。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
  • releaseShared()释放锁
1
2
3
4
5
6
7
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { //尝试释放锁,在子类实现
doReleaseShared();
return true;
}
return false;
}
  • doAcquireShared

doAcquireShared()的作用是在CLH队列中自旋获取共享锁。doAcquireShared()在每一次尝试获取锁时,是通过tryAcquireShared()来执行的!

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
private void doAcquireShared(int arg) {
// 创建“当前线程”对应的Shared节点,并将该线程添加到CLH队列中。
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取“node”的前一节点
final Node p = node.predecessor();
// 如果“当前线程”是CLH队列的表头,则尝试获取共享锁。
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 如果“当前线程”不是CLH队列的表头,则通过shouldParkAfterFailedAcquire()判断是否需要等待,
// 需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。若阻塞等待过程中,线程被中断过,则设置interrupted为true。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

3. 参考

http://www.cnblogs.com/leesf456/p/5350186.html

【Java多线程】JUC锁 03. LockSupport

发表于 2018-07-08 | 分类于 Java多线程 , JUC锁

LockSupport

  • 基本线程阻塞原语
  • LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。 因为park() 和 unpark()有许可的存在;调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性。

1. 源码解读

1
private LockSupport() {}	 //私有构造函数,无法被实例化

1.1 成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static final sun.misc.Unsafe UNSAFE;	//UnSafe对象
private static final long parkBlockerOffset; //获取内存偏移地址,记录线程被谁阻塞
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
parkBlockerOffset = UNSAFE.objectFieldOffset
(tk.getDeclaredField("parkBlocker"));
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception ex) { throw new Error(ex); }
}

1.2 核心方法

1.2.1 park()

三种形式的park()还各支持一个blocker对象参数,即有没有设置线程的parkBlocker字段

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
//阻塞当前线程,在阻塞当前线程的时候做了记录当前线程等待的对象操作
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null); //可运行后清除blocker
}

//阻塞当前线程,最长等待时间不超过nanos毫秒,在阻塞当前线程的时候做了记录当前线程等待的对象操作
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, nanos);
setBlocker(t, null);
}
}

//阻塞当前线程直到deadline时间,也做了阻塞前记录当前线程等待对象的操作
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(true, deadline);
setBlocker(t, null);
}

线程可重新运行的情况:

  • 其他某个线程将当前线程作为目标调用 unpark
  • 其他某个线程中断当前线程 interrupt()
  • 该调用不合逻辑地(即毫无理由地)返回

wait() 和 park() 的区别在于:

  • wait() 阻塞线程前,必须通过synchronized获取同步锁
  • 更加灵活。使用park/unpark,不会造成由wait/notify调用顺序不当所引起的阻塞

1.2.2 unpark(Thread thread)

1
2
3
4
5
//解除阻塞线程
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}

2. 参考资料

【JUC】JDK1.8源码分析之LockSupport(一)

Java多线程系列–“JUC锁”07之 LockSupport

【Java多线程】JUC锁 02. UnSafe

发表于 2018-07-07 | 分类于 Java多线程 , JUC锁

Unsafe

java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作

1. Unsafe调用

Unsafe类是一个单例,调用的方法为getUnsafe。

getUnsafe()内部会检查该CallerClass是不是由系统类加载器BootstrapClassLoader加载。

由系统类加载器加载的类调用getClassLoader()会返回null,所以要检查类是否为bootstrap加载器加载只需要检查该方法是不是返回null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class Unsafe {
private static final Unsafe theUnsafe;

private Unsafe() {
}

@CallerSensitive
public static Unsafe getUnsafe() { //Unsafe类是个单例
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
}

调用Unsafe方法:

  1. 通过JVM参数-Xbootclasspath指定要使用的类为启动类;
  2. 在Unsafe类中有一个成员变量theUnsafe,因此我们可以通过反射将private单例实例的accessible设置为true,然后通过Field的get方法获取
1
2
3
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

可以通过allocateInstance方法在未调用构造方法的情况下生成对象

1
public native Object allocateInstance(Class<?> var1) throws InstantiationException;

2. 内存操作

1
2
3
public native long allocateMemory(long l);	//分配内存
public native long reallocateMemory(long l, long l1); //扩充内存
public native void freeMemory(long l); //释放内存

3. 对象某字段内存值操作

1
2
3
4
5
6
7
8
9
10
11
12
public native long staticFieldOffset(Field var1);	//对给定Field的定位,返回Field的内存地址偏移量
public native long getLong(long var1); //获取指定offset偏移地址对应的long型Field的值
public native int getIntVolatile(Object var1, long var2); //获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。

public native int arrayBaseOffset(Class<?> var1);
public native int arrayIndexScale(Class<?> var1);

static
{
ARRAY_INT_BASE_OFFSET = theUnsafe.arrayBaseOffset(int[].class);
ARRAY_INT_INDEX_SCALE = theUnsafe.arrayIndexScale(int[].class);
}

*_BASE_OFFSET常量: 通过arrayBaseOffset本地方法实现,返回数组中第一个元素的偏移地址

*_INDEX_SCALE常量: 通过arrayIndexScale 本地方法实现,获取数组的转换因子,也就是数组中元素的增量地址

通过二者结合,可定位数组中每个元素在内存中的位置

4. 线程挂起和恢复

1
2
public native void unpark(Object var1);		//终止一个挂起的线程,使其恢复正常
public native void park(boolean var1, long var2); //线程挂起

封装在LockSupport 类中

5. CAS操作

1
2
3
4
5
6
7
8
9
10
/**
* 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
*
* @param obj 需要更新的对象
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值
* @return 如果field的值被更改返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。

6. 参考资料

JAVA并发编程学习笔记之Unsafe类

【JVM】java内存区域

发表于 2018-07-05 | 分类于 JVM

Java内存区域

程序计数器

  • 当前线程所执行字节码的行号指示器
  • 线程私有,保证线程切换能恢复到正确的执行位置
  • 线程正在执行Java方法,则记录的是正在执行的虚拟机字节码指令的地址; Native方法,则为空(undefined)
  • 此内存区域是唯一一个没有规定OutOfMemoryError的区域

Java虚拟机栈

  • 线程私有,生命周期同线程一样

  • 描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧来存储局部变量表,操作数栈、动态链接、方法出口等信息

  • 局部变量表存放编译期可知的各种基本数据类型、对象引用类型(reference类型)和returnAddress类型(指向一条字节码指令的地址)

  • 栈上分配技术,对线程私有的对象可以分配在栈上,不需要GC介入,提高性能

  • 通过 -Xss参数设置栈大小

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

    如果虚拟机栈可以动态扩展、扩展时候无法申请到足够的内存,将抛出OutOfMemoryError异常

本地方法栈

为虚拟机使用到的native方法服务

Java堆

  • Java内存管理中最大的一块,虚拟机启动时创建
  • 主要存储对象实例和数组
  • 从内存回收角度可分为新生代和老年代,新生代可分为Eden区,From Survivor区, To Survivor 区
  • 通过 -Xmx 和 - Xms设置堆空间
  • 如果堆中没有内存完成实例分配,且堆也无法再扩展。将抛出OutOfMemoryError

方法区(永久代)

  • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 永久代是HotSpot虚拟机特有的概念,是对方法区的实现,别的JVM没有永久代的概念
  • 通过 -XX: permSize 和 -XX:MaxPermSize设置大小
  • 在JDK8中,JDK8的HotSpot VM已经是以前的HotSpot VM与JRockit VM的合并版,永久代替换为元空间(避免永久代OOM: PermGen问题,同时整合JRockit)
  • 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError

元空间

  • 元空间是一块与堆不相连的本地内存

  • 永久代中原来存储的字符串常量(池)、符号引用(这两个在jdk7普遍就已经将其放在堆上了)和类的静态变量现在存储在java堆中,其余的数据(类信息,JIT编译后的代码)作为元数据存储在元空间中

  • 默认情况下,类元数据只受可用的本地内存限制,新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小

  • 元数据: 描述数据的数据

    元数据可以为数据说明其元素或属性(名称、大小、数据类型、等),或其结构(长度、字段、数据列),或其相关数据(位于何处、如何联系、拥有者)

运行时常量池

  • 方法区的一部分,用于存放编译期生成的各种字面量和符号引用(保存Class文件中描述的符号引用、翻译出来的直接引用)
  • 动态性,运行期间也可将新的常量放入池中
  • 常量池无法再申请到内存将抛出OutOfMemoryError

直接内存

  • 不是虚拟机规范中定义的内存区域
  • NIO中,使用Native函数库分配堆外内存,通过存储在Java堆中的directByBuffer对象作为这块内存的引用进行操作
  • 动态扩展时,各内存区域超过物理内存限制,导致OOM
12345
JumpsZ

JumpsZ

42 日志
10 分类
8 标签
© 2018 JumpsZ
All rights reserved
|
本站访客数: