目录
参考
一、JAVA基础
常见面试题
1、final、finally、finalize区别
(1)final是Java关键字可以用来修饰类,方法和变量
修饰类时,该类不能被其他类所继承;
修饰方法时,该方法不能被重写;当继承的父类中有被final修饰的方法,子类无法继承该方法
修饰变量时,该变量不能被修改。
(2)finally只可以用在try/catch中
(3)finalize在对象被回收的时候被调用,一般是调用native()方法后用此方法
2、String、StringBuilder、StringBuffer区别
1.String 因为被final修饰,所以不可被继承,修改;StringBuilder、StringBuffer可以被修改
2.String 线程安全,StringBuffer线程安全,因为很多方法用synchronized 修饰,StringBuilder线程不安全
String
参考
该类被final修饰,参数也被final修饰。
当String中字符串相加时,底层用的是StringBuilder去相加,然后将结果转成String返回。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
}
3、重载和重写的区别
1.重载:存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
【返回值不同,其它都相同不算是重载】rklq ,1;.,m k
其他修饰符可以相同,也可以不同
可以抛出不同异常
2.重写:存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。使用 @Override 注解。
4、抽象类和接口类的区别
关键字不同
抽象类要被 abstract 修饰, 如果一个类中包含抽象方法,那么这个类必须声明;
接口类要 interface 修饰实现方式不同
抽象类与普通类相比,只能能被继承extends,不能能实例化;
接口类是被实现 implements。一个类可以实现多个接口,但是不能继承多个抽象类
接口的字段、方法只能是 static 和 final 类型的,而抽象类的字段没有这种限制
接口的成员默认都是 public 的,并且不允许定义为 private 或者 protected;而抽象类的成员可以有多种访问权限。
JDK1.8中对接口增加了新的特性:
- 增加默认方法:使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;
- 增加静态方法(static method):使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名)
5、Session和cookie的区别
相同点:都是浏览器上用来跟踪用户身份的会话方式。
不同点:
- Session 存储在服务端;Cookie存储在客户端
- Session可以存储任意数据类型;Cookie知识一段字符串
所以,Session不能随意篡改,但是时间久了或者用户量大会数据堆积,给服务器带来压力
cookie黑客可以根据获取浏览器的信息来伪造,不安全。所以尽量存放不重要的东西
Json Web Token(JWT)
由3部分组成,中间用点隔开:head.payload.singurater
6、enquals和==的区别
可以理解成等价与相等。
①在基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法
②在引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
【对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
补充:enquals和hashCode()区别
在object类中,equals方法比较的是地址(即比较两个对象的地址值),hashcode()方法是本地方法,返回的是对象的地址值;
String类中enquals比较的是具体数据,因为String对enquals重写了。以此类推,可以知道Integer、Double等封装类中经过重写的equals()和hashcode()方法也同样适合于这个原则。
7、get和post请求的区别
get一般用于查询/获取资源,post一般用于更新资源。
1、get请求参数拼接在Url上,用?隔开,参数之间用&相连,只支持 ASCII 码;post传输请求体
2、POST的安全性要比GET的安全性高
3、GET是幂等的,连续调用多次,客户端接收到的结果都是一样的;POST不是幂等的,如果调用多次,就会增加多行记录。
8、int和Integer的区别
拆箱与装箱问题
【注】 Integer 等包装类也不能被继承
如下图案例,首先要明白,常量池中,int范围是-128~127;直接赋值时,首先从常量池先直接获取,拿不到,才会new 一个;当Integer 与int比较时,Integer会自动解封装;
控制台打印结果:
9、关键字
final:
static
10、jdk和jre的区别
1.Jre【java runtime environment】: Java运行环境
2.jdk【java develop kit】:Java开发,包含了jre
11、列出5种常见的runtime exception
【上图引自公众号<Java后端技术>】
Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: Error 和 Exception。其中 Error 用来表示 JVM 无法处理的错误。Exception 分为两种:受检异常、非受检异常 。
在图中,红色都是受检异常,它们必须被捕获,或者在函数中声明为抛出该异常。
12、JAVA和C++区别
JAVA | C++ | |
---|---|---|
跨平台 | 通过虚拟机从而实现跨平台特性 | 依赖于特定的平台 |
垃圾回收 | 自动回收 | 手动回收 |
指针 | 无,它的引用可以理解为安全指针 | 有 |
类继承 | 一次只可继承一个 | 可以多重继承 |
goto | 保留字 | 直接可用 |
二、JAVA常见集合
List
对象集合, 排列有序,数据可重复
for循环、循环迭代器循环
删除和插入会改变位置;查询速度快(二分查找)
ArrayList
底层:数组
特点:查询速度快;线程不安全
扩容:默认加载因子:1,默认容量 :10,在add元素时候才开始扩容
扩大后新容量是旧容量的1.5倍int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1);
- 总结:
ArrayList是List接口的实现类,数据底层使用的是数组。初始化的时候给定初始容量,当调用add()方法的时候会判断是否需要扩容,需要扩容的化grow()方法就会调用动态扩展数组长度函数,即当前长度+当前长度右移一位(大约是原长度的1.5倍),再调用Array.copy函数将elementData数组复制到新的数组中。
Vector
底层:数组
特点:查询速度快,线程安全,因为其方法被synchronized修饰了
扩容:默认加载因子:1,初始(默认)容量 - 10,在insert()\add()时候才扩容
扩大后新容量为原容量的2倍int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
- 总结:Vector类似ArrayList,其方法被synchronized关键字修饰了,所以线程安全,另外扩容机制上长度为原容量的2倍。
LinkedList
底层:双向链表
特点:查询慢,增删快(首尾元素);线程不安全
扩容:没有初始化大小,没有扩容机制:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
transient int size = 0;
// 定义了首位节点
transient Node<E> first;
transient Node<E> last;
//...
}
// Node节点
static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
Map
键值对集合,无序、没索引,
不重复(其key值不允许重复) - 注意key为null,只能有一个,value 为null ,可以多个
HashMap
底层:Node类型的数组+ 链表 + 红黑树
特点:线程不安全,键和值都可存放null,键只能存放一个null,键为null时存放入table[0]
扩容:
- 触发条件
数组大小超过临界值: 默认大小是16,加载因子是0.75,所以当数组长度>12的时候就会扩容。扩容倍数为2。
Hash碰撞:- 原理
1、如果底层的table数组为空,或者长度等于0,就扩容(第一次扩容到16)
2、添加key-value时候,根据key的hash值在table中得到索引i,先判断该索引下(table[i])数据是否为null?是null,就新增(新增后要判断下是否扩容);
3、有元素,判断table[i]的key(首个元素的Key)是否和要加入元素的key相等?相等,则覆盖值;
4、如果不相等,判断当前元素table[i]是否是树节点还是链表节点?是树节点,插入键值对(添加后,判断是否需要扩容)
5、不是树节点,遍历链表,同时记录遍历长度 -
5.1、遍历查看是否key已存在,存在则直接覆盖值,否则,添加元素
5.2、长度大于8,且table的容量小于64,则转换成红黑树
能不能直接使用key的hashcode值计算下标存储?
如果使用直接使用hashCode对数组大小取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让
hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动。static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // (h >>> 16)是无符号右移16位的运算,右边补0,得到 hashCode 的高16位 // (h = key.hashCode()) ^ (h >>> 16) 把 hashCode 和它的高16位进行异或运算,可以使得到的 hash 值更加散列,尽可能减少哈希冲突,提升性能。 }
引入了红黑树目的
是避免单条链表过长而影响查询效率
LinkedHashMap
底层:哈希表
特点:有序、不重复
每个键值对元素又额外的多了一个双链表的机制记录元素顺序
TreeMap
底层:Entry
private transient Entry<K,V> root;
特点:按大小默认排序,不重复1、把实现了 Comparator接口的匿名内部类(对象),传给了TreeMap的comparator
2、调用put()方法
- 第一次添加,将Entry(key-value)对象封装放到root
- 后面添加时候,遍历key,并且根据Comparator比较,若key相等就不添加
所以不会重复Key
Hashtable
HashMap 类大致等价于 Hashtable,只不过它是 线程不安全的,并允许 null;
Hashtable被synchronized修饰,线程安全
== ConcurrentHashMap ==
底层:Node数组+链表+红黑树
特点:线程安全(CAS+Synchronized来保证并发更新的安全)
对于红黑树数据结构中的Node节点的val和next数据都用volatile保证,保证可见性;
查找,替换,赋值操作都是用CAS算法
原理:
1、初始化数组长度
2、put数据时候,检测是否有Hash冲突,没有就采用CAS机制插入
3、存在Hash冲突,会加锁保障线程安全;
3.1 、链表上新增,若当前长度没有超过8,则在链表上新增数据(遍历到尾端插入)
3.2 、链表转换成红黑树后,再按树结构新增数据
4、新增完数据,记录一下长度size,判断是否需要扩容
Set
排列无序号(添加数据的顺序和获取的数据顺序不一致);数据不能重复(若有重复值会自动覆盖);无索引
遍历用循环迭代器
删除和插入不会改变其位置
HashSet
底层: HashMap<E,Object> map
特点:无须;数据不可重复【使用HashMap 的 key 不能重复机制来实现没有重复的 HashSet】;运行null,但只能放一个;线程不安全
扩容
TreeSet
底层:TreeMap
特点:
扩容
public TreeSet() {
this(new TreeMap<>());
}
LinkedHashSet
底层:实际维护的是LinkedHashMap
特点:加入顺序和取出元素,数据的顺序一致;
扩容
常见面试题
线程安全的集合类有哪些
Vector(List)、ConcurrentHashMap(Map)、HashTable(Map)
三、进程和线程
1、进程和线程的概念
- 进程是一个程序的运行过程。一个进程会有多个线程组成;
- 进程是资源的最小分配单位;线程是程序执行的最小单位
- 进程有独立的堆栈空间和数据段,正因为每个进程都有自己的独立空间,所以保证了一定的安全性
- 一个线程死掉就会杀死整个进程。
线程生命周期
- 新建 :使用new创建线程后,仅由JVM为其分配内存并初始化其中变量。
- 就绪:一切就绪,只差CPU,一旦调用start()方法就进入就绪状态
- 运行:线程获取到CPU,执行run()里的执行体
就绪时进入运行状态的唯一入口 - 阻塞:当无法获取到线程资源时候
(1)等待状态(阻塞):使用了wait()方法
(2)同步阻塞:获取同步锁失败,需等待其他线程使用完再获取
(3)其他阻塞:sleep()、join()等待线程、发出I/O请求的时候 - 死亡:程序正常运行或者异常结束,推出了run方法
多线程的几种实现方式
1、继承Thread类,重写run()方法
2、实现Runable类,重写run()方法
3、实现Callable接口的call()方法,配合Future实现
4、使用线程池,Excuter、ThreadPoolExcuter
2、并行和并发的概念
并行:同一时刻,多个处理器同时执行多条指令
并发:一个处理器同时处理多个任务 - 同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行
来个比喻:并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头
。
并发编程特性
原子性
指一个操作是不可中断的,要么执行成功要么执行失败(类比事务)
在多线程中,一个线程正在执行就不会被其他线程干扰。
如何保障原子性
- synchornized关键字
- Lock类
- 乐观锁CAS
可见性
当其中一个线程修改了共享变量的值,其他线程能够看到修改的值。
如何保障可见性
- synchornized关键字
- volatile 关键字
- 通过final关键字修饰
- 通过Lock保障
- 通过内存屏障保证可见性
有序性
程序按代码顺序来执行。
JVM 存在指令重排,所以存在有序性问题
如何保障可见性
- synchornized关键字
- volatile 关键字
- 通过Lock保障
- 通过内存屏障保证可见性
可重入性
并发下如何保障修改数据的安全性
悲观锁
乐观锁 (CAS+版本号)
FIFO的缓存队列
3、线程池实现原理
多线程可以实现在后台同时进行多个任务
逻辑(过程)
首先,要了解线程的生命周期和线程池中的关键字段
1、 当任务到来时,先检测当前线程数是否大于核心线程数,
2、小于核心线程数,新建线程并执行任务;否则,检测当前队列是否已满
3、队列未满,放入阻塞队列中等待执行;已满,检测当前线程数是否大于最大线程数
4、否,则新建线程执行任务;否则,根据拒绝策略拒绝任务
关键字段
- 核心线程数
- 最大线程数
- 线程存活时间
- 线程存活时间单位
- 阻塞队列
- 拒绝策略:AbortPolicy(默认)- 拒绝任务,报嘎异常;CallerRunsPolicy - 用当前调用者所在的线程来执行任务;DiscardOldestPolicy - 丢掉队列中最老的任务并执行该任务;DiscardPolicy - 直接丢弃任务并不抛异常
4、线程池的几种实现方式
通过 ThreadPoolExecutor 创建的线程池
通过 Executors 创建的线程池
5、常见问题
wait()和sleep()的区别
- wait() 是属于Object类的方法,线程会放弃对象锁,使用notify()唤醒;
- sleep()是属于Thread的方法,使程序停止运行指定时间,让出CPU(不会释放锁),到了指定时间会自动恢复运行状态。
- yield() 让出当前线程CPU,不会停止线程运行
wait会释放锁,而 sleep 一直持有锁。Wait 通常被用于线程间交互,sleep 通常被用于暂停执行。
wait()和notify()必须在synchronized代码块中调用
线程中start方法与run方法的区别
- start()是新创建一个线程,使线程进入就绪状态,等待CPU,一旦获得资源,就会执行run()线程体。
- 同一个线程,start()只可执行一次,否则会报错,java.lang.IllegalThreadStateException.
- run()可以执行多次,也可没有调用start(),直接执行run(),此时它只是一个普通的方法。
线程中终止方法
2个函数
- stop() - 不建议使用,因为会立即终止线程,可能导致数据不一致
- inturrept() - 安全的方法。会发一个中断信号,线程接受到信号后安全的停止。
如何实现线程安全
一段代码在多个线程同时执行情况下,能付保证结果正确,具体案例参考:
(1)使用线程安全的类;
(2)使用synchronized同步代码块、方法等;
原理:当两个并发线程访问同一个对象object中的被synchronized(this)修饰的同步代码块时,一个时间内同一时间内只能有一个线程得到执行。当这个线程使用结束后, 另一个线程才能执行该代码块。
(3)多线程并发情况下,线程共享的变量改为方法局部级变量;
(4) 采用lock()加锁,unlock()解锁,来保护指定的代码块
总的方式划分为三种:互斥同步锁、非阻塞同步、无同步方案
1. 互斥同步方式(属于一种悲观的并发策略)
一种保证并发正确性的常见手段
互斥手段,达到同步效果
synchronized关键字
被修饰后,会在代码前后形成monitorEnter和monitorExit2个指令来加锁和解锁。
首先,线程会尝试获取锁,
若这个对象没有锁 或者 当前线程已经拥有这个锁,就把锁的计数器加1,当线程使用完后会执行monitorExit,计数器会-1;
否则,获取不到锁时候就会阻塞,等待其他线程释放锁
ReentranLocke类
比synchronized多了一些特性。
- 等待可中断
当线程长期不是放资源时候,等待的线程会主动放弃去执行其他任务- 增加了公平锁
当等待时候,按获取锁的时间来依次获取锁- 可以绑定多个对象,实现选择性通知
借助于Condition接口与newCondition()方法(多次调用newCondition()方法)。
synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。
内部类Sync来具体实现,该类使用了AQS
public class SynDemo{
public static void main(String[] arg){
Runnable t1=new MyThread();
new Thread(t1,"t1").start();
new Thread(t1,"t2").start();
}
}
class MyThread implements Runnable {
// 锁
private Lock lock=new ReentrantLock();
public void run() {
lock.lock();
try{
for(int i=0;i<5;i++)
System.out.println(Thread.currentThread().getName()+":"+i);
}finally{
lock.unlock();
}
}
AQS(AbstractQueuedSynchronizer)抽象队列同步器
是并发编程的基类。
数据结构
- state:被volatile修饰,通过CAS方式修改int类型的state变量
0 - 无线程持有的- 双向链表:
head-链表头节点 - 是一个哨兵节点,不存放实际的“线程”节点
node、
tail-链表尾节点 -
AQS维护的双向链表,通过addWaite() 将没有获取到锁资源的线程被扔到AQS队列中排队】==- ConditionObject中维护的双向链表
过程
A线程占有资源时,会调用 tryAcquire() 方法独占该锁并将 state+1;其他线程再 tryAcquire() 时就会失败,直到A线程释放锁,state 变为0,其它线程才有机会获取该锁
A线程自己是可以重复获取这个锁的,每重入一次,state 就累加一次,获取多少次就要释放多么次,保证 state 能回到零态
- 数据结构
看下图
看下图
- state volatile修饰的
- AQS资源共享 (2种方式)
AQS的state已经使用CAS了,为什么还要用volatile
其setter/getter没有被synchronized修饰。所以必须要volatile,保证可见性
若对数据修改,volatile就不适用了,所以用CAS合适
2. 非阻塞同步(基于冲突检测的乐观并发策略)
先进性操作,如果共享资源没存在竞争,就操作成功了
共享资源存在竞争,产生了冲突,就使用补偿手段(例如重试机制),一直到操作成功
好处:不需要将线程挂起
3. 无同步方案
同步只是保障共享数据的正确性,如果没有共享数据,就不必同步了。
- 可重入代码
允许多个线程同时访问。【为了数据安全,所以不运行任何进程对其修改】
可重入代码,必须保证资源的互不影响的使用,比如全局变量,系统资源等。- 线程本地存储
一段代码中所需要的数据必须与其他代码共享 - 全局变量
大部分的消息队列架构
Java中volatile关键字、ThreadLocal线程。ThreadLocal本地线程存储
就是将共享的数据存储到每个线程本地,这样每个线程拥有的都是该共享数据的副本,以此来限制共享数据的可见范围或可变性。
使用场景
- 每个线程要独享数据
- 每个线程中存放着“全局”数据
结构
存储对象是ThreadLocalMap,- key是ThreadLocal对象,它包含了一个唯一的threadLocalHashCode值
private static AtomicInteger nextHashCode = new AtomicInteger();
- value是实际保存对象
原理
每个线程创立独立的副本、内部使用了安全的数据结构、弱引用与内存管理
内存泄漏问题(对象不再使用但是内存空间却没有释放)
ThreadLocalMap对象的key引用了WeakReference 弱,而value是强引用。当GC时候,弱引用不管内存是否足够都会被清理了。
【简单的GCROOT引用链如下:Thread–>TreadLocalMap–>Entry(null,value)–>value,即Thread和value之间一直存在强引用关系。】
正确用法,调用remove()方法,删除对应的key-value,就会内存泄漏了
8、关键字
(1)volatile:
轻量级同步关键词
- 满足特性
满足可见性,不保证原子性;禁止指令重排- 场景
volatile最适合使用的地方是一个线程写、其它线程读的场合
(2)synchronized
可以修饰静态方法、成员函数,同时还可以直接定义代码块。
- synchronized的锁到底是加给谁了?
static修饰的静态方法、静态属性都是归类所有,同时该类的所有实例对象都可以访问;
普通成员属性、成员方法是归实例化的对象所有。
- 满足特性
原子性、可见性、有序性、可重入性- 根据对象头中的锁 状态标志位和锁记录来实现
四、锁机制
锁是一种同步机制,在并发中协调线程的运行,保证共享资源的安全
场景:
1、JAVA:悲观锁是sync,乐观锁就是原子类。
2、数据库:悲观锁for update(锁定行查询);乐观锁version字段,与上一次版本做比较,若一样,则更新,否则,重新读写一遍。
1、乐观锁
先进行业务操作,不到万不得已不去拿锁。响应速度高、回滚开销大。
乐观锁的ABA问题 - 增加版本标识来解决
适合并发读频繁而写不频繁的场景,例如缓存并发控制
理论逻辑:CAS算法涉及到三个操作的数,需要读写的内存值V,进行比较的值C,拟写入的新值N。
当且仅当V值等于C值时,CAS算法通过原子方式用新值N更新内存中的值V,否则不会执行任何操作。
ABA是什么?
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。
怎么解决ABA:通过版本号。
1、one线程更新数据,先查询版本号,
2、提交数据时候,先判断当前版本号是否和查询到的一致
3、一致则更新;否则,继续以上步骤
2、悲观锁
先获取锁成功后再进行业务操作。冲突频率高、回滚开销小。
适合并发读写屏藩的场景,例如数据库事务。
3、其他锁
偏向锁、轻量级锁和重量级锁,其中重量级锁是最常用的一种锁实现
偏向锁
如果一个线程获得了锁,那么锁就进入偏向模式。
在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。
在中国过程中,只有检查锁,无需上锁,因为是自己本身
轻量级锁
当第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。(2个不同线程一前一后地交替执行同步块)
轻量级锁失败后,为了避免线程挂起,有了自旋锁。
这个过程不释放CPU,让线程执行循环等待锁的释放:得到锁就进入临界区,否则就在操作系统挂起。
这一点会消耗性能
重量级锁
当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁
4、死锁
多个线程抢占资源陷入僵局,必须有外力介入,否则这些进程都无法继续下去。
必须满足4个条件
- 互斥:一个资源只能被一个线程占用,其他线程处于等待状态
- 不剥夺权利:当一个线程占有资源后,其他线程不能强制获取资源
- 请求和保持:当进程请求资源失败阻塞后,对现有的资源不释放
- 循环和等待:存在一个循环等待队列,形成一个回路,A占有B的资源,B占有C的资源,C占有D的资源,D占有A的资源
如何避免
- 给予资源:从其他进程剥夺足够的资源
- 撤销进程:直接撤销死锁进程、撤销代价最小的进程
- 一次性分配:创建进程时候,要么满足他所有的需求,要么什么都不给
- 银行家算法
五、JVM
- 含义:JVM就是Java虚拟机。职责就是运行Java字节码文件。
- 功能:编写一次,到处运行的跨平台特性。
- 组成:4部分,类加载器、执行引擎、内存区(运行时数据区) - JVM管理的部分
本地方法(本地接口)、本地方法库 - JVM不管理,直接在内存中,底层是C/C++实现
组成详解
类加载器
- 作用:加载字节码文件到内存中。
- 对应着生命周期中的三个阶段:加载 - 连接 - 初始化
Bean的生命周期
类生命周期:加载- 连接 - 初始化 - 使用 - 卸载
(1)加载
(2)连接
- 验证: 版本号、文件类型、关键字等
- 准备: 给静态变量分配内存和初始值
- 解析: 将符号引用变为直接引用
(3)初始化
加载机制-双亲委派
- 双亲委派:自底向上查找是否被加载过,若加载过则直接返回;若无,再由顶向下进行加载。
- 好处:避免恶意代码替换JDK的核心库;避免类被重复加载。
- Bootstrap ClassLoader 启动类加载器
用来加载Java的核心库,是用原生代码来实现的- Extension ClassLoader 扩展类加载器【java9后变更为模块类加载器】
根据Java应用的类路径来加载Java类- App ClassLoader 应用类加载器
根据Java应用的类路径来加载Java类
打破双亲委派:可继承ClassLoader类后,重写findClass方法
反射
- 反射是什么?
JVM通过.class ,反编译后找到对应.java,从而获取对象的各种信息、操作对应的方法和属性- 作用?
在程序运行时动态加载类并获取类的详细信息,可以动态的创建对象、修改对象的属性值、调用对象的方法- 原理
看下图
1、java文件会变编译成class文件,然后被ClassLoader加载到JVM内存中
2、当一个类被加载后,JVM会自动再内存中加载一个Class对象(通过Class对象获取Field/Method/Construcor)
3、平时通过new的形式创建对象,实际上就是通过这些Class来创建的【这个class文件是编译的时候就生成的,程序相当于写死了在jvm】
4、但实际情况中,有些类不用一开始加载到JVM,而是运行时候再加载
5、使用反射,可以只传入类名参数,就可以生成对象,降低了耦合性,使得程序更具灵活性。
这些类都位于java.lang.reflect包中
类对象获取方式- Class
要操作一个类的字节码,需要首先获取到这个类的字节码,怎么获取java.lang.Class实例
- 对象.getClass()
- 类名.class
- Class.forName(“完整类名带包名”) — 最常用
// 方式一:Class.forName()
Class class1 = Class.forName("com.example.learndemo.struct.BinarySearch");
//对象调用getClass()方法
BinarySearch obj2 = new BinarySearch();
Class class2 = obj2.getClass();
//3、任何类型.class
Class class3 = BinarySearch.class;
通过反射实例化对象
要操作一个类的字节码,需要首先获取到这个类的字节码,怎么获取java.lang.Class实例
- 通过Class对象调用newInstance()【Class.forName(“类的全限定名”).newInstance()】— java9开始被废弃了,替换为Class.forName(“类的全限定名”).getDeclaredConstructor().newInstance();
- 通过Constructor(构造方法)对象调用newInstance()
// 2、通过构造方法实例化对象
Class class1 = Class.forName("com.example.learndemo.struct.BinarySearch");
Constructor constructor = class1.getConstructor(String.class);
constructor.newInstance("xpp");
通过反射动态调用方法 - Method
- 获取方法
getDeclaredMethods() — 所有方法
getMethods() — 所有公有方法- 调用方法
method.invoke(类实例, 实参)
// 获得一个私有方法
Method methods = class1.getDeclaredMethod("test", String.class);
// 只有私有方法需要加,否则忽略
methods.setAccessible(true);
// 实例化对象
Object obj = class1.getDeclaredConstructor().newInstance();
// 调用方法
methods.invoke(obj, "aaa");
通过反射动态修改属性 - Field
- 获取字段
getDeclaredFields() — 所有字段
getFields() — 所有公有字段- 修改字段值
field.set(类实例, 实参)
// 获得calss对象
Class class1 = Class.forName("com.example.learndemo.struct.BinaryTreeData");
// 获得字段
Field field = class1.getDeclaredField("data") ;
//类实例
Object obj = class1.getDeclaredConstructor().newInstance();
//打开修饰符访问权限
field.setAccessible(true);
//字段赋值
field.set(obj,20);
执行引擎
- 作用:对字节码文件指令做解析,翻译成机器码,解析完成后提交到操作系统中。GC功能
内存区(运行时数据区)
- 含义:JVM核心内存空间结构模型
- 组成:程序计数器、栈(虚拟机栈、本地方法栈)、方法区、堆
程序计数器
本地方法栈
对应的方式是本地方法区提供的方法,称为native()方法。和虚拟机栈存储在一起,且结构组成也一样。详情查看虚拟机栈。
虚拟机栈
虚拟机栈就是一个栈,栈里存的东西叫做栈帧 - 栈帧对应的是一个方法的调用过程。
栈帧组成
- 局部变量
- 操作数栈
- 帧数据:方法出口、链接、异常表信息
堆
存放新创建的对象。这个区域较大,为了垃圾回收方便,因此划分为:
新生代
- 伊甸园区
- 幸存区
老年代
方法区
元数据区:包含了常量池
本地接口、本地方法库 (不归JVM)
这是由C/C++实现的,直接存在内存中(不是Java虚拟机),可直接调用
垃圾回收
判断是否可回收的算法
引用标记算法
可达性算法
三色标记算法
黑:表示对象被引用、
灰:表示搜索对象的引用还未查询结束、
白:表示对象没有引用
垃圾回收算法
标记清除算法
步骤:1、标记对象,2、删除被标记对象
好处:简单易实现
缺点:内存碎片,不易于后续给对象分配内存
标记复制算法
步骤:1、将内存分为2块等大小区域,2、每次只使用其中一个区域,3、当该区域满了,就会触发GC,会将该区域中未使用的对象复制到另一个区域中,4、然后删除这个区域中的所有对象5、交换两个区域的角色
这是一种Stop The Word算法
好处:解决内存碎片化、吞吐大
缺点:内存占用率大
标记整理算法
步骤:1、标记对象 2、将被标记对象对象移动到一起,3、按分界线删除
好处:解决内存碎片化
缺点:效率低
分代回收算法
将内存区域分成新生代和、老年代、永久代(元数据区);新生代又分为伊甸园和幸存区
永久代(Permanent Generation):永久代用于存放静态文件、类信息等,一般不进行垃圾回收。在Java 8 及以后的版本中,永久代被元数据区
过程:新创建的对象先在Edge区,伊甸园区满了进行新生代回收(将存活的对象放到其中一个幸存区,然后清空Edge区)
这个过程不断重复,存活的对象达到一定次数(默认15次),该对象会晋升到老年区
好处:
提高了效率 - 不同生命周期的对象采用不同的回收算法
减少停顿时间 - 回收一般发生在年轻代上
提高了内存使用率 - 年轻代整理算法,内存碎片化问题解决
垃圾回收器
CMS - 多线程标记清除回收器
- 步骤:
- 初始标记:寻找和GC Root直接相关的对象,
会停止所有用户线程
- 并发标记: 进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程
- 并发预清理
- 重新标记:修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,
会停止所有用户线程
- 并发清理:清除 GC Roots 不可达对象
- 并发重置
在这个过程中,GC线程和用户线程在并发进行,所以重新标记是为了处理这段过程中发生改变的对象
- 缺点:对CPU资源敏感、会产生浮动垃圾、还是标记-清除算法的缺点
- 优点:并发收集、低停顿
G1 -
重新定义了堆空间。 G1从整体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。
步骤:
1、初始标记,和GCRoot直接关联的对象
2、并发标记:通过可达性算法找到所有相关的存活对象
3、最终标记:修正并发标记期间( 因用户程序继续运作而导致标记产生变动的那一部分标记记录,索引需要再标记一次)
4、筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
ZGC
JAVA17默认使用该回收器
常见问题
对象创建方式
- 使用关键字new来创建对象,需要声明、实例化和初始化1。
- 调用java.lang.Class的newInstance()。
- 调用对象的clone()方法。
- 利用反序列化创建对象。
ApplicationContext通常的实现是什么?
- FileSystemXmlApplicationContext
此容器从一个 XML文件中加载beans的定义,XMLBean配置文件的全 路径名必须提供给它的构造函数。- ClassPathXmlApplicationContext
此容器也从一个 XML文件中加载beans的定义,这里,你需要正确设置 classpath 因为这个容器将在classpath里找bean配置。- WebXmlApplicationContext
此容器加载一个XML 文件,此文件定义了一个WEB应用的所有bean。
内存溢出问题该如何解决
内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存
1、修改JVM启动内存 -Xms、-Xmx
2、检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误
3、对代码进行走查和分析,找出可能发生内存溢出的位置
Java17相较于Java8的垃圾回收机制改进
Java8 使用的是G1垃圾回收器,java17使用ZGC
都使用了标记复制算法。
ZGC:初始标记、再标记、初始转移
https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html
cms和g1区别
CMS | G1 | |
---|---|---|
算法 | 基于标记-清除算法 | 基于标记-整理算法 |
使用 | 是针对老年代回收的,新生代回收需要配合其他垃圾回收器,Seria、Parnew | 覆盖老年代和新生代,不需要配合其他垃圾回收器使用 |
停顿时间 | 以最小停顿时间为目标 | 允许用户指定最大停顿时间 |
内存 | G1比CMS大 |
相比与 CMS 收集器,G1 收 集器两个最突出的改进是:
- G1基于标记-整理算法,不产生内存碎片。
- 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收
CPU飙升但是内存没有报警,如何排查
七、网络I/O基础
八、数据结构
1、树
二叉数
package com.example.learndemo.struct;
import lombok.Data;
/**
* 二叉排序树(二叉查找树)是最简单的树表查找算法,
* 该算法需要利用待查找的数据,进行生成树,确保树的左分支的值小于右分支的值,然后在就行和每个节点的父节点比较大小,然后再进行查找。
* 1、若左子树不空,则左子树上所有结点的键值均小于或等于它的根结点的键值。
* 2、若右子树不空,则右子树上所有结点的键值均大于或等于它的根结点的键值。
* 3、左、右子树也分别为二叉排序树。
*/
public class BinaryTree {
public static void main(String[] args) {
int[] array = {35,76,6,22,16,49,49,98,46,9,40};
BinaryTreeData root = new BinaryTreeData();
root.setData(array[0]);
for (int i = 1; i < array.length; i++) {
createBST(root, array[i]);
}
System.out.println("中序遍历结果:");
midOrderPrint(root);
System.out.println();
// searchBST(root, 22, null);
// searchBST(root, 100, null);
}
public static void createBST(BinaryTreeData root, int element){
BinaryTreeData node = new BinaryTreeData();
node.setData(element);
if(element < root.getData()) {
// 左节点
if(root.getLeft() == null) {
root.setLeft(node);
} else {
createBST(root.getLeft(), element);
}
} else if(element > root.getData()){
// 右节点
if(root.getRight() == null) {
root.setRight(node);
} else {
createBST(root.getRight(), element);
}
} else {
//System.out.println("【" + element +"】该节点已经存在");
return;
}
}
/*二叉树的中序遍历*/
public static void midOrderPrint(BinaryTreeData rt){
if(rt != null){
midOrderPrint(rt.getLeft());
System.out.print(rt.getData() + " ");
midOrderPrint(rt.getRight());
}
}
/*二叉树中查找元素*/
public static void searchBST(BinaryTreeData root, int target, BinaryTreeData p){
if(root == null){
System.out.println("查找"+target+"失败");
}else if(root.getData() == target){
System.out.println("查找"+target+"成功");
}else if(root.getData() >= target){
searchBST(root.getLeft(), target, root);
}else{
searchBST(root.getRight(), target, root);
}
}
}
/**
* 树节点
*/
@Data
class BinaryTreeData{
private int data;
private BinaryTreeData left;
private BinaryTreeData right;
}
平衡二叉树
B数 - 多路平衡搜索树
主要是为外部存储器设计的,例如磁盘。
优于普通二叉树:
- 有效降低了树的高度、搜索速度快
- 使用
。理由?
MongoDb是聚合性数据库,B-树恰好是 key 和 data 域聚合在一起。
B+树 - B树的变种
- 变化:
1、所有的关键字(数据)存储在叶子节点,非叶子节点不存储真正的数据
2、为所有叶子节点增加了一个链指针- B树和B+树的区别
1、B+树内节点不存储数据,所有 data 存储在叶节点导致查询时间复杂度固定为 log n。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好情况下是O(1)。
由于B-树节点内部每个 key 都带着 data 域,而B+树节点只存储 key 的副本,真实的 key 和 data 域都在叶子节点存储
2、 B+树叶节点两两相连可大大增加区间访问性,可使用在范围查询等,而B-树每个节点 key 和 data 在一起,则无法区间查找。- 使用
Mysql InnoDb。理由?
1、叶子节点通过指针串联在一起,大大增加了区间访问性(很容易进行区间遍历或者全局遍历)
2、查询效率稳定,数据全部存储在叶子节点上,时间复杂度为 O(log n)
3、B+树更适合外部存储,因为非叶子节点不存放data,每个节点能索引的范围更大
2、二分搜索
package com.example.learndemo.struct;
import com.example.learndemo.LearnDemoApplication;
/**
* 时间复杂度 O(lgN)
* 二分查找:是一种在有序数组中查找某一特定元素的查找算法
* 当查找表不会频繁有更新、删除操作时,使用折半查找是比较理想的
* 逻辑:
* 用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;
* 若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,
* 这样递归进行,直到查找到或查找结束发现表中没有这样的结点。
*/
public class BinarySearch extends LinkageError {
public static void main(String[] args) {
int[] x1 = {1,2,3,4,5,6,7};
int k = 7;
System.out.println(search( k, x1, x1.length));
}
public static int search(int k, int[] array, int len){
// 记录坐标
int mid = -1;
int left = 0, right = len - 1;
while (left <= right) {
mid = (left + right) / 2;
if(k > array[mid]){
left = mid-1;
} else if (k < array[mid]) {
right = mid-1;
} else {
return mid;
}
}
return mid;
}
}
九、设计模式
基本特性
单一职责原则
里氏替换原则
依赖倒置原则
接口隔离原则
迪米特法原则
开闭原则
常用模式
1、单例模式 - 创建型设计模式 - 违反了单一职责原则
定义 :确保一个类只有一个实例,在全局只有一个访问点
使用场景
- 数据库连接方式(JDBC)
- Spring 容器中,每个Bean默认就是单例,可以方便管理Bean生命周期
- 配置文集的读取
- 线程池的管理
- 生成唯一序号ID
实现方式(5种)
- 懒汉式(用的时候才创建)
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance;
- 饿汉式(初始化时候就创建了)浪费内存
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }
- 双检索
在sychonized关键字前后都进行了判空,第一次判空减少性能开销,第二次判空避免生成多个对象实例。public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
- 静态内部类(线程安全)
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } // ...... }
- 枚举enum
public enum EnumSingleton { INSTANCE; public void whateverMethod() { } // .... }
2、观察者模式
- 定义:
- 实现方式
- 使用场景
3、装饰者模式
定义:不改变当前对象情况下,扩展对象功能
使用场景
不改变当前对象情况下,只扩展对象功能,而且可以随意装配或者卸载
Java中的I/O,如 FileInputStream、ByteArrayInputStream、BufferedInputStream 等
不能使用继承的情况下,例如被final修饰了
实现方式
首先要知道其中三个
- 抽象组件角色:规定被装饰的对象的一些行为
- 具体组件角色:
- 抽象装饰器类:扩展抽象组件角色
- 具体装饰器类:
//1、 抽象组件类
public interface Order{
public void seal ();
}
//2、 具体组件类
public class ZheKou1Order implements Order{
public void seal (){
// 折扣1售卖,例如优惠券
}
}
public class ZheKou2Order implements Order{
public void seal (){
// 折扣2售卖,例如红包
}
}
//3、 抽象装饰器类
public abstract class OrderDecorator implements Order{
private Order orderDecorator;
public OrderDecorator (Order orderDecorator){
this. orderDecorator = orderDecorator;
}
public void seal (){
}
public void seal2 (){
}
}
//4、具体装饰器类
public class ExtOrderDecorator extends OrderDecorator{
OrderDecorator(Order orderDecorator){
super(OrderDecorator)
}
public void seal (){
}
@Override
public void seal (){
//增加扩展代码2
}
}
//5. 具体使用
main(){
Order order = new ExtOrderDecorator();
order.seal();
}
5、工厂模式
- 定义:一种创建对象的方式。
- 实现方式
抽象工厂模式
为创建一组相关或相互依赖的对象提供的一个接口,无须指定它们的实现类。
使用场景:
在不同条件下要创建不同的实例
// 1、==============抽象工厂类==============
public abstract class AbstractFactory {
public abstract Color getColor(String color);
public abstract Shape getShape(String shape);
}
//2、==============图形工厂==============
public class ShapeFactory extends AbstractFactory {
@Override
public Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("CIRCLE")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("SQUARE")){
return new Square();
}
return null;
}
@Override
public Color getColor(String color) {
return null;
}
}
// 颜色工厂
public class ColorFactory extends AbstractFactory {
@Override
public Shape getShape(String shapeType){
return null;
}
@Override
public Color getColor(String color) {
if(color == null){
return null;
}
if(color.equalsIgnoreCase("RED")){
return new Red();
} else if(color.equalsIgnoreCase("GREEN")){
return new Green();
} else if(color.equalsIgnoreCase("BLUE")){
return new Blue();
}
return null;
}
}
//=================工厂生产对应的类==============
public class FactoryProducer {
public static AbstractFactory getFactory(String choice){
if(choice.equalsIgnoreCase("SHAPE")){
return new ShapeFactory();
} else if(choice.equalsIgnoreCase("COLOR")){
return new ColorFactory();
}
return null;
}
}
//================= 具体使用==============
//获取抽象工厂
AbstractFactory shapeFactory = FactoryProducer.getFactory("SHAPE");
//获取形状为 Circle 的对象
Shape shape1 = shapeFactory.getShape("CIRCLE");
//获取颜色工厂
AbstractFactory colorFactory = FactoryProducer.getFactory("COLOR");
//获取颜色为 Red 的对象
Color color1 = colorFactory.getColor("RED");
//调用 Red 的 fill(填充) 方法
color1.fill();
4,策略模式
策略模式将算法封装在独立的Strategy类中使得
你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展
需要注意的是:每添加一个策略就要增加一个类,当策略过多是会导致类数目庞大
//1、定义Strategy接口
public interface LoginStrategy{
boolean verity(String code);
}
//2、策略实现类
public class QQLoginStrategy implements LoginStrategy{
boolean verity(String code){}
}
public class DingLoginStrategy implements LoginStrategy{
boolean verity(String code) {}
}
//3、Context
public class LoginContext{
private LoginStrategy loginStrategy;
public LoginContext(LoginStrategy loginStrategy){
this.loginStrategy = loginStrategy
}
boolean verity(String loginCode) {
return loginStrategy.verity(loginCode);
}
}
// 4、使用时候,
public class Main{
LoginContext context = new LoginContext(new QQLoginStrategy);
context.verity("test");
}
5,MVC模式
常见问题
数据库点这里
缓存点这里
分布式点这里
消息队列
Spring、SpringBoot、SpringCloud点这里
常见面试题
类名以stream结尾的都是字节流;类名以reader或者writer结尾的都是字符流
输入流和输出流是相对于什么而言?
输入流和输出流是相对于内存而言,那从磁盘中读取的流肯定是输入流了,所以以Reader结尾的肯定是输入流