面试发现常常有些重复的面试问题,本身也应该学会记录下来,最好本身能作成笔记,在下一次面的时候说得有条不紊,深刻具体,面试官想必也很开心。如下是我我的总结,请参考:html
HashSet底层原理:(问了大概率跟HashMap一块儿面)java
HashMap底层原理:(很是大概率问到)linux
Hashtable底层原理:(问的少,问了大概率问你跟HashMap的区别)程序员
synchronized底层如何实现?锁优化,怎么优化?面试
ReentrantLock 底层实现;算法
ConcurrentHashMap 的工做原理,底层原理(谈到多线程高并发大概率会问它)spring
JVM调优(JVM层层渐进问时大概率问)编程
JVM内存管理,JVM的常见的垃圾收集器,GC调优,Minor GC ,Full GC 触发条件(像是必考题)数组
java内存模型浏览器
线程池的工做原理(谈到多线程高并发大概率会问它)
ThreadLocal的底层原理(有时问)
voliate底层原理
NIO底层原理
IOC底层实现原理(Spring IOC ,AOP会问的两个原理,面试官常常会问看过源码吗?因此你有所准备吧)
AOP底层实现原理
MyisAM和innodb的有关索引的疑问(容易混淆,能够问的会深刻)
HashSet底层原理:(面试过)
http://zhangshixi.iteye.com/blog/673143
http://www.javashuo.com/article/p-memupmkd-bc.html
HashSet实现Set接口,由哈希表(其实是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。此类容许使用null元素。
2. HashSet的实现:
对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存全部元素,所以HashSet 的实现比较简单,相关HashSet的操做,基本上都是直接调用底层HashMap的相关方法来完成, (实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。)
HashSet的源代码
对于HashSet中保存的对象,请注意正确重写其equals和hashCode方法,以保证放入的对象的惟一性。
插入
当有新值加入时,底层的HashMap会判断Key值是否存在(HashMap细节请移步深刻理解HashMap),若是不存在,则插入新值,同时这个插入的细节会依照HashMap插入细节;若是存在就不插入
HashMap底层原理:
1. HashMap概述:
HashMap是基于哈希表的Map接口的非同步实现。此实现提供全部可选的映射操做,并容许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2. HashMap的数据结构:
HashMap其实是一个“数组+链表+红黑树”的数据结构
3. HashMap的存取实现:
(1.8以前的)
当咱们往HashMap中put元素的时候,先根据key的hashCode从新计算hash值,根据hash值获得这个元素在数组中的位置(即下标),若是数组该位置上已经存放有其余元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最早加入的放在链尾。若是数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
1.8:
put():
根据key计算获得key.hash = (h = k.hashCode()) ^ (h >>> 16);
根据key.hash计算获得桶数组的索引index = key.hash & (table.length - 1),这样就找到该key的存放位置了:
① 若是该位置没有数据,用该数据新生成一个节点保存新数据,返回null;
② 若是该位置有数据是一个红黑树,那么执行相应的插入 / 更新操做
③ 若是该位置有数据是一个链表,分两种状况一是该链表没有这个节点,另外一个是该链表上有这个节点,注意这里判断的依据是key.hash是否同样: 若是该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null; 若是该链表已经有这个节点了,那么找到該节点并更新新数据,返回老数据。 注意: HashMap的put会返回key的上一次保存的数据。
get():
计算需获取数据的hash值(计算过程跟put同样),计算存放在数组table中的位置(计算过程跟put同样),而后依次在数组,红黑树,链表中查找(经过equals()判断),最后再判断获取的数据是否为空,若为空返回null不然返回该数据
树化与还原
哈希表的最小树形化容量
当哈希表中的容量大于这个值时(64),表中的桶才能进行树形化
不然桶内元素太多时会扩容,而不是树形化
为了不进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
一个桶的树化阈值
当桶中元素个数超过这个值时(8),须要使用红黑树节点替换链表节点
这个值必须为 8,要否则频繁转换效率也不高
一个树的链表还原阈值
当扩容时,桶中元素个数小于这个值(6),就会把树形的桶元素 还原(切分)为链表结构
这个值应该比上面那个小,至少为 6,避免频繁转换
条件1. 若是当前桶数组为null或者桶数组的长度 < MIN_TREEIFY_CAPACITY(64),则进行扩容处理(见代码片断2:resize());
条件2. 当不知足条件1的时候则将桶中链表内的元素转换成红黑树!!!稍后再详细讨论红黑树。
扩容机制的实现
扩容(resize)就是从新计算容量。当向HashMap对象里不停的添加元素,而HashMap对象内部的桶数组没法装载更多的元素时,HashMap对象就须要扩大桶数组的长度,以便能装入更多的元素。
capacity 就是数组的长度/大小,loadFactor 是这个数组填满程度的最大比比例。
size表示当前HashMap中已经储存的Node<key,value>的数量,包括桶数组和链表 / 红黑树中的的Node<key,value>。
threshold表示扩容的临界值,若是size大于这个值,则必需调用resize()方法进行扩容。
在jdk1.7及之前,threshold = capacity * loadFactor,其中 capacity 为桶数组的长度。 这里须要说明一点,默认负载因子0.75是是对空间和时间(纵向横向)效率的一个平衡选择,建议你们不要修改。 jdk1.8对threshold值进行了改进,经过一系列位移操做算法最后获得一个power of two size的值
何时扩容
当向容器添加元素的时候,会判断当前容器的元素个数,若是大于等于阈值---即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。
扩容必须知足两个条件:
一、 存放新值的时候 当前已有元素的个数 (size) 必须大于等于阈值
二、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)
//若是计算的哈希位置有值(及hash冲突),且key值同样,则覆盖原值value,并返回原值value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
resize()方法: 该函数有2种使用状况1.初始化哈希表 2.当前数组容量太小,需扩容
过程:
插入键值对时发现容量不足,调用resize()方法方法,
1.首先进行异常状况的判断,如是否须要初始化,二是若当前容量》最大值则不扩容,
2.而后根据新容量(是就容量的2倍)新建数组,将旧数组上的数据(键值对)转移到新的数组中,这里包括:(遍历旧数组的每一个元素,从新计算每一个数据在数组中的存放位置(原位置或者原位置+旧容量),将旧数组上的每一个数据逐个转移到新数组中,这里采用的是尾插法。)
3.新数组table引用到HashMap的table属性上
4.最后从新设置扩容阙值,此时哈希表table=扩容后(2倍)&转移了旧数据的新table
synchronized底层如何实现?锁优化,怎么优化?
synchronized 是 Java 内建的同步机制,因此也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其余试图获取的线程只能等待或者阻塞在那里。
原理:
synchronized能够保证方法或者代码块在运行时,同一时刻只有一个方法能够进入到临界区,同时它还能够保证共享变量的内存可见性
底层实现:
同步代码块是使用monitorenter和monitorexit指令实现的, ,当且一个monitor被持有以后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor全部权,即尝试获取对象的锁;
同步方法(在这看不出来须要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。 synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并无任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示 Klass 作为锁对象。
Java对象头和monitor是实现synchronized的基础!
synchronized存放的位置:
synchronized用的锁是存在Java对象头里的。
其中, Java对象头包括:
Mark Word(标记字段): 用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。它是实现轻量级锁和偏向锁的关键
Klass Pointer(类型指针): 是对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例
monitor: 能够把它理解为一个同步工具, 它一般被描述为一个对象。 是线程私有的数据结构
锁优化,怎么优化?
jdk1.6对锁的实现引入了大量的优化。 锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。 注意锁能够升级不可降级,这种策略是为了提升得到锁和释放锁的效率。 重量级锁降级发生于STW阶段,降级对象为仅仅能被VMThread访问而没有其余JavaThread访问的对象。( HotSpot JVM/JRockit JVM是支持锁降级的)
偏斜锁:
当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操做(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,因此并不涉及真正的互斥锁。
自旋锁:
自旋锁 for(;;)结合cas确保线程获取取锁
就是让该线程等待一段时间,不会被当即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无心义的循环便可(自旋)。
轻量级锁:
引入偏向锁主要目的是:为了在无多线程竞争的状况下尽可能减小没必要要的轻量级锁执行路径。 当关闭偏向锁功能或者多个线程竞争偏向锁致使偏向锁升级为轻量级锁,则会尝试获取轻量级锁
重量级锁:
重量级锁经过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操做系统的Mutex Lock实现,操做系统实现线程之间的切换须要从用户态到内核态的切换,切换成本很是高。
ReentrantLock 底层实现
https://blog.csdn.net/u011202334/article/details/73188404
AQS原理:
AQS和Condition各自维护了不一样的队列,在使用lock和condition的时候,其实就是两个队列的互相移动。若是咱们想自定义一个同步器,能够实现AQS。它提供了获取共享锁和互斥锁的方式,都是基于对state操做而言的。
概念+实现:
ReentrantLock实现了Lock接口,是AQS( 一个用来构建锁和同步工具的框架, AQS没有 锁之 类的概念)的一种。加锁和解锁都须要显式写出,注意必定要在适当时候unlock。ReentranLock这个是可重入的。其实要弄明白它为啥可重入的呢,咋实现的呢。其实它内部自定义了同步器Sync,这个又实现了AQS,同时又实现了AOS,然后者就提供了一种互斥锁持有的方式。其实就是每次获取锁的时候,看下当前维护的那个线程和当前请求的线程是否同样,同样就可重入了。
和synhronized相比:
synchronized相比,ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,二者是同样的,因此无特殊状况下,推荐使用synchronized。ReentrantLock的优点在于它更灵活、更强大,增长了轮训、超时、中断等高级功能。
可重入锁。可重入锁是指同一个线程能够屡次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
可中断锁。可中断锁是指线程尝试获取锁的过程当中,是否能够响应中断。synchronized是不可中断锁,而ReentrantLock则z,dz提供了中断功能。
公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则容许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,可是也能够设置为公平锁。
lock()和unlock()是怎么实现的呢?
由lock()和unlock的源码能够看到,它们只是分别调用了sync对象的lock()和release(1)方法。而 Sync是ReentrantLock的内部类, 其扩展了AbstractQueuedSynchronizer。
lock():
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
首先用一个CAS操做,判断state是不是0(表示当前锁未被占用),若是是0则把它置为1,而且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操做只能保证一个线程操做成功,剩下的只能乖乖的去排队啦。( “非公平”即体如今这里)。
设置state失败,走到了else里面。咱们往下看acquire。
2. 第二步,入队。( 自旋+CAS组合来实现非阻塞的原子操做)
3. 第三步,挂起。 让已经入队的线程尝试获取锁,若失败则会被挂起
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
unlock():
流程大体为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,若是是则唤醒头结点的下个节点关联的线程,
若是释放失败那么返回false表示解锁失败。这里咱们也发现了,每次都只唤起头结点的下一个节点关联的线程。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
ConcurrentHashMap 的工做原理
概念:
ConcurrentHashMap的目标是实现支持高并发、高吞吐量的线程安全的HashMap。
1.8以前:
数据结构:
ConcurrentHashMap是由Segment数组结构和 多个HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每一个HashEntry是一个链表结构的元素, 每一个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先得到它对应的Segment锁。
put和get的时候,都是现根据key.hashCode()算出放到哪一个Segment中: ConcurrentHashMap中默认是把segments初始化为长度为16的数组
http://www.javashuo.com/article/p-sfmdxgau-bp.html
1.8后:
变化:
ConcurrentHashMap的JDK8与JDK7版本的并发实现相比,最大的区别在于JDK8的锁粒度更细,理想状况下talbe数组元素的大小就是其支持并发的最大个数
实现:
改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素做为锁,从而实现了对每一行数据进行加锁,进一步减小并发冲突的几率。
数据结构:
改进二:将原先table数组+单向链表的数据结构,变动为table数组+单向链表+红黑树的结构。对于hash表来讲,最核心的能力在于将key hash以后能均匀的分布在数组中。
概念:
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操做,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,可是已经简化了属性,只是为了兼容旧版本。
树化和还原:
与HashMap同样 。
一些成员:
Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据。 ,就是一个链表,可是只容许对数据进行查找,不容许进行修改
经过TreeNode做为存储结构代替Node来转换成黑红树。
TreeBin
TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制
// 读写锁状态
static final int WRITER = 1; // 获取写锁的状态
static final int WAITER = 2; // 等待写锁的状态
static final int READER = 4; // 增长数据时读锁的状态
构造器
public ConcurrentHashMap() {
} 初始化实际上是一个空实现, 初始化操做并非在构造函数实现的,而是在put操做中实现。 还提供了其余的构造函数,有指定容量大小或者指定负载因子,跟HashMap同样。
存取实现:
put(): 对当前的table进行无条件自循环直到put成功
若是没有初始化就先调用initTable()方法来进行初始化过程
若是没有hash冲突就直接CAS插入
若是还在进行扩容操做就先进行扩容
若是存在hash冲突,就加锁来保证线程安全,这里有两种状况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
最后一个若是该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
若是添加成功就调用addCount()方法统计size,而且检查是否须要扩容。
get()
计算hash值,定位到该table索引位置,若是是首节点符合就返回
若是遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
以上都不符合的话,就往下遍历节点,匹配就返回,不然最后就返回null
归纳版:
(1)对于get读操做,若是当前节点有数据,还没迁移完成,此时不影响读,可以正常进行。
若是当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时get线程会帮助扩容。
(2)对于put/remove写操做,若是当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时写线程会帮助扩容,若是扩容没有完成,当前链表的头节点会被锁住,因此写线程会被阻塞,直到扩容完成。
扩容机制:https://www.e-learn.cn/content/java/1154828
引入了一个ForwardingNode类,在一个线程发起扩容的时候,就会改变sizeCtl这个值,
sizeCtl :默认为0,用来控制table的初始化和扩容操做,具体应用在后续会体现出来。
-1 表明table正在初始化
-N 表示有N-1个线程正在进行扩容操做 。
扩容时候会判断这个值,
若是超过阈值就要扩容,首先根据运算获得须要遍历的次数i,而后利用tabAt方法得到i位置的元素f,初始化一个forwardNode实例fwd,若是f == null,则在table中的i位置放入fwd,
不然采用头插法的方式把当前旧table数组的指定任务范围的数据给迁移到新的数组中,
而后
给旧table原位置赋值fwd。直到遍历过全部的节点之后就完成了复制工做,把table指向nextTable,并更新sizeCtl为新数组大小的0.75倍 ,扩容完成。在此期间若是其余线程的有读写操做都会判断head节点是否为forwardNode节点,若是是就帮助扩容。
Hashtable底层原理:
概念:
HashTable类继承自Dictionary类, 实现了Map接口。 大部分的操做都是经过synchronized锁保护的,是线程安全的, key、value都不能够为null, 每次put方法不容许null值,若是发现是null,则直接抛出异常。
官方文档也说了:若是在非线程安全的状况下使用,建议使用HashMap替换,若是在线程安全的状况下使用,建议使用ConcurrentHashMap替换。
数据结构:
数组+链表。
存取实现:
put():
限制了value不能为null。
因为直接使用key.hashcode(),而没有向hashmap同样先判断key是否为null,因此key为null时,调用key.hashcode()会出错,因此hashtable中key也不能为null。
Hashtable是在链表的头部添加元素的。
int index = (hash & 0x7FFFFFFF) %tab.length;获取index的方式与HashMap不一样
扩容机制:
Hashtable默认capacity是11,默认负载因子是0.75.。当前表中的Entry数量,若是超过了阈值,就会扩容,即调用rehash方法,从新计算每一个键值对的hashCode;
判断新的容量是否超过了上限,没超过就新建一个新数组,大小为原数组的2倍+1,将旧数的键值对从新hash添加到新数组中。
JVM调优
查看堆空间大小分配(年轻代、年老代、持久代分配)
垃圾回收监控(长时间监控回收状况)
线程信息监控:系统线程数量
线程状态监控:各个线程都处在什么样的状态下
线程详细信息:查看线程内部运行状况,死锁检查
CPU热点:检查系统哪些方法占用了大量CPU时间
内存热点:检查哪些对象在系统中数量最大
jvm问题排查和调优:
jps主要用来输出JVM中运行的进程状态信息。
jstat命令能够用于持续观察虚拟机内存中各个分区的使用率以及GC的统计数据
jmap能够用来查看堆内存的使用详情。
jstack能够用来查看Java进程内的线程堆栈信息。 jstack是个很是好用的工具,结合应用日志能够迅速定位到问题线程。
Java性能分析工具
jdk会自带JMC(JavaMissionControl)工具。能够分析本地应用以及链接远程ip使用。提供了实时分析线程、内存,CPU、GC等信息的可视化界面。
JVM内存调优
对JVM内存的系统级的调优主要的目的是减小GC的频率和Full GC的次数。 过多的GC和Full GC是会占用不少的系统资源(主要是CPU),影响系统的吞吐量。
使用JDK提供的内存查看工具,好比JConsole和Java VisualVM。
致使Full GC通常因为如下几种状况:
旧生代空间不足
调优时尽可能让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要建立过大的对象及数组避免直接在旧生代建立对象
新生代设置太小
一是新生代GC次数很是频繁,增大系统消耗;二是致使大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC
2). 新生代设置过大
一是新生代设置过大会致使旧生代太小(堆总量必定),从而诱发Full GC;二是新生代GC耗时大幅度增长
3). Survivor设置太小
致使对象从eden直接到达旧生代
4). Survivor设置过大
致使eden太小,增长了GC频率
通常说来新生代占整个堆1/3比较合适
GC策略的设置方式
1). 吞吐量优先 可由-XX:GCTimeRatio=n来设置
2). 暂停时间优先 可由-XX:MaxGCPauseRatio=n来设置
JVM内存管理:
1.先讲内存5大模块以及他们各类的做用。
2.将垃圾收集器,垃圾收集算法
3.适当讲讲GC优化,JVM优化
JVM的常见的垃圾收集器:
(注:此回答源于杨晓峰的Java核心技术36讲之一)
GC调优:
GC日志分析
调优命令
调优工具
调优命令
Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
jps,JVM Process Status Tool,显示指定系统内全部的HotSpot虚拟机进程。
jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它能够显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jmap,JVM Memory Map命令用于生成heap dump文件
jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,能够在浏览器中查看
jstack,用于生成java虚拟机当前时刻的线程快照。
jinfo,JVM Configuration info 这个命令做用是实时查看和调整虚拟机运行参数。
调优工具
经常使用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。
GC触发的条件有两种。(1)程序调用System.gc时能够触发;(2)系统自身来决定GC触发的时机。
要彻底回收一个对象,至少须要通过两次标记的过程。
第一次标记:对于一个没有其余引用的对象,筛选该对象是否有必要执行finalize()方法,若是没有执行必要,则意味可直接回收。(筛选依据:是否复写或执行过finalize()方法;由于finalize方法只能被执行一次)。
第二次标记:若是被筛选断定位有必要执行,则会放入FQueue队列,并自动建立一个低优先级的finalize线程来执行释放操做。若是在一个对象释放前被其余对象引用,则该对象会被移除FQueue队列。
Minor GC ,Full GC 触发条件
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,可是没必要然执行
(2)老年代空间不足
(3)方法区空间不足
(4)经过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
java内存模型
与JVM 内存模型不一样。
Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工做方式。JVM是整个计算机虚拟模型,因此JMM是隶属于JVM的。
Java内存模型定义了多线程之间共享变量的可见性以及如何在须要的时候对共享变量进行同步。
Java线程之间的通讯采用的是过共享内存模型,这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入什么时候对另外一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每一个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。
线程池的工做原理
1.先讲下做用
减小资源的开销 能够减小每次建立销毁线程的开销
提升响应速度 因为线程已经建立成功
提升线程的可管理性
2.讲实现
线程池主要有两部分组成,多个工做线程和一个阻塞队列。
其中 工做线程是一组已经处在运行中的线程,它们不断地向阻塞队列中领取任务执行。而 阻塞队列用于存储工做线程来不及处理的任务。
3.细分讲下线程的组成
建立一个线程池须要要的一些核心参数。
corePoolSize:基本线程数量 它表示你但愿线程池达到的一个值。线程池会尽可能把实际线程数量保持在这个值上下。
maximumPoolSize:最大线程数量 这是线程数量的上界。 若是实际线程数量达到这个值: 阻塞队列未满:任务存入阻塞队列等待执行 阻塞队列已满:调用饱和策略 。
keepAliveTime:空闲线程的存活时间 当实际线程数量超过corePoolSize时,若线程空闲的时间超过该值,就会被中止。 PS:当任务不少,且任务执行时间很短的状况下,能够将该值调大,提升线程利用率。
timeUnit:keepAliveTime的单位
runnableTaskQueue:任务队列
这是一个存听任务的阻塞队列,能够有以下几种选择:
ArrayBlockingQueue 它是一个由数组实现的阻塞队列,FIFO。
LinkedBlockingQueue 它是一个由链表实现的阻塞队列,FIFO。 吞吐量一般要高于ArrayBlockingQueue。fixedThreadPool使用的阻塞队列就是它。 它是一个无界队列。
SynchronousQueue 它是一个没有存储空间的阻塞队列,任务提交给它以后必需要交给一条工做线程处理;若是当前没有空闲的工做线程,则当即建立一条新的工做线程。 cachedThreadPool用的阻塞队列就是它。 它是一个无界队列。 PriorityBlockingQueue 它是一个优先权阻塞队列。
handler:饱和策略 当实际线程数达到maximumPoolSize,而且阻塞队列已满时,就会调用饱和策略。
AbortPolicy 默认。直接抛异常。 CallerRunsPolicy 只用调用者所在的线程执行任务。 DiscardOldestPolicy 丢弃任务队列中最久的任务。 DiscardPolicy 丢弃当前任务。
4.运行机制
当有请求到来时:
1.若当前实际线程数量 少于 corePoolSize,即便有空闲线程,也会建立一个新的工做线程;
2 若当前实际线程数量处于corePoolSize和maximumPoolSize之间,而且阻塞队列没满,则任务将被放入阻塞队列中等待执行;
3.若当前实际线程数量 小于 maximumPoolSize,但阻塞队列已满,则直接建立新线程处理任务;
4.若当前实际线程数量已经达到maximumPoolSize,而且阻塞队列已满,则使用饱和策略。
ThreadLocal的底层原理
归纳:
该类提供了线程局部 (thread-local) 变量。这些变量不一样于它们的普通对应物,由于访问某个变量(经过其 get 或 set 方法)的每一个线程都有本身的局部变量
使用:
set(obj):向当前线程中存储数据 get():获取当前线程中的数据 remove():删除当前线程中的数据
实现原理:
ThreadLocal并不维护ThreadLocalMap(ThreadLocalMap是Thread的)并非一个存储数据的容器,它只是至关于一个工具包,提供了操做该容器的方法,如get、set、remove等。而ThreadLocal内部类ThreadLocalMap才是存储数据的容器,而且该容器由Thread维护。 每个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程中全部ThreadLocal对象及其对应的值( ThreadLocalMap 是个弱引用类,内部 一个Entry由ThreadLocal对象和Object构成,
为何要用弱引用呢?
若是是直接new一个对象的话,使用完以后设置为null后才能被垃圾收集器清理,若是为弱引用,使用完后垃圾收集器自动清理key,程序员不用再关注指针。
操做细节
进行set,get等操做都是首先会获取当前线程对象,而后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key ,再作相应的处理。
内存泄露问题
在ThreadLocalMap中,只有key是弱引用,value仍然是一个强引用。
每次操做set、get、remove操做时,ThreadLocal都会将key为null的Entry删除,从而避免内存泄漏。
固然,当 若是一个线程运行周期较长,并且将一个大对象放入LocalThreadMap后便再也不调用set、get、remove方法,此时该仍然可能会致使内存泄漏。 这个问题确实存在,没办法经过ThreadLocal解决,而是须要程序员在完成ThreadLocal的使用后要养成手动调用remove的习惯,从而避免内存泄漏。
使用场景;
Web系统Session的存储
当请求到来时,能够将当前Session信息存储在ThreadLocal中,在请求处理过程当中能够随时使用Session信息,每一个请求之间的Session信息互不影响。当请求处理完成后经过remove方法将当前Session信息清除便可。
voliate 的实现原理
为何volatile能保证共享变量的内存可见性?
volatile变量写
当被volatile修饰的变量进行写操做时,这个变量将会被直接写入共享内存,而非线程的专属存储空间。
volatile变量读
当读取一个被volatile修饰的变量时,会直接从共享内存中读,而非线程专属的存储空间中读。
禁止指令重排序
volatile读
若volatile读操做的前一行为volatile读/写,则这两行不会发生重排序 volatile读操做和它后一行代码都不会发生重排序
volatile写
volatile写操做和它前一行代码都不会发生重排序; 若volatile写操做的后一行代码为volatile读/写,则这两行不会发生重排序。
当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,所以线程B再须要读取从主内存中去读取该变量的最新值。
NIO底层原理
1概念:
NIO 指新IO,核心是 同步非阻塞,解决传统IO的阻塞问题。操做对象是Buffer。 其实NIO的核心是IO线程池,(必定要记住这个关键点)。 NIO中的IO多路复用调用系统级别的select和poll模型,由系统进行监控IO状态,避免用户线程经过反复尝试的方式查询状态。
2.工做原理:
由一个专门的线程来处理全部的 IO 事件,并负责分发。
事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
线程通信:线程之间经过 wait,notify 等方式通信。保证每次上下文切换都是有意义的。减小无谓的线程切换。
3.通讯模型是怎么实现的呢?
java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上能够注册咱们感兴趣的事件。
四种事件
服务端接收客户端链接事件SelectionKey.OP_ACCEPT(16)
客户端链接服务端事件SelectionKey.OP_CONNECT(8)
读事件SelectionKey.OP_READ(1)
写事件SelectionKey.OP_WRITE(4)
服务端和客户端各自维护一个管理通道的对象,咱们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。咱们以服务端为例,若是服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,若是访问selector时发现有感兴趣的事件到达,则处理这些事件,若是没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。
IOC底层实现原理
概念:
IOC 是面向对象编程中的一种设计原则,IOC理论提出的观点大致是这样的:借助于“第三方”实现具备依赖关系的对象之间的解耦。 。所谓IoC,对于spring框架来讲,就是由spring来负责控制对象的生命周期和对象间的关系。 是说建立对象的控制权进行转移,之前建立对象的主动权和建立时机是由本身把控的,而如今这种权力转移到第三方。
实现原理:
它是经过反射机制+工厂模式实现的,在实例化一个类时,它经过反射调用类中set方法将事先保存在HashMap中的类属性注入到类中。
控制反转就是:得到依赖对象的方式反转了。
一、依赖注入发生的时间
(1).用户第一次经过getBean方法向IoC容索要Bean时,IoC容器触发依赖注入。
(2).当用户在Bean定义资源中为
2.依赖注入实如今如下两个方法中:
(1).createBeanInstance:生成Bean所包含的java对象实例。
(2).populateBean :对Bean属性的依赖注入进行处理。
AOP底层实现原理
概念
AOP(Aspect-OrientedProgramming,面向方面编程),能够说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP引入封装、继承和多态性等概念来创建一种对象层次结构,用以模拟公共行为的一个集合。 而AOP技术则偏偏相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。 简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减小系统的重复代码,下降模块间的耦合度,并有利于将来的可操做性和可维护性。
AOP的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。
AOP的实现
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法建立“方面”,从而使得编译器能够在编译期间织入有关“方面”的代码。
如何使用Spring AOP
能够经过配置文件或者编程的方式来使用Spring AOP。 配置能够经过xml文件来进行,大概有四种方式:
2. 配置AutoProxyCreator,这种方式下,仍是如之前同样使用定义的bean,可是从容器中得到的其实已是代理对象 3. 经过
4. 通
Spring AOP的实现
如何生成代理类:
Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪一种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是若是目标类是接口,则使用JDK动态代理技术,不然使用Cglib来生成代理
切面是如何织入的?
InvocationHandler是JDK动态代理的核心,生成的代理对象的方法调用都会委托到InvocationHandler.invoke()方法。
MyisAM和innodb的有关索引的疑问
二者都是什么索引?汇集仍是非汇集https://www.cnblogs.com/olinux/p/5217186.html
MyISAM( 非汇集)
使用B+Tree做为索引结构,叶节点的data域存放的是数据记录的地址。
MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,若是指定的Key存在,则取出其data域的值,而后以data域的值为地址,读取相应数据记录。
InnoDB( 汇集索引)
第一个重大区别是InnoDB的数据文件自己就是索引文件, 这棵树的叶节点data域保存了完整的数据记录。
可是辅助索引搜索须要检索两遍索引:首先检索辅助索引得到主键,而后用主键到主索引中检索得到记录。
由于InnoDB的数据文件自己要按主键汇集,因此InnoDB要求表必须有主键(MyISAM能够没有),若是没有显式指定,则MySQL系统会自动选择一个能够惟一标识数据记录的列做为主键,若是不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段做为主键,这个字段长度为6个字节,类型为长整形。
简单说:
做者:睶先生
来源:CSDN
原文:http://www.javashuo.com/article/p-ocublqxy-n.html 版权声明:本文为博主原创文章,转载请附上博文连接!