Java之经常被问到的底层原理+调优总结+ 特别的疑问的面试题
https://blog.csdn.net/Butterfly_resting/article/details/82894172
面试发现经常有些重复的面试问题,自己也应该学会记录下来,最好自己能做成笔记,在下一次面的时候说得有条不紊,深入具体,面试官想必也很开心。以下是我个人总结,请参考:
HashSet底层原理:(问了大几率跟HashMap一起面)
HashMap底层原理:(非常大几率问到)
Hashtable底层原理:(问的少,问了大几率问你跟HashMap的区别)
synchronized底层如何实现?锁优化,怎么优化?
ReentrantLock 底层实现;
ConcurrentHashMap 的工作原理,底层原理(谈到多线程高并发大几率会问它)
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
https://blog.csdn.net/HD243608836/article/details/80214413
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():
1. 根据key计算得到key.hash = (h = k.hashCode()) ^ (h >>> 16);
2. 根据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的时候则将桶中链表内的元素转换成红黑树!!!稍后再详细讨论红黑树。
扩容机制的实现
1. 扩容(resize)就是重新计算容量。当向HashMap对象里不停的添加元素,而HashMap对象内部的桶数组无法装载更多的元素时,HashMap对象就需要扩大桶数组的长度,以便能装入更多的元素。
2. capacity 就是数组的长度/大小,loadFactor 是这个数组填满程度的最大比比例。
3. size表示当前HashMap中已经储存的Node<key,value>的数量,包括桶数组和链表 / 红黑树中的的Node<key,value>。
4. threshold表示扩容的临界值,如果size大于这个值,则必需调用resize()方法进行扩容。
5. 在jdk1.7及以前,threshold = capacity * loadFactor,其中 capacity 为桶数组的长度。 这里需要说明一点,默认负载因子0.75是是对空间和时间(纵向横向)效率的一个平衡选择,建议大家不要修改。 jdk1.8对threshold值进行了改进,通过一系列位移操作算法最后得到一个power of two size的值
什么时候扩容
当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。
扩容必须满足两个条件:
1、 存放新值的时候 当前已有元素的个数 (size) 必须大于等于阈值
2、 存放新值的时候当前存放数据发生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。
1. 第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。
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的数组
https://www.cnblogs.com/wuzhitong/p/8492228.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成功
1. 如果没有初始化就先调用initTable()方法来进行初始化过程
2. 如果没有hash冲突就直接CAS插入
3. 如果还在进行扩容操作就先进行扩容
4. 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
5. 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
6. 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容。
get()
1. 计算hash值,定位到该table索引位置,如果是首节点符合就返回
2. 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
概括版:
(1)对于get读操作,如果当前节点有数据,还没迁移完成,此时不影响读,能够正常进行。
如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时get线程会帮助扩容。
(2)对于put/remove写操作,如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时写线程会帮助扩容,如果扩容没有完成,当前链表的头节点会被锁住,所以写线程会被阻塞,直到扩容完成。
扩容机制:https://www.e-learn.cn/content/java/1154828
引入了一个ForwardingNode类,在一个线程发起扩容的时候,就会改变sizeCtl这个值,
1. sizeCtl :默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。
2. -1 代表table正在初始化
3. -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。
* jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控
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状态,避免用户线程通过反复尝试的方式查询状态。
* Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
2.工作原理:
1. 由一个专门的线程来处理所有的 IO 事件,并负责分发。
2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
3. 线程通讯:线程之间通过 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、依赖注入发生的时间
(1).用户第一次通过getBean方法向IoC容索要Bean时,IoC容器触发依赖注入。
(2).当用户在Bean定义资源中为<Bean>元素配置了lazy-init属性,即让容器在解析注册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文件来进行,大概有四种方式:
1. 配置ProxyFactoryBean,显式地设置advisors, advice, target等
2. 配置AutoProxyCreator,这种方式下,还是如以前一样使用定义的bean,但是从容器中获得的其实已经是代理对象 3. 通过<aop:config>来配置
4. 通<aop:aspectj-autoproxy>过来配置,使用AspectJ的注解来标识通知及切入点
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个字节,类型为长整形。
简单说:
如果我们定义了主键(PRIMARY KEY),那么InnoDB会选择其作为聚集索引;如果没有显式定义主键,则InnoDB会选择第一个不包含有NULL值的唯一索引作为主键索引;
后补ing....