JVM课程五

垃圾回收算法和收集器

算法

引用计数(reference counting)

古老的算法。此对象有一个引用,增加一个计数,删除一个引用减少一个引用。垃圾回收时,只用收集为0的对象。
致命问题是无法解决循环引用的问题。

复制(copying)

eg:survivor from to
此算法将内存空间分为两份。每次使用其中一个区域。(占用空间)
垃圾回收时,遍历当前使用的区域,把正在使用中的对象复制到另一个区域中。

标记清楚(Mark-Sweep)

执行分为两阶段。
第一阶段:从对象根节点开始标记所有被引用的对象。
第二阶段:遍历整个堆,把未标记的对象清除,此算法需要暂停整个应用(Stop the world)。
同时会产生内存碎片。
缺点:
1)未使用的内存空间不连续,浪费。
2)暂停整个应用。

标记整理(Mark-Compact)

执行分为两阶段。
第一阶段:从对象根节点开始标记所有被引用的对象。
第二阶段:遍历整个堆,把未标记的对象清除,同时把存活的对象压缩到一块,顺序排放,避免碎片问题。
避免了复制算法的空间问题。

JVM课程四

JVM分代策略

新生代

三个区的默认存放空间是Eden : From : To = 8: 1: 1

目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用空间。

新生成的对象优先存放在新生代中,在新生代中,常规应用采取一次垃圾收集可以回收70%-90%。

新生成的对象在Eden区中分配(大对象除外,直接老年代)。

当Eden没有足够空间,虚拟机发起一次Minor GC。(只会收集一次Eden区)

GC开始时,对象只会存在于Eden区和From Suvivor区,To Survivor为空。用于复制

GC进行时,Eden区中所有存活的对象会被复制到To Survivor(连续存储),
而在From Survivor区中仍存活的对象,会根据年龄决定去向。
阈值为15,每熬过一次GC,年龄就加1,GC分代年龄值存在对象的header中,
大于15,移至老年代中,没有达到15的复制到To Survivor。
之后,From和To交换角色。From被清空,变成下一次GC的To区, 而To区成为下一次GC的From区。

无论如何,保证To区域在下一轮GC中为空的。

note: 当TO区域用于复制From中的对象时,出现空间不足的时候,需要依赖老年代分配内存进行存放。

老年代

在新生代中经历了多次的GC后存活下来的对象会进入老年代。
老年代的对象生命周期较长,存活率比较高,在老年代中进行GC的频率较低,效率也较低。

永久代

永久代中存放类信息,常量,静态变量,即时编译器编译后的代码等数据。
对此区域,Java虚拟机规范指出可以不进行垃圾收集。一般而言不会进行垃圾回收。

JVM课程二

类加载器子系统和方法区:

从文件系统中加载class文件,加载类信息。加载的信息存放于一个叫做方法区的内存空间。

方法区中包括: 类信息,常量池(字符串字面量和数字常量)

堆:

主要内存空间。存放对象的实例。线程共享

直接内存:

NIO允许Java程序直接访问直接内存。

不受JVM内存 -Xmx 最大堆大小限制。不属于堆内存。直接内存速率高于堆内存。

读写频繁的场合可能会用到直接内存,与系统内存挂钩。

垃圾回收系统

回收java堆,方法区,直接内存都可以回收。java所有的对象的释放都是隐式的。
垃圾回收系统后台默默运行,标示和释放对象,实现全自动化管理。

虚拟机栈

每一个虚拟机线程都有一个私有的栈,栈在线程创建的时候被创建。栈保存着帧信息。

栈中保存方法中的局部变量,方法参数,和程序的调用,返回相关。

本地方法栈

本地方法栈和虚拟机栈类似,虚拟机栈用于方法的调用。本地方法栈则用于本地方法(Native API)的调用。

Java虚拟机的重要拓展之一,不同的操作系统本地方法的API也不一样。

程序计数器

线程私有,字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,
分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。

JVM课程三

JVM内存分代策略

堆内存一般分为几块:新生代,老年代和永久代。是垃圾回收最频繁的区域。

分代的意义?

给堆内存分代为了提高对象分配内存和垃圾回收的效率。

如果堆内存没有划分区域,所有新创建的对象需要和生命周期很长的对象放在一起,(内存地址是连续的)。
随着程序的运行,堆内存需要频繁的进行垃圾收集,每次回收都需要遍历所有的对象,遍历花费的时间代价过大,
严重影响GC效率。

  • 新生代: 新创建的对象 频繁GC
  • 老年代: 经过多次回收仍存活下来的对象 (GC频率低)
  • 永久代: 静态属性、类信息等 (一般不回收)

根据不同的区域(代),采用不同的垃圾回收算法。分代收集大大提升了收集效率。

永久代是HotSpot中的概念。

    young             old         permanent
      |
Eden   From   To
伊甸园  |     |
           Survivor Spaces(幸存空间)

`

JVM课程一

JDK 编译Java文件为Class文件
Class文件由JVM解释为Native Code

Java HotSpot:
热点代码,(类似于缓存)避免jvm频繁将class文件转换为cpu可以直接理解的代码(native code 本地代码)。

Client VM 是默认启用的版本,更适合C/S 在为客户端环境中启动减少启动时间而优化
Server VM 在内存分配上更大,更适合于B/S 在为服务端环境中最大化程序执行速度而优化

-client KNOWN
-server KNOWN

ConcurrentHashMap线程安全的具体实现方式/底层具体实现

ConcurrentHashMap线程安全的具体实现方式/底层具体实现

JDK1.7的ConcurrentHashMap:

JDK1.7的ConcurrentHashMap

JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

JDK1.8的ConcurrentHashMap

JDK1.7(上面有示意图)
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

JDK1.8 (上面有示意图)
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

Map中的hash理解

Map中的hash理解

哈希(散列)

Hash,一般翻译做“散列”,也有直接音译为“哈希”的。

就是把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间。
不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

所有散列函数都有如下一个基本特性:

根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。
但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。

碰撞

输入不同,散列值相同的现象。

常见的Hash函数有以下几个

直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。

数字分析法:提取关键字中取值比较均匀的数字作为哈希地址。

除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。

分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。

平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。

伪随机数法:采用一个伪随机数当作哈希函数。

衡量一个哈希函数的好坏的重要指标就是发生碰撞的概率以及发生碰撞的解决方案。

常见的解决碰撞的方法有以下几种:

开放定址法:

开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

链地址法:

将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。

再哈希法:

当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。

建立公共溢出区:

将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。

HashMap 和 HashTable的区别

HashMap 和 HashTable的区别

区别:

1)线程是否安全:

HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);

2)效率:

因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;

3)对Null key 和Null value的支持:

HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。
但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。

4)初始容量大小和每次扩充容量大小的不同 :

①Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。
HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。

②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小。
而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证)。8 16 32 64 …
也就是说 HashMap 总是使用2的幂作为哈希表的大小。

5) 底层数据结构:

JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

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
HasMap 中带有初始容量的构造函数:

public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。

static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

HashMap 和 HashSet区别

如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。

HashMap HashSet
实现了Map接口 实现Set接口
存储键值对 仅存储对象
调用 put()向map中添加元素 调用 add()方法向Set中添加元素

Java容器(二)

Java容器(二)

ArrayList扩容机制(1.5倍数)

构造函数(三种)

以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。

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
/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10;


private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
*默认构造函数,使用初始容量10构造一个空列表(无参数构造)
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
* 带初始容量参数的构造函数。(用户自己指定容量)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//初始容量大于0
//创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {//初始容量等于0
//创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {//初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}


/**
*构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
*如果指定的集合为null,throws NullPointerException。
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}

add方法添加元素

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
   /**
* 将指定的元素追加到此列表的末尾。
*/
public boolean add(E e) {
//添加元素之前,先调用ensureCapacityInternal方法
ensureCapacityInternal(size + 1); // Increments modCount!!
//这里看到ArrayList添加元素的实质就相当于为数组赋值
elementData[size++] = e;
return true;
}

/**
* 得到最小扩容量
*/
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}

/**
* 判断是否需要扩容
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}

grow 扩容方法

扩容核心函数:

1
2
3
4
5
6
7
8
9
10
11
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //位运算 1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

System.arraycopy() 和 Arrays.copyOf()方法

阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)、toArray() 等方法中都用到了该方法!

System.arraycopy() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 在此列表中的指定位置插入指定的元素。
* 先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
* 再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
*/
public void add(int index, E element) {
rangeCheckForAdd(index);

ensureCapacityInternal(size + 1); // Increments modCount!!
//arraycopy()方法实现数组自己复制自己
//elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}

Arrays.copyOf()方法

1
2
3
4
5
6
7
/**
以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。
*/
public Object[] toArray() {
//elementData:要复制的数组;size:要复制的长度
return Arrays.copyOf(elementData, size);
}

区别和联系

联系:

看两者源代码可以发现 copyOf() 内部实际调用了 System.arraycopy() 方法

区别:

arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组。

ensureCapacity方法

ArrayList 源码中有一个 ensureCapacity 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
*
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;

if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}

最好在 add 大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数

HashMap 的长度为什么是2的幂次方?

HashMap 的长度为什么是2的幂次方?

原因

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,
Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,
一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。
用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。

这个算法应该如何设计呢?

我们首先可能会想到采用%取余的操作来实现。重点来了:
“取余(%)操作中如果除数是2的幂次则等价于
与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。”
并且采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

通俗版本

Hash值取值范围很大40亿,不可能用这么大的数组存Hash表。所以想到取模,将Hash值局限在一个小的数组中。
针对取模运算,有公式: hash % length = hash & (length - 1) 能够显著提高取模的效率问题。
前提是 length 必须是2的幂次方。可以解释这就解释了 HashMap 的长度为什么是2的幂次方。

HashMap的底层实现

JDK1.8 之前 HashMap底层是数组和链表 结合在一起使用也就是链表散列。
1)HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,
2)然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),
3)如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,
不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法。
换句话说使用扰动函数之后可以减少碰撞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1.8

static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

1.7:
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).

h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

jdk1.8之前的内部结构-HashMap

JDK1.8之后

相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

jdk1.8之后的内部结构-HashMap

TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。