1 Java基础
1.1、Java里的接口和抽象类的区别
- 抽象类中可以有抽象方法,也可以有非抽象方法共存
- 接口,jdk1.8之前只能有抽象方法,jdk1.8之后可以存在一个默认default方法
- 抽象类只能单继承,接口可以实现多个父接口
- 抽象类可以存在普通成员变量,接口只能存在static final的变量,必须初始化
分别在什么情况下使用:
根据抽象类和接口的不同之处,当我们仅仅需要定义一些抽象方法而不需要其余额外的具体方法或者变量的时候,我们可以使用接口。反之,则需要使用抽象类,因为抽象类中可以有非抽象方法和变量。
1.2、final关键字不可变
- 当final修饰基本变量类型时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变 。
- 当final修饰引用类型变量时,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但是这个对象(对象的非final成员变量的值可以改变)完全可以发生改变。
1.3、Java中的集合,哪些线程安全,哪些不安全?
安全:
List:
- Vector
- CopyOnWriteArrayList
- Collections.synchronizedList(new ArrayList<>());
Map:
- HashTable
- ConcurrHashMap
- Collections.synchronizedMap(new Hashtable<String,Object>());
Set:
- CopyOnWriteArraySet
- Collections.synchronizedList(new HashSet<>());
不安全:
-
ArrayList
-
HashMap
-
HashSet
1.4、HashMap和ConcurrentHashMap底层有哪些不一样的
HashMap底层:HashMap底层实现数据结构为数组+链表的形式,JDK8及其以后的版本中使用了数组+链表+红黑树实现,解决了链表太长导致的查询速度变慢的问题。
HashMap的初始容量,加载因子,扩容增量是多少?
HashMap的初始容量16,加载因子为0.75,扩容增量是原容量的1倍。如果HashMap的容量为16,一次扩容后容量为32。HashMap扩容是指元素个数**(包括数组和链表+红黑树中)**超过了16*0.75=12之后开始扩容。
HashMap的长度为什么是2的幂次方:
- 我们将一个键值对插入HashMap中,通过将Key的hash值与length-1进行&运算,实现了当前Key的定位,2的幂次方可以减少冲突(碰撞)的次数,提高HashMap查询效率
- 如果length为2的幂次方,则length-1 转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费
- 如果length不是2的幂次方,比如length为15,则length-1为14,对应的二进制为1110,在与h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。
- 也就是说2的N次幂有助于减少碰撞的几率,空间利用率比较大。
ConcurrentHashMap:
ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
jdk1.7中采用Segment + HashEntry的方式进行实现,结构如下:
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。
jdk1.8中的实现,相比于1.7的版本基本上全部都变掉了。首先,取消了Segment分段锁的数据结构,取而代之的是数组+链表(红黑树)的结构。而对于锁的粒度,调整为对每个数组元素加锁(Node)。然后是定位节点的hash算法被简化了,这样带来的弊端是Hash冲突会加剧。因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。这样一来,查询的时间复杂度就会由原先的O(n)变为O(logN)。
1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现
1.5、HashMap、TreeMap区别
1、HashMap是通过hashcode()对其内容进行快速查找的;HashMap中的元素是没有顺序的;
TreeMap中所有的元素都是有某一固定顺序的,如果需要得到一个有序的结果,就应该使用TreeMap;
2、HashMap和TreeMap都不是线程安全的;
3、HashMap继承AbstractMap类;覆盖了hashcode() 和equals() 方法,以确保两个相等的映射返回相同的哈希值;
TreeMap继承SortedMap类;他保持键的有序顺序;
4、HashMap:基于hash表实现的;使用HashMap要求添加的键类明确定义了hashcode() 和equals() (可以重写该方法);为了优化HashMap的空间使用,可以调优初始容量和负载因子;
TreeMap:基于红黑树实现的;TreeMap就没有调优选项,因为红黑树总是处于平衡的状态;
5、HashMap:适用于Map插入,删除,定位元素;
TreeMap:适用于按自然顺序或自定义顺序遍历键(key);
6、HashMap: Key和Value 都可以为 null ( 如果key为null 的话, hashCode=0)
TreeMap: Key不能为null, Value可以为null
1.6、HashMap为什么用红黑树
-
红黑树是”近似平衡“的。
-
红黑树相比avl树,在检索的时候效率其实差不多,都是通过平衡来二分查找。但对于插入删除等操作效率提高很多。红黑树不像avl树一样追求绝对的平衡,他允许局部很少的不完全平衡,这样对于效率影响不大,但省去了很多没有必要的调平衡操作,avl树调平衡有时候代价较大,所以效率不如红黑树,在现在很多地方都是底层都是红黑树的天下啦。
-
红黑树的高度只比高度平衡的AVL树的高度(log2n)仅仅大了一倍,在性能上却好很多。
-
HashMap在里面就是链表加上红黑树的一种结构,这样利用了链表对内存的使用率以及红黑树的高效检索,是一种很happy的数据结构。
-
AVL树是一种高度平衡的二叉树,所以查找的非常高,但是,有利就有弊,AVL树为了维持这种高度的平衡,就要付出更多代价。每次插入、删除都要做调整,就比较复杂、耗时。所以,对于有频繁的插入、删除操作的数据集合,使用AVL树的代价就有点高了。
-
红黑树只是做到了近似平衡,并不严格的平衡,所以在维护的成本上,要比AVL树要低。
-
所以,红黑树的插入、删除、查找各种操作性能都比较稳定。对于工程应用来说,要面对各种异常情况,为了支撑这种工业级的应用,我们更倾向于这种性能稳定的平衡二叉查找树。
1.7、ArrayList是线程安全的么?多线程操作下会发生什么?
不是,多线程并发情况下出现ConcurrentModificationException并发修改异常。
1.8、== 和 equals() 区别
- 对于==,比较的是值是否相等。如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;如果作用于引用类型的变量,则比较的是所指向的对象的地址。
- 对于equals方法,注意:equals方法不能作用于基本数据类型的变量,equals继承Object类,比较的是是否是同一个对象。如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
重写equals()方法就必须重写hashCode()方法:
-
hashCode()方法是一个本地native方法,返回的是对象引用中存储的对象的内存地址,而equals方法是利用==来比较的也是对象的内存地址。从上边我们可以看出,hashCode方法和equals方法是一致的。
-
hashCode()和equals()保持一致,如果equals方法返回true,那么两个对象的hasCode()返回值必须一样。如果equals方法返回false,hashcode可以不一样,但是这样不利于哈希表的性能,一般我们也不要这样做。重写equals()方法就必须重写hashCode()方法的原因也就显而易见了。
-
假设两个对象,重写了其equals方法,其相等条件是属性相等,就返回true。如果不重写hashcode方法,其返回的依然是两个对象的内存地址值,必然不相等。这就出现了equals方法相等,但是hashcode不相等的情况。这不符合hashcode的规则。
1.9、Java异常
Exception和Error:
- Exception是程序正常运行中预料到可能会出现的错误,并且应该被捕获并进行相应的处理,是一种异常现象
- Error是正常情况下不可能发生的错误,Error会导致JVM处于一种不可恢复的状态,不需要捕获处理,比如说OutOfMemoryError
Exception又分为了运行时异常和编译时异常。
**编译时异常(受检异常)**表示当前调用的方法体内部抛出了一个异常,所以编译器检测到这段代码在运行时可能会出异常,所以要求我们必须对异常进行相应的处理,可以捕获异常或者抛给上层调用方。
**运行时异常(非受检异常)**表示在运行时出现的异常,常见的运行时异常包括:空指针异常,数组越界异常,数字转换异常以及算术异常等。
前边说到了异常Exception应该被捕获,我们可以使用try – catch – finally 来处理异常,并且使得程序恢复正常。
2 Java多线程
2.1、进程与线程
java创建线程的三种方式:
- 继承Thread类创建线程类
- 实现Runnable接口
- 通过Callable和Future创建线程
进程与线程之间的主要区别可以总结如下:
- 进程是一个“执行中的程序”,是系统进行资源分配和调度的一个独立单位
- 线程是进程的一个实体,一个进程中一般拥有多个线程。线程之间共享地址空间和其它资源(所以通信和同步等操作,线程比进程更加容易)
- 线程一般不拥有系统资源,但是也有一些必不可少的资源(使用ThreadLocal存储)
- 线程上下文的切换比进程上下文切换要快很多。
线程可以拥有独属于自己的资源吗?
可以的,通过ThreadLocal可以存储线程的特有对象,也就是属于当前线程的资源。
进程之间常见的通信方式:
- 通过使用套接字Socket来实现不同机器间的进程通信
- 通过映射一段可以被多个进程访问的共享内存来进行通信
- 通过写进程和读进程利用管道进行通信
多线程与单线程的关系:
- 多线程是指在一个进程中,并发执行了多个线程,每个线程都实现了不同的功能
- 在单核CPU中,将CPU分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上轮流占用CPU的机制。由于CPU轮询的速度非常快,所以看起来像是“同时”在执行一样
- 多线程会存在线程上下文切换,会导致程序执行速度变慢
- 多线程不会提高程序的执行速度,反而会降低速度。但是对于用户来说,可以减少用户的等待响应时间,提高了资源的利用效率
线程的状态有哪些:
- NEW
- RUNNABLE
- BLOCKED
- WAITING
- TIMED_WAITING
- TERMINATED
线程死锁:(重点掌握)
死锁是最常见的一种线程活性故障。死锁的起因是多个线程之间相互等待对方而被永远暂停(处于非Runnable)。死锁的产生必须满足如下四个必要条件:
- 资源互斥:一个资源每次只能被一个线程使用
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:线程已经获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
那么,如何避免死锁的发生?
- **粗锁法:**使用一个粒度粗的锁来消除“请求与保持条件”,缺点是会明显降低程序的并发性能并且会导致资源的浪费。
- 锁排序法:(必须回答出来的点)
指定获取锁的顺序,比如某个线程只有获得A锁和B锁,才能对某资源进行操作,在多线程条件下,如何避免死锁?
通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。这通常被认为是解决死锁很好的一种方法。
- 使用显式锁中的**ReentrantLock.try(long,TimeUnit)**来申请锁
2.2、多线程编程中常用的函数
sleep 和 wait 的区别:
- **sleep方法:**是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。
- wait方法:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。
**join 方法:**当前线程调用,则其它线程全部停止,等待当前线程执行完毕,接着执行。
**yield 方法:**该方法使得线程放弃当前分得的 CPU 时间。但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。
2.3、Java中的线程池
java.util.concurrent.ThreadPoolExecutor类就是一个线程池。客户端调用ThreadPoolExecutor.submit(Runnable task)提交任务,线程池内部维护的工作者线程的数量就是该线程池的线程池大小,有3种形态:
- 当前线程池大小:表示线程池中实际工作者线程的数量
- 最大线程池大小(maxinumPoolSize):表示线程池中允许存在的工作者线程的数量上限
- 核心线程大小(corePoolSize ):表示一个不大于最大线程池大小的工作者线程数量上限
线程池的排队策略:
当我们向线程池提交任务的时候,需要遵循一定的排队策略,具体策略如下:
- 如果运行的线程少于corePoolSize,则Executor始终首选添加新的线程,而不进行排队
- 如果运行的线程等于或者多于corePoolSize,则Executor始终首选将请求加入队列,而不是添加新线程
- 如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超出maxinumPoolSize,在这种情况下,任务默认将被拒绝。
创建线程池的几种方法:
- newCachedThreadPool 创建一个可缓存的线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,若无可回收,则新建线程
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行
使用线程池的时候,怎么防止线程池会导致内存溢出
- 无界队列
队列大小无限制,常用的为无界的LinkedBlockingQueue,使用该队列做为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM。阅读代码发现,Executors.newFixedThreadPool 采用就是 LinkedBlockingQueue,而楼主踩到的就是这个坑,当QPS很高,发送数据很大,大量的任务被添加到这个无界LinkedBlockingQueue 中,导致cpu和内存飙升服务器挂掉。
- 有界队列
常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue与有界的LinkedBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。
使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。
- 同步移交队列
如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。
2.4、Synchronized关键字(非公平调度方式)
synchronized是Java中的一个关键字,是一个内部锁。它可以使用在方法上和方法块上,表示同步方法和同步代码块。在多线程环境下,同步方法或者同步代码块在同一时刻只允许有一个线程在执行,其余线程都在等待获取锁,也就是实现了整体并发中的局部串行。
内部锁底层实现:
- 进入时,执行monitorenter,将计数器+1,释放锁monitorexit时,计数器-1
- 当一个线程判断到计数器为0时,则当前锁空闲,可以占用;反之,当前线程进入等待状态
synchronized内部锁对原子性的保证:
锁通过互斥来保障原子性,互斥是指一个锁一次只能被一个线程所持有,所以,临界区代码只能被一个线程执行,即保障了原子性。
synchronized内部锁对可见性的保证:
synchronized内部锁通过写线程冲刷处理器缓存和读线程刷新处理器缓存保证可见性。
- 获得锁之后,需要刷新处理器缓存,使得前面写线程所做的更新可以同步到本线程。
- 释放锁需要冲刷处理器缓存,使得当前线程对共享数据的改变可以被推送到下一个线程处理器的高速缓冲中。
synchronized内部锁对有序性的保证:
由于原子性和可见性的保证,使得写线程在临界区中所执行的一系列操作在读线程所执行的临界区看起来像是完全按照源代码顺序执行的,即保证了有序性。
2.5、Volatile关键字
volatile关键字是一个轻量级的锁,可以保证可见性和有序性,但是不保证原子性。
- volatile 可以保证主内存和工作内存直接产生交互,进行读写操作,保证可见性
- volatile 仅能保证变量写操作的原子性,不能保证读写操作的原子性。
- volatile可以禁止指令重排序(通过插入内存屏障),典型案例是在单例模式中使用。
volatile实现可见性的原理
JVM线程工作时的原子性指令有:
-
read: 从主存读取一个变量的值的副本到线程的工作内存。
-
load:read读来的值,把这个值填充到线程使用的变量中,然后就可以使用了。
-
use:要使用一个变量,先发出这个指令。
-
assign:赋值,给变量一个新值。
-
write:将变量的新值运送到主存中。
-
store:将写到主存的新值存到那个变量中。
上述操作必定是顺序执行的,但可不一定连续,中间可能插入其他指令。为了保证可见性:关键就是保证load、use的执行顺序不被打乱(保证使用变量前一定进行了load操作,从主存拿最新值来),assign、wirte的执行顺序不被打乱(保证赋值后马上就是把值写到主存)。
所以使用内存屏障, CPU指令,可以禁止指令执行乱序:插入一个内存屏障, 相当于告诉CPU和编译器指令顺序先于这个指令的必须先执行,后于这个命令的必须后执行。
-
解决第一个导致不可见的因素(更新不及时):内存屏障,对于volatile修饰的变量,读操作时在读指令use之前插入一条读屏障指令重新从主存加载最新值进来,保证了load、use指令的执行顺序不乱;写操作时在写指令assign之后插入一条写屏障指令,将工作内存变量的最新值立刻写入主存变量。
-
解决第二个因素(指令重排): 由于读写数据时会在之前/后插入一条内存屏障指令,因此volatile可以禁止指令重排序。
2.6、单例模式为什么有synchronized还要加入volatile一起用
具体来说就是synchronized虽然保证了原子性,但却没有保证指令重排序的正确性,会出现A线程执行初始化,但可能因为构造函数里面的操作太多了,所以A线程的uniqueInstance实例还没有造出来,但已经被赋值了。而B线程这时过来了,错以为uniqueInstance已经被实例化出来,一用才发现uniqueInstance尚未被初始化。要知道我们的线程虽然可以保证原子性,但程序可能是在多核CPU上执行。
2.7、ReentrantLock可重入锁
ReentrantLock是显示锁,其提供了一些内部锁不具备的特性,但并不是内部锁的替代品。显式锁支持公平和非公平的调度方式,默认采用非公平调度。
synchronized 内部锁简单,但是不灵活。显示锁支持在一个方法内申请锁,并且在另一个方法里释放锁。显示锁定义了一个tryLock()方法,尝试去获取锁,成功返回true,失败并不会导致其执行的线程被暂停而是直接返回false,即可以避免死锁。
ReentrantLock与synchronized区别:
(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。
3 JVM
3.1、Java内存管理分配
方法区:方法区是一个线程之间共享的区域。常量,静态变量以及JIT编译后的代码都在方法区。主要用于存储已被虚拟机加载的类信息,也可以称为“永久代”,垃圾回收效果一般,通过-XX:MaxPermSize控制上限。
堆内存:堆内存是垃圾回收的主要场所,也是线程之间共享的区域,主要用来存储创建的对象实例,通过-Xmx 和-Xms 可以控制大小。
虚拟机栈(栈内存):栈内存中主要保存局部变量、基本数据类型变量以及堆内存中某个对象的引用变量。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈的操作。
程序计数器: 程序计数器是当前线程执行的字节码的位置指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是内存区域中唯一一个在虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
本地方法栈: 主要是为JVM提供使用native 方法的服务。
对象创建过程中的内存分配:
一般情况下我们通过new指令来创建对象,当虚拟机遇到一条new指令的时候,会去检查这个指令的参数是否能在常量池中定位到某个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。如果没有,那么会执行类加载过程。
通过执行类的加载,验证,准备,解析,初始化步骤,完成了类的加载,这个时候会为该对象进行内存分配,也就是把一块确定大小的内存从Java堆中划分出来,在分配的内存上完成对象的创建工作。
对象的内存分配有两种方式,即指针碰撞和空闲列表方式。
指针碰撞方式:
假设Java堆中的内存是绝对规整的,用过的内存在一边,未使用的内存在另一边,中间有一个指示指针,那么所有的内存分配就是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
空闲列表方式:
如果Java堆内存中不是规整的,已使用和未使用的内存相互交错,那么虚拟机就必须维护一个列表用来记录哪块内存是可用的,在分配的时候找到一块足够大的空间分配对象实例,并且需要更新列表上的记录。
需要注意的是,Java 堆内存是否规整是由所使用的垃圾收集器是否拥有压缩整理功能来决定的。
那么内存的分配如何保证线程安全呢?
- 对分配内存空间的动作进行同步处理,通过“CAS + 失败重试”的方式保证更新指针操作的原子性
- 把分配内存的动作按照线程划分在不同的空间之中,即给每一个线程都预先分配一小段的内存,称为本地线程分配缓存(TLAB),只有TLAB用完并分配新的TLAB时,才需要进行同步锁定。 虚拟机是否使用TLAB,可以通过**-XX: +/-UserTLAB**参数来设定
3.2、垃圾回收
JVM的内存可以分为堆内存和非堆内存。堆内存分为年轻代和老年代。年轻代又可以进一步划分为一个Eden(伊甸)区和两个Survivor(幸存)区组成。
JVM堆内存的分配:
JVM初始分配的堆内存由**-Xms指定,默认是物理内存的1/64。JVM最大分配的堆内存由-Xmx**指定,默认是物理内存的1/4。
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此我们一般设置-Xms和-Xmx相等以避免在每次GC 后调整堆的大小。
通过参数**-Xmn2G** 可以设置年轻代大小为2G。通过**-XX:SurvivorRatio**可以设置年轻代中Eden区与Survivor区的比值,设置为8,则表示年轻代中Eden区与一块Survivor的比例为8:1。注意年轻代中有两块Survivor区域。
JVM非堆内存的分配:
JVM使用**-XX:PermSize** 设置非堆内存初始值,默认是物理内存的1/64。由**-XX:MaxPermSize**设置最大非堆内存的大小,默认是物理内存的1/4。
堆内存上对象的分配与回收:
我们创建的对象会优先在Eden分配,如果是大对象(很长的字符串数组)则可以直接进入老年代。虚拟机提供一个
-XX:PretenureSizeThreshold参数,令大于这个参数值的对象直接在老年代中分配,避免在Eden区和两个Survivor区发生大量的内存拷贝。
另外,长期存活的对象将进入老年代,每一次MinorGC(年轻代GC),对象年龄就大一岁,默认15岁晋升到老年代,通过
-XX:MaxTenuringThreshold设置晋升年龄。
堆内存上的对象回收也叫做垃圾回收,那么垃圾回收什么时候开始呢?
垃圾回收主要是完成清理对象,整理内存的工作。上面说到GC经常发生的区域是堆区,堆区还可以细分为新生代、老年代。新生代还分为一个Eden区和两个Survivor区。垃圾回收分为年轻代区域发生的Minor GC和老年代区域发生的Full GC,分别介绍如下。
Minor GC(年轻代GC):
对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快。
Full GC(老年代GC):
Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC。
动态对象年龄判定:
如果Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,那么年龄大于等于该对象年龄的对象即可晋升到老年代,不必要等到-XX:MaxTenuringThreshold。
空间分配担保:
发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小。如果大于,则进行一次Full GC(老年代GC),如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC。
3.3、JVM如何判定一个对象是否应该被回收
判断一个对象是否应该被回收,主要是看其是否还有引用。判断对象是否存在引用关系的方法包括引用计数法以及root根搜索方法。
引用计数法:
是一种比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
root根搜索方法:
root搜索方法的基本思路就是通过一系列可以做为root的对象作为起始点,从这些节点开始向下搜索。当一个对象到root节点没有任何引用链接时,则证明此对象是可以被回收的。以下对象会被认为是root对象:
- 栈内存中引用的对象
- 方法区中静态引用和常量引用指向的对象
- 被启动类(bootstrap加载器)加载的类和创建的对象
- Native方法中JNI引用的对象。
4 Spring
4.1、SpringIOC容器启动过程
1)创建IoC配置文件的抽象资源,这个抽象资源包含了BeanDefinition的定义信息.
2)创建一个BeanFactory,比如常用的DefaultListableBeanFactory。
3)创建一个载入BeanDefinition的读取器,比如XmlBeanDefinitionReader来载入XML文件形式的BeanDefinition,通过一个回调配置给BeanFactory。
4)从定义好的资源位置读入配置信息,具体的解析过程由XmlBeanDefinitionReader来完成。完成整个载入和注册Bean定义之后,需要的IoC容器就建立起来了。这个时候就可以直接使用IoC容器了。
4.2、Spring怎么解决循环依赖的
简单说明spring如何解决循环依赖:
我们先实例化A,实例化好后,调用addSingletonFactory放入三级缓冲池中,然后此时我们设置属性的时候会发现我们还依赖B,于是我们就先去实例化B后并为进行属性赋值,而当我们进行赋值的时发现B也依赖A,但是之前已经把A存放在singletonFactories,那么B会将其取出并装配自己,实例化以后把自己放入单例池中,然后A也就可以继续进行属性赋值了,最后A 实例好以后,将自己也放入单例池中,这样Spring就解决了循环依赖。
5 计算机网络
5.1、TCP三次握手、四次挥手
三次握手:
第一次握手(SYN=1, seq=J):
客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 J,保存在包头的序列号(Sequence Number)字段里。发送完毕后,客户端进入 SYN_SEND 状态。
第二次握手(SYN=1, ACK=1, seq=k, ack=J+1):
服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN(初始序号) 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。
第三次握手(ACK=1,ack=k+1):
客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。
四次挥手:
第一次挥手(FIN=1,seq=x)
假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。发送完毕后,客户端进入 FIN_WAIT_1 状态。
第二次挥手(ACK=1,ACKnum=x+1)
服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。
第三次挥手(FIN=1,seq=y)
服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。
第四次挥手(ACK=1,ACKnum=y+1)
客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。
5.2、TCP的端口号是解决什么问题的?
传输层中一定要知道端口号的概念:
数据链路和 IP 中的地址,分别指的是 MAC 地址和 IP 地址。前者用来识别同一链路中不同的计算机,后者用来识别 TCP/IP 网络中互连的主机和路由器。在传输层也有这种类似于地址的概念,那就是端口号。**端口号用来识别同一台计算机中进行通信的不同应用程序。**因此,它也被称为程序地址。
5.3、TCP、UDP知识点
TCP(Transmission Control Protocol)
- TCP提供一种面向连接的、可靠的字节流服务。
在一个TCP连接中,仅有两方进行彼此通信。广播和多播不能用于TCP - TCP使用校验和,确认和重传机制来保证可靠传输
- TCP给数据分节进行排序,并使用累计确认保证数据的顺序不变和非重复
- TCP使用滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制
- TCP提供全双工通信。(发送缓存和接收缓存)
TCP是如何保证可靠传输的:
- 确认和重传:接收方收到报文就会确认,发送方发送一段时间后没有收到确认就重传。
- 数据校验
- 数据合理分片和排序
- 流量控制:当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。
- 拥塞控制:当网络拥塞时,减少数据的发送。
UDP(User Datagram Protocol)
- UDP是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
- 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等, 因此一台服务机可同时向多个客户机传输相同的消息,并且低延时。
- UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小。
- 吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、 源端和终端主机性能的限制。
- UDP使用尽最大努力交付,即不保证可靠交付, 因此主机不需要维持复杂的链接状态表(这里面有许多参数)。
- UDP是面向报文的。发送方的UDP对应用程序交下来的报文, 在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。
6 MySQL
6.1、MySQL的事务特性与隔离级别
事务是单个逻辑工作单元执行的一系列操作,是一个不可分割的工作单位。满足如下的四大特性:
- **原子性(Atomicity):**事务作为一个整体被执行 ,要么全部执行,要么全部不执行;
- **一致性(Consistency):**保证数据库状态从一个一致状态转变为另一个一致状态;
- **隔离性(Isolation):**多个事务并发执行时,一个事务的执行不应影响其他事务的执行;
- **持久性(Durability):**一个事务一旦提交,对数据库的修改应该永久保存
多个事务并发操作数据库会产生哪些问题:
- 丢失更新:
两个不同事务同时获得相同数据,然后在各自事务中同时修改了该数据,那么先提交的事务更新会被后提交事务的更新给覆盖掉,这种情况先提交的事务所做的更新就被覆盖,导致数据更新丢失。
- 脏读:
事务A读取了事务B未提交的数据,由于事务B回滚,导致了事务A的数据不一致,结果事务A出现了脏读;
- 不可重复读:
一个事务在自己没有更新数据库数据的情况,同一个查询操作执行两次或多次得到的结果数值不同,因为别的事务更新了该数据,并且提交了事务。
- 幻读:
事务A读的时候读出了N条记录,事务B在事务A执行的过程中增加 了1条,事务A再读的时候就变成了N+1条,这种情况就叫做幻读。
**注意:**幻读是指一种结构上的改变,比如说条数发生了改变;不可重复读是指读出的数值发生了改变。
隔离级别:
为了避免数据库事务操作中的问题,MySQL定义了4个事务隔离级别,不同的隔离级别对事务的处理不同。
- 读未提交(Read Uncommitted):
允许脏读取。如果一个事务已经开始写数据,则另外一个数据则不允许同时进行写操作,但允许其他事务读此行数据。
- 读已提交(Read Committed):
允许不可重复读取,但不允许脏读取。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。
- 可重复读(Repeatable Read):
禁止不可重复读取和脏读取,但是有时可能出现幻读。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
- 序列化(Serializable):
提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。
事务的隔离级别越高,对数据的完整性和一致性保证越佳,但是对并发操作的影响也越大。MySQL事务默认隔离级别是可重复读。
6.2、MySQL锁分类
MySQL数据库的锁分为表级锁和行级锁。从数据库的角度看,行级锁又可以分为独占锁和共享锁。
独占锁(排他锁),也称X锁(Exclusive Lock):
独占锁锁定的资源只允许进行锁定操作的程序使用,其它任何对它的操作均不会被接受。执行数据更新命令,即INSERT、UPDATE 或DELETE 命令时,MySQL会自动使用独占锁。但当对象上有其它锁存在时,无法对其加独占锁。独占锁一直到事务结束才能被释放。
在select命令中使用独占锁的SQL语句为:select … for update;
共享锁,也叫S锁(Shared Lock):
共享锁顾名思义,那就是其锁定的资源可以被其它用户读取,但其它用户不能修改。如果在select查询语句中要手动加入共享锁,那么对应的SQL语句为:select … lock in share mode
这里需要注意:
一个事务在一行数据上加入了独占锁,那么其余事务不可以在该数据行上加入任何锁。也就是说加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。
间隙锁:
间隙锁(Gap Lock)是Innodb在可重复读提交下为了解决幻读问题时引入的锁机制。
数据库是通过行锁和间隙锁共同组成的(next-key lock),来实现的加锁规则有以下特性,我们会在后面的案例中逐一解释:
- 加锁的基本单位是(next-key lock),他是前开后闭原则
- 插叙过程中访问的对象会增加锁
- 索引上的等值查询–给唯一索引加锁的时候,next-key lock升级为行锁
- 索引上的等值查询–向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁
- 唯一索引上的范围查询会访问到不满足条件的第一个值为止
6.3、MySQL存储引擎
MySQL中最常见的存储引擎有InnoDB和MyISAM,它们的主要区别如下:
- MyISAM不支持事务;InnoDB是事务类型的存储引擎。
- MyISAM只支持表级锁;InnoDB支持行级锁和表级锁,默认为行级锁。
- MyISAM引擎不支持外键;InnoDB支持外键。
- 对于count(*)查询来说MyISAM更有优势,因为其保存了行数。
- InnoDB是为处理巨大数据量时的最大性能设计的存储引擎。
- MyISAM支持全文索引(FULLTEXT);InnoDB不支持。
6.4、Innodb索引数据结构,有几类索引?
索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息,就像一本书的目录一样,可以加快查询速度。InnoDB 存储引擎的索引模型底层实现数据结构为B+树,所有数据都是存储在 B+ 树中的。
为什么底层数据结构使用B+树,而不是B树?
- B+树是B树的变种,B+树的非叶子节点只用来保存索引,不存储数据,所有的数据都保存在叶子节点;而B树的非叶子节点也会保存数据。这样就使得B+树的查询效率更加稳定,均为从根节点到叶子节点的路径。
- B+树的内部结点并没有指向关键字具体信息的指针,因此其内部结点相对B 树更小,同样空间可以读入更多的节点,所以B+树的磁盘读写代价更低。
聚簇索引和非聚簇索引:
聚簇索引也称为主键索引,其索引树的叶子节点中存的是整行数据,表中行的物理顺序与键值的逻辑(索引)顺序相同。一个表只能包含一个聚集索引。因为索引(目录)只能按照一种方法进行排序。
非聚簇索引(普通索引)的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。
6.5、MySQL索引优化-最左前缀
最左匹配原则:
- 索引可以简单如一个列
(a)
,也可以复杂如多个列(a, b, c, d)
,即联合索引。 - 如果是联合索引,那么key也由多个列组成,同时,索引只能用于查找key是否存在(相等),遇到范围查询
(>、<、between、like
左匹配)等就不能进一步匹配了,后续退化为线性查找。 - 因此,列的排列顺序决定了可命中索引的列数。
例子:
- 如有索引
(a, b, c, d)
,查询条件a = 1 and b = 2 and c > 3 and d = 4
,则会在每个节点依次命中a、b、c,无法命中d。(会一直向右匹配直到遇到范围查询(>,<,BETWEEN,LIKE
)就停止匹配)
7 算法题
7.1、最小(大)的k个数【剑指Offer 40】
题目链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if (k == 0 || arr.length == 0) return new int[0];
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(k, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//小顶堆 topK大
//return o1-o2;
//大顶堆 topK小
return o2-o1;
}
});
int[] res = new int[k];
for (int i = 0; i < k; i++) {
priorityQueue.offer(arr[i]);
}
for (int i = k; i < arr.length; i++) {
//if (priorityQueue.peek() < arr[i]){
if (priorityQueue.peek() > arr[i]){
priorityQueue.poll();
priorityQueue.offer(arr[i]);
}
}
for (int i = 0; i < k; i++) {
res[i] = priorityQueue.poll();
}
return res;
}
}
7.2、只出现一次的数字【LeetCode136】
题目链接:https://leetcode-cn.com/problems/single-number/
法1:
class Solution {
public int singleNumber(int[] nums) {
int res = 0;
for(int i = 0; i < nums.length; i++) {
res ^= nums[i];
}
return res;
}
}
法2:
class Solution {
public int singleNumber(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
for (int i=0;i<nums.length;i++) {
if (!map.containsKey(nums[i])) {
map.put(nums[i], 1);
} else {
map.put(nums[i], map.get(nums[i])+1);
}
}
int res = 0;
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (entry.getValue() == 1) {
res = entry.getKey();
}
}
return res;
}
}
7.3、判断链表中是否有环【LeetCode141】
题目链接:https://leetcode-cn.com/problems/linked-list-cycle/
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head;
while(fast!=null && fast.next!=null) {
slow = slow.next;
fast = fast.next.next;
if (slow==fast) {
return true;
}
}
return false;
}
}
7.4、判断链表中是否有环及环的入口节点【LeetCode142】
题目链接:https://leetcode-cn.com/problems/linked-list-cycle-ii/
增加了判断环前节点数,环中节点数,链表总长度
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
ListNode slow = head.next;
ListNode fast = head.next.next;
ListNode res = head;
int huanqian = 0;
while(fast!=null) {
if(fast!=slow){
if(fast.next!=null)
fast = fast.next.next;
else
return null;
slow = slow.next;
}
else{
res = head;
while(res!=fast){
res = res.next;
fast = fast.next;
huanqian++;
}
System.out.println(huanqian);
int huan = 1;
ListNode cur = res;
while(cur.next != res){
huan++;
cur = cur.next;
}
System.out.println(huan);
System.out.println(huanqian+huan);
return res;
}
}
return null;
}
}
参考链接
[1] https://www.nowcoder.com/zhuanlan/detail/10014/a41e7c7a0e3c4efd926d0462dfd66429
[2] https://blog.csdn.net/zhouxiaoyujcut/article/details/83743310
[3] https://blog.csdn.net/weixin_34203832/article/details/91872584
[4] https://zhuanlan.zhihu.com/p/40396971
[5] https://www.zhihu.com/question/341680201/answer/796759612
[6] https://blog.csdn.net/ym123456677/article/details/78860719