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
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
堆内存一般分为几块:新生代,老年代和永久代。是垃圾回收最频繁的区域。
给堆内存分代为了提高对象分配内存和垃圾回收的效率。
如果堆内存没有划分区域,所有新创建的对象需要和生命周期很长的对象放在一起,(内存地址是连续的)。
随着程序的运行,堆内存需要频繁的进行垃圾收集,每次回收都需要遍历所有的对象,遍历花费的时间代价过大,
严重影响GC效率。
根据不同的区域(代),采用不同的垃圾回收算法。分代收集大大提升了收集效率。
永久代是HotSpot中的概念。
young old permanent
|
Eden From To
伊甸园 | |
Survivor Spaces(幸存空间)
`
Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.
1 | $ hexo new "My New Post" |
More info: Writing
1 | $ hexo server |
More info: Server
1 | $ hexo generate |
More info: Generating
1 | $ hexo deploy |
More info: Deployment
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
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倍。
为了能让 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的幂次方。
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
211.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之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
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 | HasMap 中带有初始容量的构造函数: |
如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
HashMap | HashSet |
---|---|
实现了Map接口 | 实现Set接口 |
存储键值对 | 仅存储对象 |
调用 put() 向map中添加元素 |
调用 add() 方法向Set中添加元素 |
Hash,一般翻译做“散列”,也有直接音译为“哈希”的。
就是把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间。
不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
所有散列函数都有如下一个基本特性:
根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。
但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。
输入不同,散列值相同的现象。
直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。
数字分析法:提取关键字中取值比较均匀的数字作为哈希地址。
除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。
分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。
平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。
伪随机数法:采用一个伪随机数当作哈希函数。
衡量一个哈希函数的好坏的重要指标就是发生碰撞的概率以及发生碰撞的解决方案。
常见的解决碰撞的方法有以下几种:
开放定址法:
开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
链地址法:
将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
再哈希法:
当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。
建立公共溢出区:
将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。
以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。
1 | /** |
1 | /** |
扩容核心函数:1
2
3
4
5
6
7
8
9
10
11private 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);
}
阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)、toArray() 等方法中都用到了该方法!
1 | /** |
1 | /** |
联系:
看两者源代码可以发现 copyOf() 内部实际调用了 System.arraycopy() 方法
区别:
arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组。
ArrayList 源码中有一个 ensureCapacity 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢?
1 | /** |
最好在 add 大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数
ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构 (JDK1.6还是双向循环链表)
Arraylist插入和删除受元素所在的位置的影响。
① ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。
比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。
但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
② LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
基于数组结构的ArrayList是可以的。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
LinkedList 不支持高效的随机元素访问。
ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
1 | public interface RandomAccess { |
查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!
List遍历1
2实现了 RandomAccess 接口的list,优先选择普通 for 循环 ,其次 foreach,
未实现 RandomAccess 接口的list,优先选择iterator遍历(foreach遍历底层也是通过iterator实现的,),大size的数据,千万不要使用普通for循环
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
Arraylist不是同步的,所以在不需要保证线程安全时建议使用Arraylist。
##问题1:HashM安排的初始长度,为什么?
初始长度是 16,每次扩展或者是手动初始化,长度必须是 2的幂。
因为: index = HashCode(Key) & (length - 1), 如果 length是 2的 幂的话,则 length - 1就是 全是 1的二进制数,比如 16 - 1 = 1111,这样相当于是 坐落在长度为 length的hashMap上的位置只和 HashCode的后四位有关,这只要给出的HashCode算法本身分布均匀,算出的index就是分布均匀的。
因为HashMap的key是int类型,所以最大值是2^31次方,但是查看源码,当到达 2^30次方,即
MAXIMUM_CAPACITY,之后,便不再进行扩容。
我们看到默认HashMap的初始长度是16,比较小,每一次push的时候,都会检查当前容量是否超过 预定的 threshold,如果超过,扩大HashMap容量一倍,整个表里的所有元素都需要按照新的hash算法被算一遍,这个代价较大。提到死锁,对于 HashMap来说,貌似只能和链表操作有关。
正常ReHash过程,可以看到,每个元素重新算hash值,将链表翻转(目的遍历每个bucket上的链表还是用的是头插法,时间复杂度最低),放到对应的bucket上的链表中
简单说: java7中 hashMap每个桶中放置的是链表,这样当hash碰撞严重时,会导致个别位置链表长度过长,从而影响性能。
java8中,HashMap 每个桶中当链表长度超过8之后,会将链表转换成红黑树,从而提升增删改查的速度。