- 永久代存储满了,触发Full GC,针对常量池的回收和类型的卸载
- Minor GC后放入老年代大小>老年代可用内存,即老年代放不下
- Minor GC后,放入一个1区中时,放不下,溢出来部分放入老年区,老年区放不下就会触发Full GC
GC会触发“stop-the-world”,即工作线程全部关闭,进行gc回收,当gc回收结束后,才会执行任务
HashMap
(1)美团面试题:Hashmap的结构,1.7和1.8有哪些区别,史上最深入的分析
简述
影响性能的两个参数:
-
初始容量:2的幂,默认是16
-
加载因子:什么时候扩容的标志,默认0.75,即16*0.75=12的时候开始hashmap扩容(容量为原来的2倍)
-
最大容量:2的30次方,如果大于,则使用2的30次方的大小
-
可以存储key == null,value == null,key == null则存储在table[0]位置
-
删除元素的本质是“删除单向链表的节点”
-
Entry是单向链表
计算key的hash值,并将hash值添加到对应的链表中,若key存在,则更新vlaue值
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //计算出来的hash值
final K key; //key
V value; //value
Node<K,V> next; //链表next引用
…
}
和修改
- 因为是非synchronized的,非线程安全,所以比较快
- HashMap可以接受null键和null值
数组下标index的计算过程
//数组长度-1 & hash值
(n - 1) & hash
同等于hash值对数组长度的求余
描述一下具体的put过程
- 对key求hash值,然后计算数组下标
- 如果数组下标没有碰撞,将Node放置在数组中
- 如果碰撞,将Node以链表的形式连接在后面
- 如果链表长度超过阈值(8),将链表转化为红黑树,链表长度低于6,则将红黑树转回链表
- 如果节点存在,则替换旧值
- 如果数组快满了(最大容量16*加载因子0.75),就需要resize(扩容两倍)
为什么选择6和8 ?
因为中间7的位置放置频繁的数据结构切换后,影响性能
get方法
- 计算key的hash,在计算index值
- 在数组中查找index值,在比对key值,取出value,复杂度最好是O(1),最坏为O(n)
为什么不直接使用红黑树?
空间和时间的选择,链短的时候空间上占用小,时间还好,转化为红黑树后,便于查找,但是耗费空间。
处理hash冲突的方法有以下几种:
- 开放地址法(线性探测再散列(碰撞后,位置后挪,数组长度+x)x可为正数,二次探测再散列(数组长度+x的平方)x可为正负数,平方后均为正数)
- 再哈希法(多种计算哈希的方法,相同则替换方法,直到算出不重复的哈希值)
- 链地址法(链表)
- 建立公共溢出区(建立一个溢出表,存放冲突的数据)
HashMap的性能慢原因?
- 数据类型自动装箱问题
- resize扩容重新计算index值和hashcode,重新赋值(1.7)
1.8后,扩容位置 = hash值 & 数组长度,如果为0,则不动,反之则反
线程不安全会导致什么
环状链表,resize(扩容)时头插法导致环形链表(1.7版本)
都存在数据丢失的问题数据丢失,1.8版本修复环形链表(尾插)
HashMap中默认容量为什么是2的幂?
因为如果不是2的幂,可能会造成更多的hash碰撞(index 下标碰撞)
假设n为17,n-1的二进制为10000,01001和01101算出的index值均为0
假设n为16,n-1的二进制为01111,01001和01101算出的index值不同
hashcode计算原理
对于int类型,hashcode为它本身,eg:int i = 1; hashcode = 1;
对于对象来说,hashcode是内部地址和对象值的一个映射
hash()算法原理
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
拿到key的hashCode(),在将该值与该值的高16位(h无符号右移16位)进行亦或运算(相同为0,不同为1)
HashTable的理解
put和get方法是用了synchronized修饰,锁住了整个map,同一时刻只有一个线程可以操作
不可以存储null值和null健
SparseArray理解
原理
装箱,int数据类型---->Integer对象,拆箱,Integer对象---->int数据类型
默认容量是10
- key是int值(避免装箱问题),使用二分查找寻找key,同样也是用二分插入,从小到大排列好的
- 两个数组,一组存放key(int []),一组存放value(object [])
mKeys[i] = key;
mValues[i] = value;
- 如果冲突,直接替换value的值
二分插入:
while (lo <= hi) {
//二分法一分而二,数组中间下标
final int mid = (lo + hi) >>> 1;
//二分法一分而二,数组中间下标处的值
final int midVal = array[mid];
if (midVal < value) {
/**
如果数组中间处的值比要找的值小,代表要找的值
在数组的中后部部分,所以当前下标取值为mid + 1
/
lo = mid + 1;
} else if (midVal > value) {
/*
如果数组中间处的值比要找的值大,代表要找的值
在数组的前中部部分,所以当前下标取值为mid - 1
*/
hi = mid - 1;
} else {
//数组中间处的值与要找的值相等,直接返回数组中部的下标mid
return mid; // value found
}
}
第一个值放到最中间位置
第二个值如果大于中间的值放置在左边的中间位置
………….
put方法中,容量充足,计算key值所需存放的index,如果key相同,就直接替换value,如果不同,就insert数组,后续index元素后移,新key放置在index上
较HashMap的优点
- 节省内存
- 性能更好,避免装箱问题
- 数据量不达到千级,key为int值,可以用SparseArray替换HashMap
SparseArray与HashMap的比较,应用场景是?
- SparseArray采用的不是哈希算法,HashMap采用的是哈希算法
- SparseArray采用的是两个一维数组分别用于存储键和值,HashMap采用的是一维数组+单向链表/红黑树
- SparseArray key只能是int类型,而HashMap可以任何类型
- SparseArray key是有序存储(升序),而HashMap不是
- SparseArray 默认容量是10,而HashMap默认容量是16
- SparseArray 内存使用要优于HashMap,因为:
- SparseArray key是int类型,而HashMap是Object
- SparseArray value的存储被不像HashMap一样需要额外的需要一个实体类(Node)进行包装
- SparseArray查找元素总体而言比HashMap要逊色,因为SparseArray查找是需要经过二分法的过程,而HashMap不存在冲突的情况其技术处的hash对应的下标直接就可以取到值
针对上面与HashMap的比较,采用SparseArray还是HashMap,建议根据如下需求选取:
- 如果对内存要求比较高,而对查询效率没什么大的要求,可以是使用SparseArray
- 数量在百级别的SparseArray比HashMap有更好的优势
- 要求key是int类型的,因为HashMap会对int自定装箱变成Integer类型
- 要求key是有序的且是升序
ArrayMap的理解
内部也使用二分算法进行存储和查找,设计上更多考虑了内存中的优化
- int []存储hash值,array[index]存储key,array[index+1]存储value
数据量最好在千级以内
ArrayMap和SparseArray怎么进行选取?
- 如果key为int,那么选取SparseArray进行存储, 不存在封/拆箱问题
- 如果key不为int,则使用ArrayMap
TreeMap的理解
TreeMap是一个二叉树的结构,红黑树
不允许重复的key
TreeMap没有调优选项,因为其红黑树总保持在平衡状态
TreeMap和HashMap的区别?
- TreeMap由红黑树构成,HashMap由数组+链表/红黑树构成
- HashMap元素没有顺序,TreeMap元素会根据可以进行升序排序
- HashMap进行插入,查找,删除最好,TreeMap进行自然顺序便利或者自定义顺序便利比较好
ThreadLocal的理解
面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)
线程隔离,数据不交叉
- ThreadLocalMap,每个thread都存在一个变量ThreadLocalMap threadLocals
- threadLocalMap中存在Entry,同ThreadLocal之间为弱引用关系
- ThreadLocalMap中key为ThreadLocal的弱引用,value为Entry,内部为一个object对象
- table默认大小为16,存在初始容量(16)和阈值(16*2/3)
- 在ThreadLocal中使用get()和set()方法初始化threadLocals
- get、set、remove方法将key==null的数据清除
- table是环形数组
线性探测法避免哈希冲突,增量查找没有被占用的地方
通过hashcode计算索引位置,如果key值相同,则替换,不同就nextIndex,继续判断,直到插入数据
ThreadLocal就是管理每个线程中的ThreadLocalMap,所以线程隔离了。
ThreadLocalMap的理解
新建ThreadLcoal的时候,创建一个ThreadLocalMap对象,计算hash的时候使用0x61c88647这个值,他是黄金分割数,导致计算出来的hash值比较均匀,这样回大大减少hash冲突,内部在采用线性探测法解决冲突 set:
- 根据key计算出数组索引值
- 遍历该索引值的链表,如果为空,直接将value赋值,如果key相等,直接更新value,如果key不相等,使用线性探测法再次检测。
ThreadLocal使用弱引用的原因
key使用了弱引用,如果key使用强引用,那么当ThreadLocal的对象被回收了,但ThreadLocalMap还持有ThreadLocal的强引用,回导致ThreadLocal不会被回收,导致内存泄漏
ThreadLocal的内存泄漏
- 避免使用static修饰ThreadLocal:延长生命周期,可能造成内存泄漏
- ThreadLocal弱引用被gc回收后,则key为null,object对象没有被回收,只有当再次调用set,get,remove方法的时候才会清楚key为null的对象
ThreadLocalMap清理过期key的方式
- 探测式清理 本该放在4的位置上的值,放到了7的位置上,当5过时后,将7的数据挪到5的位置上
- 启发式清理 遍历数组,清理数据
ConcurrentHashMap和HashMap的区别
jdk 1.7 ReentrantLock+segments + hashEntry(不可变)
- 线程安全,分段线程锁,hashtable是整段锁,所以性能有所提高
- 默认分配16个锁,比Hashtable效率高16倍
- hashEnty是final的,不能被修改,只要被修改,该节点之前的链就要重新创建,采用头插插入,所以顺序反转
- 获取size,因为是多线程访问,所以size会获取三遍,如果前后两个相等就返回,假设不相等,就将Segment加锁后计算。
jdk 1.8 : synchronized +node+volatile+红黑树
put:
- 根据key的hash值算出Node数组的相应位置
- 如果该Node不为空,且当前该节点不处于移动状态,则对节点加synchronized锁,进行遍历节点插入操作
- 如果是红黑树节点,向红黑树插入操作
- 如果大于8个,拓展为红黑树
get:
- 计算hash值,定位到该table索引位置,如果是首节点符合就返回
- 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,通知在新表中查找该节点,匹配就返回
- 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
1.7和1.8的区别:
- 1.7:ReentrantLock+segments + hashEntry(不可变)
1.8:synchronized +node+volatile+红黑树
-
1.8的锁的粒度更低,锁的是一个链表(table[i]),而1.7锁的是一个小的hashmap(segement)
-
ReentrantLock性能比synchronized差
扩容:
1.7下进行小HashMap(segement)扩容操作
1.8下使用synchrozied节点加锁,所以可以通过多个线程扩容处理。一个线程创建新的ConcurrentHashMap,并设置大小,多个线程将旧的内容添加到新的map中,如果添加过的内容就会设置标记,其他线程就不会处理
为什么只有hashmap可以存储null值和null键
因为hashmap是线程不安全的,而在其他中都是线程安全的,在多线程访问时,无法判断key为null是没有找到,还是key为null
锁
常见锁
锁的分类
- 公平锁/非公平锁
- 公平锁:多个线程按照申请锁的顺序获取锁。
- 非公平锁:多个线程申请锁并不是按照顺序获取锁,有可能先申请后获取锁。(Synchronized)
ReentrantLock默认是非公平锁,通过构造传参可设置为公平锁。非公平锁的优点在于吞吐量比公平锁大
- 可重入锁:又名递归锁,指在外层方法获取锁以后,在进入内层方法也会自动获取锁。
synchronized void setA() throws Exception(){
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception(){
Thread.sleep(1000);
}
如果不是可重入锁,那么setB方法不会被当前线程执行,容易造成死锁
synchronized是可重入锁
- 独享锁/共享锁
- 独享锁:一个锁一次只能被一个线程所持有(ReentrantLock,synchronized)
- 共享锁:一个锁被多个线程所持有。(ReadWriteLock)
-
互斥锁/读写锁 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock -
乐观锁/悲观锁
- 悲观锁:对同一数据的并发操作,一定会发生修改的。(利用各种锁实现)
- 乐观锁:对同一数据的并发操作,一定不会发生修改的。(无锁编程,CAS算法,自旋实现原子操作的更新)
-
分段锁
是一种锁的设计,并不是具体的锁,在1.7版本的ConcurrentHashMap中,使用分段锁设计,该分段锁又称为Segment,map中每一个链表由ReentrantLock修饰 -
偏向锁/轻量级锁/重量级锁 这三种锁是描述synchronized的三种状态。
- 偏向锁:一段同步代码一直被一个线程访问,那么会自动获取锁,降低获取锁的代价
- 轻量级锁:当锁是偏向锁的时候,被另一个线程访问,偏向锁会升级为轻量级锁,其他线程通过自旋的方式获取锁,不会阻塞,提高性能
- 重量级锁:在轻量级锁的基础上,自旋达到上限就会阻塞,升级为重量级锁,会让其他线程进入阻塞,影响性能。
锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后无法降为偏向锁,这种升级无法降级的策略目的就是为了提高获得锁和释放锁的效率。
- 自旋锁
获取锁的过程中,不会立即阻塞,会采用循环的方式获取锁,减少线程切换上下文的消耗,缺点是循环会消耗cpu
java中常用锁的类型
- synchronized:非公平,悲观,独享,互斥,可重入,重量级锁
- ReentrantLock:默认非公平(可公平),悲观,独享,互斥,可重入,重量级锁
CAS,全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM 只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。
synchronized和volatile
简述synchronized的原理
可见性:表示A修改的值对于B执行时可以看见A修改后的值
- 内部使用monitorenter指令,同时只有一个线程可以获取monitor
- 未获取monitor的线程会被阻塞,等待获取monitor
- 线程A获取主内存值后加锁,在本地内存更新值(临时区)后,推送到主内存,通过synchronized隐式通知线程B访问主存获取值,在B的把本地内存更新值后推送到主存,重复以上操作。
通过Monitor对象来实现方法和代码块的同步,存在monitorEnter和monitorExit指令,插入程序中,在一个线程访问时,通过Monitor进行线程阻塞
synchronized修饰静态方法、⾮静态方法区别
静态方法:该类的对象,new出来的多个实例对象是被一个锁锁住的,多线程访问需要等待
非静态方法:实例对象
volatile
修饰成员变量,保证可见性,下一个操作再上一个操作之上。++操作不保证和原子性,
将本地缓存同步到主存中,使其他本地缓存失效,本地缓存通过嗅探检查自己的缓存是否过期。(下一次访问,主存不会主动通知)
volatile无法保证原子性,可以使用乐观锁的重试机制进行优化
synchronized和volatile区别
-
Synchronized 引起线程阻塞,而volatile不会
-
区别在于,synchronized是隐式通知B去主存获取值,volatile是B主动通过嗅探的方法发现自己的内存过期后去主存做同步
-
synchronized:先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。
-
都存在可见性,但是volatile不具备原子性,所以不会造成线程阻塞
假设某一时刻i=10,线程A读取10到自己的工作内存,A对该值进行加一操作,但正准备将11赋给i时,由于此时i的值并未改变,B读取了主存的值仍为10到自己的工作内存,并执行了加一操作,正准备将11赋给i时,A将11赋给了i,由于volatile的影响,立即同步到主存,主存中的值为11,并使得B工作内存中的i失效,B执行第三步,虽然此时B工作内存中的i失效了,但是第三步是将11赋给i,对B来说,我只是赋值操作,并没有使用i这个动作,所以这一步并不会去刷新主存,B将11赋值给i,并立即同步到主存,主存中的值仍为11。虽然A/B都执行了加一操作,但主存却为11,这就是最终结果不是10000的原因。
- synchronized修饰方法,类,变量,代码块,volatile只能修饰变量
synchronized修饰不同对象的区别
- 修饰类:作用的对象是这个类的所有对象
- 方法:作用对象是这个方法的对象
- 静态方法:作用对象是这个类的对象
- 代码块:作用对象是这个代码块的对象
悲观锁和乐观锁(CAS)
悲观锁:当前线程获得锁会阻塞其他线程(sychronized)
乐观锁:不会添加锁,会存在三个值内存实际值,内存的旧值,更新的新值,如果内存实际值和旧值相等,则没有线程修改该值,将更新的新值直接赋值给内存,如果不相等,就重新尝试赋值操作(volatile)
CAS的缺点:
- ABA问题,A->B->A,乐观锁认为没有变化,都是A,所以直接赋值
- 重新赋值的话,会导致时间过长。
ReentrantLock
CAS+AQS实现,乐观锁
AQS(单链表队列)维护一个等待队列,将获取不到锁的线程放入到队列中进行等待,当当前线程执行结束后,进行出队操作,使用一个volatile的int成员变量(state)来表示同步状态
通过ReentrantLock的Lock方法进行加锁
通过ReentrantLock的unLock方法进行解锁
线程
新建线程有几种方式?
- new Thread
- 新建Runnable对象
- 新建Callable或者Future对象
- 线程池使用
new Thread的弊端
执行一个异步任务你还只是如下new Thread吗? new Thread的弊端如下:
- 每次new Thread新建对象性能差。
- 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
- 缺乏更多功能,如定时执行、定期执行、线程中断。
相比new Thread,Java提供的四种线程池的好处在于:
- 重用存在的线程,减少对象创建、消亡的开销,性能佳。
- 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
- 提供定时执行、定期执行、单线程、并发数控制等功能。
线程池
简述线程池
线程的5种状态
- NEW:创建一个新线程
- RUNNABLE:可运行
- BLOCKED:阻塞
- WAITING:进入等待状态
- TIMED_WAITING:等待结束,重新获取锁
- TERMINATED:结束
- RUNNING:运行中
- READY:就绪
一般来说分为五大状态:
- 新建(New):
创建线程对象,进入新建状态。eg:Thread thread = new Thread(); - 就绪(Runnable):
调用thread.start()方法,随时可被cpu执行 - 运行(Runnable):
CPU执行线程 - 阻塞(Blocked): 出于某些原因,cpu放弃线程执行,线程进入暂停状态
- 等待阻塞:调用wait方法,进行阻塞,线程等待某工作完成
- 同步阻塞:在获取Synchronized同步锁时,进行等待
- 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 死亡(Dead):
堪称执行完毕或者因异常退出,线程死亡,回收
start和run的区别?sleep和wait的区别?join,yield,interrupt
- start是启动一个线程
- run只是Thread的实现方法,主要实现是Runnable的接口回调run方法
- sleep不会释放对象锁,只是暂停了线程的运行,当指定时间到了,就恢复运行状态
- wait方法放弃对象锁,只有调用了notify()方法,才会重新获取锁,进入运行状态
- join方法是规定线程的执行顺序,如果在B线程中调用了A的join方法,那么,直到A执行完毕,才会执行B,按照顺序串行执行。实际内部方法是调用了wait方法,让B处于等待状态,A执行完成后,启动B
注意:wait方法是调用u哦在线程放弃对象锁,所以在B线程调用A的join方法,只是让B等待了。
- yield方法,通知cpu该线程任务不紧急,可以被暂停让其他线程运行
- interrupt方法,中断通知线程,具体操作由线程执行,根据不同状态,执行不同逻辑
线程t1、t2、t3,如何保证他们顺序执行?
t3开始中调用t2.join(),t2开始中调用t1.join()。
t1执行完毕后,t2中t1.join()方法不阻塞,即t1执行完,执行t2中的方法,后续类似
使用CountDownLacth,进行计数
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(“t1”);
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
//引用t1线程,等待t1线程执行完
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“t2”);
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
//引用t2线程,等待t2线程执行完
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“t3”);
}
});
t3.start();
t2.start();
t1.start();
}
什么是死锁
资源竞争互相等待
假设线程A,线程B,资源A,资源B
线程A访问资源A,持有资源A锁,线程B访问资源B,持有资源B锁,而后线程A要访问资源B,但是线程B持有资源B锁,线程A等待,线程B要访问资源A,但是线程A持有资源A锁。所以B等待。
结果就是A、B相互等待对方释放资源,造成死锁。
一个线程崩溃会影响其他线程吗?
不一定。
如果崩溃发生在堆区(线程共享区域),会导致其他线程崩溃。
如果崩溃发生在栈区(线程私有区域),不会导致其他线程的崩溃
java反射
- 反射类及反射方法的获取,都是通过从列表中搜寻查找匹配的方法,所以查找性能会随类的大小方法多少而变化;
- 每个类都会有一个与之对应的Class实例,从而每个类都可以获取method反射方法,并作用到其他实例身上;
- 反射也是考虑了线程安全的,放心使用;
- 反射使用软引用relectionData缓存class信息,避免每次重新从jvm获取带来的开销;
- 反射调用多次生成新代理Accessor, 而通过字节码生存的则考虑了卸载功能,所以会使用独立的类加载器;
- 当找到需要的方法,都会copy一份出来,而不是使用原来的实例,从而保证数据隔离;
- 调度反射方法,最终是由jvm执行invoke0()执行;
使用反射从jvm中的二进制码文件中读取数据
反射原理
.java–>.class–>java.lang.Class对象
编译过程:
- 将.java文件编译成机器可以识别的二进制文件.class
- .class文件中存储着类文件的各种信息。
比如版本号、类的名字、字段的描述和描述符、方法名称和描述、是不是public、类索引、字段表集合,方法集合等等数据 - JVM从二进制文件.class中取出并拿到内存解析
- 类加载器获取类的二进制信息,并在内存中生成java.lang.Class对象
- 最后开始类的生命周期并初始化(先静态后非静态和构造,先父类在子类)
而反射操作的就是内存中的java.lang.Class对象。
总结来说.class是一种有顺序的结构文件,而Class对象就是对这种文件的一种表示,所以我们能从Class对象中获取关于类的所有信息,这就是反射的原理。
为什么反射耗时?
- 校验时间长
- 基本类型的封箱和拆箱
- 方法内联
什么是内联函数?
方法调用过多会进行内敛优化,减少方法的嵌套层级,加快执行,缓解栈的空间存储
反射可以修改final类型的成员变量吗?
已知final修饰后不会被修改,所以获取这个变量的时候就直接帮你在编译阶段就给赋值了
编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。
所以上述的getName方法经过JVM编译内联优化后会变成:
public String getName() {
return “Bob”;
}
//打印出来也是Bob
System.out.println(user.name)
//经过内联优化
System.out.println(“Bob”)
反射是可以修改final变量的,但是如果是基本数据类型或者String类型的时候,无法通过对象获取修改后的值,因为JVM对其进行了内联优化。
反射可以修改static值吗?
Field.get(null) 可以获取静态变量。
Field.set(null,object) 可以修改静态变量。
Java异常
简析
java中的异常分为2大类,Error和Exception。Error中有StackOverFlowError和OutOfMemoryError。Exception分为IOException和RuntimeException。
Java中检查型异常和非检查型异常有什么区别?
检查型异常 extends Exception(编译时异常):需要使用try catch进行捕获,否则会出错,继承自Exception
非检查型异常 extends RuntimeException(运行时异常):不需要捕获,在必要时才会报错,
try-catch-finally-return执行顺序?
- 不管是否有异常产生,finally块中代码都会执行
- 当try和catch中有return语句时,finally块仍然会执行
- finally是在return后面的表达式运算执行的,所以函数返回值在finally执行前确定的,无论finally中的代码怎么样,返回的值都不会改变,仍然是之前return语句中保存的值
- finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值
throw和throws的区别
throw用在方法内部,抛出异常
throws用在方法外部,在方法中抛出异常
栈溢出StackOverFlowError发生的几种情况?
递归,栈内存存满,函数调用栈太深
Java常见异常有哪些
java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。
java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.
java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。
java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
java.lang.ArithmeticException:算术条
件异常。譬如:整数除零等。
java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。
java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。
java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。
java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。
java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。
linux进程通信有几种
Linux中的进程间通信有哪些?解释Binder通信为什么高效?Binder通信有什么限制?
Linux中的进程间通信有如下几种:
- 信号(signal)
- 消息队列
- 共享内存(Shared Memory)
共享内存允许两个或多个进程进程共享同一块内存(这块内存会映射到各个进程自己独立的地址空间)从而使得这些进程可以相互通信。 - 管道/命名管道(Pipe)
Pipe这个词很形象地描述了通信双方的行为,即进程A与进程B。一根管道同时具有读取端和写入端。比如进程A从write end写入,那么进程B就可以从read end读取数据。 - Socket
本地和服务端各自维护一个“文件”,在建立连接打开后,向自己的文件中写入数据,供对方读取
Binder通信是Android系统特有的IPC机制,Binder的优点有以下几个:
- 性能:Binder的效率高,只需要一次内存拷贝;而Linux中的管道、消息队列、套接字都需要2次;共享内存的方式不需要拷贝数据,但是有多进程同步的问题。
- 稳定性:Binder的架构是基于C/S结构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好。共享内存虽然无需拷贝,但是控制负责,难以使用。从稳定性的角度讲,Binder 机制是优于内存共享的。
- 安全性:传统的 IPC 接收方无法获得对方可靠的进程用户ID/进程ID(UID/PID),从而无法鉴别对方身份。Android 为每个安装好的 APP 分配了自己的 UID,故而进程的 UID 是鉴别进程身份的重要标志。Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限。从安全角度,Binder的安全性更高。
Binder通信的另外一个限制是最多16个线程。最多只能传输1M的数据,否则会有TransactionTooLarge的Exception。
CountDownLatch原理
存在4个线程,想在4个线程都执行完毕后执行另一个线程,
countDownLatch是采用计数器的原理,存在两个方法:
countDown:计数-1
await:线程挂起,当计数为0时,执行其后的逻辑
Java泛型
泛型简述
java中泛型即是“参数化类型”,即该泛型类型是一个参数传入
只在程序的源代码中存在,在编译后的字节码中已经替换为原生类型,这种方法称为伪泛型。
java中的泛型只在编译时期有效,正确检验泛型的结果后,会将泛型相关的信息擦出,并在对象进入和离开的方法边界上添加类型检查
和类型转化
的方法。
List stringArrayList = new ArrayList();
List integerArrayList = new ArrayList();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
if(classStringArrayList==classIntegerArrayList){ //返回true
System.out.println(“类型相同”);
}
泛型有泛型类
、泛型方法
和泛型接口
泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}
泛型接口:
//定义一个泛型接口
public interface Generator {
public T next();
}
/**
- 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
- 即:class FruitGenerator implements Generator{
- 如果不声明泛型,如:class FruitGenerator implements Generator,编译器会报错:“Unknown class”
/
class FruitGenerator implements Generator{
@Override
public T next() {
return null;
}
}
/* - 传入泛型实参时:
- 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator
- 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
- 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
- 即:Generator,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator {
private String[] fruits = new String[]{“Apple”, “Banana”, “Pear”};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
泛型方法:
/**
- 泛型方法的基本介绍
- @param tClass 传入的泛型实参
- @return T 返回值为T类型
- 说明:
-
1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
-
2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
-
3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
-
4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public T genericMethod(Class tClass){
T instance = tClass.newInstance();
return instance;
}
泛型对方法重载的影响?
方法不能进行重载,会报错,两种方法都有相同的擦除
,在编译期间进行泛型擦除的,会导致擦出后都一样
public class MyMethod {
public void listMethod(List list1){}
public void listMethod(List list2){}
}
类加载
java类的初始化流程
父类到子类,静态到(非静态,构造),变量----->代码块
父类静态变量----父类静态代码块----子类静态变量----子类静态代码块----父类非静态----父类构造----子类非静态----子类构造
jvm类加载机制的7个流程
加载-----验证------准备------解析------初始化-------使用------卸载 JVM将.java文件加载成二进制文件.class
加载:
- 获取二进制流class文件
- 将静态存储结构转换为方法区中运行时的数据结构,存储到方法区中
- 在堆中生成一个java对象,作为方法区的引用
获取.class文件并在堆中生成一个class对象,将加载的类结构信息存储在方法区
验证:JVM规范校验,代码逻辑校验
准备:为类变量分配内存并设置类变量的初始化,如果变量被final修饰,会直接放入对应的常量池中,并赋值
解析:常量池符号引用替换为内存的直接引用
(上述三种统称为连接)
初始化:执行代码逻辑,对静态变量,静态代码块和类对象进行初始化
使用:使用初始化好的class对象
卸载:销毁创建class对象,负责运行的jvm退出内存
全局变量和局部变量的区别
- 全局变量应用于整个类文件。局部变量只在方法执行期间存在,之后被回收。静态局部变量对本函数体始终可见
- 全局变量,全局静态变量,局部静态变量都在静态存储空间。局部变量在栈(虚拟机栈)中分配空间
- 全局变量初始化需要赋值,局部变量不需要赋值
- 一个类中不能声明同名全局变量,一个方法中不能声明同名局部变量。若全局变量和局部变量同名,则在方法中全局变量不生效。
大致流程
当JVM碰到new字节码的时候,会先判断类是否已经初始化,如果没有初始化(有可能类还没有加载,如果是隐式装载,此时应该还没有类加载,就会先进行装载、验证、准备、解析四个阶段),然后进行类初始化。 如果已经初始化过了,就直接开始类对象的实例化工作,这时候会调用类对象的方法。
类初始化的时机
- 初始化main方法的主类
- new 关键字触发,如果类还没有被初始化
- 访问静态方法和静态字段时,目标对象类没有被初始化,则进行初始化操作
- 子类初始化过程中,如果发现父类没有初始化,则先初始化父类
- 通过反射API调用时,如果类没有初始化,则进行初始化操作
- 第一次调用java.lang.invoke.MethodHandle 实例时,需要初始化 MethodHandle 指向方法所在的类。
类的实例化触发时机
- new 触发实例化,创建对象
- 反射,class.newnIstance()和constructor.newnIstance()方法触发创建对象
- Clone方法创建对象
- 使用序列化和反序列化的机制创建对象
类的初始化和类的实例化的区别
类的初始化:为静态成员赋值,执行静态代码块 类的实例化:执行非静态方法和构造方法
- 类的初始化只会执行一次,静态代码块只会执行一次
- 类的实例化会执行多次,每次实例化执行一次
在类都没有初始化完毕之前,能直接进行实例化相应的对象吗?
正常情况下是先类初始化,再类实例化
在非正常情况下,比如在静态变量中
public class Run {
public static void main(String[] args) {
new Person2();
}
}
public class Person2 {
public static int value1 = 100;
public static final int value2 = 200;
public static Person2 p = new Person2();
public int value4 = 400;
static{
value1 = 101;
System.out.println(“1”);
}
{
value1 = 102;
System.out.println(“2”);
}
题外话
不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊
这里我为大家准备了一些我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~
欢迎评论区讨论。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
e方法创建对象
4. 使用序列化和反序列化的机制创建对象
类的初始化和类的实例化的区别
类的初始化:为静态成员赋值,执行静态代码块 类的实例化:执行非静态方法和构造方法
- 类的初始化只会执行一次,静态代码块只会执行一次
- 类的实例化会执行多次,每次实例化执行一次
在类都没有初始化完毕之前,能直接进行实例化相应的对象吗?
正常情况下是先类初始化,再类实例化
在非正常情况下,比如在静态变量中
public class Run {
public static void main(String[] args) {
new Person2();
}
}
public class Person2 {
public static int value1 = 100;
public static final int value2 = 200;
public static Person2 p = new Person2();
public int value4 = 400;
static{
value1 = 101;
System.out.println(“1”);
}
{
value1 = 102;
System.out.println(“2”);
}
题外话
不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊
这里我为大家准备了一些我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~
[外链图片转存中…(img-pbKIqIvy-1714878257856)]
欢迎评论区讨论。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!