前言
回答思路
整体思路:总分总
- 一句话概括要回答的问题
- 展开说明,一定要有层次和连接词(首先,其次,然后,最后 1.2.3.4)
- 最后一句话总结下答案,回扣一下答案(总之)
基础
JDK和JRE的区别是什么
JDK是Java开发工具包,JRE是Java运行时环境,二者的区别在于
JRE是Java程序运行所必须的,它包含jvm和一些Java的基础类库
JDK是Java程序开发所必须的,它包含JRE和一些开发工具
总结一下就是:JDK包含JRE,如果仅仅是运行Java程序,只要有JRE即可;如果是要开发Java程序,则必须要有JDK
==与equals的区别是什么
- ==是一个运算符,equals 是 Object 类的方法
- 用于基本类型的变量比较时: ==比较的是值是否相等,equals不能直接用于基本数据类型的比较,需要转换为其对应的包装类型
- 用于引用类型的比较时。==和 equals 都是比较栈内存中的地址是否相等。但是通常会重写 equals 方法去实现对象内容的比较
final和finally区别是什么
这是Java提供的三个关键字,虽然长的差不多,但是其实没什么联系,使用场景也完全不同
final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、 修饰变量表示该变量是一个常量不能被重新赋值。
finally一般作用在try-catch代码块中,在处理异常的时候,无论程序是否出现异常,写在finally中的代码都会被执行,一般用来释放一些资源
=和new创建字符串的区别是什么
两种方式都可以创建出字符串,但是在内存分配上却是不一样的
-
等号方式:JVM会在常量池中创建一个字符串对象
-
new方式:JVM会先判断常量池中是否有此字符串,如果没有,它就会在常量池中创建一个
而且无论常量池中是否有,它都会在堆内存中重新创建一个
重写和重载的区别是什么
重载和重写都是用于描述方法间的关系的,但是他们的含义和场景确大不相同
-
重写是存在于子父类之间的,一般用在父类的方法无法满足子类需求时,子类重写方法来自定义方法功能
它要求子类定义的方法与父类中的方法具有相同的方法名字,相同的参数表和相同的返回类型
-
重载是存在于同一个类中的,一般用在功能相似的方法需要接收不同的参数时
它要求多个方法具有相同的名字,但方法具有不同的参数列表
this和super的应用场景是什么
this和supper都是Java提供的关键字
this代表的是当前对象,一般用于在一个方法中调用本对象的成员变量或其它方法
supper代表是父类对象,一般在本对象和父对象出现成员名称冲突时,强行调用父对象的成员,也经常用于调用父类的构造方法
throw和throws的区别是什么
throws:用在方法的声明上,声明当前方法可能抛出的异常
throw:写在方法里,真正的抛出一个异常,抛出自定义异常
接口和抽象类的区别
它们的共同点是:都不能实例化对象
它们的不同点是:
1. 抽象类一般用于抽取子类中的共同方法和属性,接口一般用于指定实现类的规范
2. 抽象类可以有构造方法,作用是用给抽象父类中中的属性赋值;接口中不能有构造方法
3. 接口中不能含有静态代码块,而抽象类可以有静态代码块
4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口
说出几个常见的异常类
Java中的异常分为运行时异常和编译时异常两大类:
-
运行时异常都是RuntimeException类及其子类异常,这类异常的特点是不强行要求程序员进行处理,常见的有
NullPointerException 空指针异常、ClassCastException 数据类型转换异常、NoSuchMethodException方法不存在异常
-
非运行时异常,也叫编译异常,是Exception的子类但不是RuntimeException的子类,类型上都属于及其子类
它要求程序员在编写代码的过程中提供异常处理方案,否则编译不通过,常见的有:IOException和SQLException
集合
集合类的体系结构
我们常见的集合主要有两大类,分别是单列集合和双列集合
-
单列集合的顶级接口是Collection,它下面有两个主要的子接口分别是List和Set
List的特点是元素有序的,可以重复的;Set的特点是元素无序的,不可重复的
List下我们常用的类有ArrayList、LinkedList等,Set下我们常用的类有HashSet、LinkedHashSet、TreeSet等
-
双列集合的顶级接口是Map,它的特点是每个元素都有键和值两部分组成,而且键不能重复
Map接口下我们常用的类有:HashMap、LinkedHashMap、TreeMap等
集合类的数据结构
集合主要分为双列集合和双列集合
-
双列集合都是Map的实现类,主要有HashMap、LinkedHashMap和TreeMap
HashMap: JDK1.8之前是由数组+链表组成的,JDK1.8之后,为了提升效率,在当链表的长度>8,并且数组长度>=64的时候,链表就会转换为红黑树
LinkedHashMap:继承自HashMap,在HashMap的基础上增加了一条双向链表,来保持键值对的插入顺序。
TreeMap:底层是红黑树
-
单列集合主要是List和Set
List有ArrayList和LinkedList,ArrayList底层是数组,查询快,增删慢;LinkedList底层是双向链表,查询慢,增删快
Set有HashSet、LinkedHashSet和TreeSet,它的实现原理和对应的Map是一样的,底层都是用的对应Map的key实现
ArrayList和LinkedList的区别
ArrayList和LinkedList都是Java中的单列结合,都是有序的,可重复的
不同点有下面几个:
- 底层数据结构不同:ArrayList 底层是动态数组,而LinkedList底层是双向链表
- 使用场景不同:ArrayList查询快,增删慢,适合查询场景;LinkedList查询慢,增删快,适合频繁修改的场景
- 占用内存空间不同:LinkedList比ArrayList更占内存,这是因为它的每个节点除了存储数据,还存储了前后节点的引用两个引用
HashMap的底层原理
HashMap底层数据结构是哈希表,哈希表在JDK1.8之前是数组+链表实现,在JDK1.8之后是数组+链表+红黑树实现的
下面我以map中存储对象的流程给您说一下它的实现原理吧
-
当我们创建一个HashMap的时候,JDK就会在内存中创建一个长度为16的数组
-
当我们调用put方法像HashMap中保存一个元素的时候,它会先调用key的hashCode方法计算出key的hash值
然后使用得到hash值对数组长度取余,找出当前对象的元素在数组中的位置
-
接下来,它会判断算出的位置上是否有元素,如果没有,就会将此元素直接存储到当前位置上
如果算出的位置上有元素或者是有链表,它会再调用key的equals方法跟存在元素的key做比较
如果有一个比较得到的结果为true,则会进行值的覆盖,如果都为false,则会将元素追加在链表的末尾
当然,为了降低Hash冲突和链表长度,HashMap还做了一些优化
-
当元素的数量超过数组大小与加载因子的乘积的时候,就会执行扩容,扩容为原来的2倍,并将原来数组中的键重新进行hash运算,然后分配到新数组中
-
当链表的长度>8,并且数组长度>=64的时候,链表就会转换为红黑树,当红黑树结点数小于6时将再次转回为链表。
HashMap如何解决哈希冲突
HashMap的底层有一个数组,它在保存元素的时候,会对元素的key进行hash运算,得到hash值,然后对数组长度取余,得到元素在数组中的位置
这样的话,不同的元素计算完毕之后,就可能会被分配到数组中的同一个位置上,这就是哈希冲突
解决hash冲突最常用的方式有链表法和开放地址法,而HashMap就是采用了链表法
具体做法就是当哈希冲突出现之后,HashMap会在发生冲突的位置上创建一个链表来保存元素
当然在JDK1.8之后,又对此做出了改进,那就是当链表的长度>8,并且数组长度>=64的时候,链表就会转换为红黑树,使得效率更高
说一下HashSet的实现原理
HashSet是基于HashMap实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present
因此HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成,由于HashMap的键是不能重复的,所有HashSet不允许重复的值
线程
创建线程有几种方式
我知道的创建线程的方式大体上可以分为四种:
- 继承Thread类并重写run方法创建线程,这种方式实现简单但线程类不可以再继承其他类
- 实现Runnable接口并重写run方法,这种方式避免了单继承局限性,编程更加灵活,实现解耦
- 实现Callable 接口并重写call方法,这种方式可以获取线程执行结果的返回值,并且可以抛出异常
- 使用线程池创建
runnable和callable的区别
这两个接口都是线程任务类的接口,区别点在于
- Runnable接口run方法无返回值;Callable接口call方法有返回值,也就是说如果需要获取线程类的执行结果,必须要使用Callable
- Runnable接口run方法只能抛出运行时异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息
start和run方法的区别
run(): 封装了要被线程执行的代码,本质上就是一个普通方法,可以被调用多次
start(): 用来启动线程,底层会自动去执行run方法中的代码,start方法只能被调用一次
也就是启动线程的时候,只能调用start方法,如果调用的run方法,不会启动新线程,而是当普通方法调用执行
notify和notifyAll的区别
这两个方法都是用户唤醒被wait方法休眠的线程的,区别点在于:
- notifyAll:唤醒所有wait的线程
- notify:随机唤醒一个 wait 线程
sleep和wait的区别
sleep和wait都是Java中用来让线程暂时放弃CPU使用权,进入阻塞状态的方法。他们的主要区别点有下面几个:
- 方法归属不同:sleep是Thread 的静态方法,而wait是Object的成员方法
- 醒来时机不同: sleep会在指定的时间后自动苏醒,而wait需要其他线程的唤醒
- 锁特性不同:sleep不会释放锁,而wait会释放锁
- 使用限制不同:wait必须用在synchronized代码块中,而sleep无此限制
说一下线程的状态及转换
在我的理解中,线程共分为7种状态,分别是:新建、就绪、运行、终止以及阻塞、等待、计时等待
它们之间的转换关系是这样的:
- 当线程new出来之后,没有start之前就会处于新建状态
- 当线程执行start方法之后,就进入就绪状态
- 当就绪的线程一旦获取到了cpu的执行权,就可以进入运行状态
- 当线程执行完了run方法之后,就进入了死亡状态
这是一条正常的流程,但是代码在运行状态下可以因为一些原因进入到其它状态,比如说:
- 当进行抢锁操作时,抢锁失败就会进入到阻塞状态
- 当代码调用了wait方法时,就会进入等待状态
- 当代码调用了sleep方法时,就会进入计时等待状态
synchronized的实现原理
在Java中,每个对象都隐式包含一个 monitor(监视器)对象,加锁的过程其实就是竞争 monitor 的过程,
当线程进入字节码monitorEnter指令之后,线程将持有monitor对象,执行monitorExit时释放 monitor 对象
如果在这个过程中,其它线程就会阻塞等待获取该monitor对象
谈一谈你对CAS的理解
CAS的全称是: Compare And Swap,也就是比较再交换,是一种对内存中的共享数据进行操作的一种特殊指令
CAS有3个操作数:内存值V ,旧的预期值A ,要修改的新值B。
当且仅当旧预期值A和内存值V相同时,将内存值V修改为B并返回true,否则什么都不做,并返回false。
Java中有哪些锁
Java中的锁有很多,根据不同的维度,我给您说一下我了解的一些:
-
悲观锁和乐观锁
悲观锁:就是认为自己在操作一个共享数据的时候,一定还有其它人来操作,所以直接锁定资源,比如:synchronized、Lock
乐观锁:就是认为自己在操作一个共享数据的时候,一定不会有其它人来操作,所有不锁定资源,而是在更新前使用类似于CAS的机制去判断
-
可重入锁&非可重入锁
可重入锁是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁,不会因为之前已经获取过还没释放而阻塞
ReentrantLock 和 synchronized 都是可重入锁
不可重入锁:与可重入相反,获取锁后不能重复获取,否则会死锁(自己锁自己)
-
自旋锁
自选锁即是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待(不放弃 CPU 资源)
然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,此时获取锁的线程一直处于活跃状态(而非阻塞)
它的好处是不会使线程状态发生切换,不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
缺点是如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU
线程池
线程池的执行流程
当我们提交一个任务到线程池中,线程池的处理流程如下:
- 首先判断线程池里的核心线程是否都在执行任务,如果不是,则创建一个新的工作线程来执行任务。
- 如果核心线程都在执行任务,则判断工作队列是否已满,如果没满,则将新提交的任务存储在这个工作队列里。
- 如果工作队列满了,则判断线程数是否小于最大线程数,如果是,则创建临时线程直接执行任务
- 如果线程数已经到达了最大线程数,则会执行对应的拒绝策略逻辑
线程池的核心参数
线程池在创建的时候最大支持传入7个参数,分别是:
-
核心线程数
-
最大线程数
-
临时线程的空闲时间:临时线程会在空闲这段时间后
-
临时线程的空闲时间单位
-
工作队列:用来保存等待执行的任务的
-
threadFactory:设置创建线程的工厂
-
handler:线程池的拒绝策略
线程池的拒绝策略有哪些
拒绝策略是指将任务添加到线程池中时,线程池拒绝该任务所采取的相应策略,官方提供的有4种:
-
AbortPolicy:直接抛出异常,默认策略
-
CallerRunsPolicy:用调用者所在的线程来执行任务
-
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
-
DiscardPolicy:直接丢弃任务
线程池的阻塞队列有哪些
阻塞队列指的是当线程中核心线程数已满,新任务到达时,存储线程的队列,常见的有下面几种:
-
ArrayBlockingQueue:基于数组结构的有界阻塞队列
-
LinkedBlockingQueue:基于链表结构的有界阻塞队列
-
PriorityBlockingQueue:具有优先级别的阻塞队列
-
SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作
submit和execute方法的区别
submit和execute方法都是线程池中用于提交线程任务的方法,区别点在于:
首先是参数有区别:两个方法的参数都可以是Runnable对象;submit方法的参数还可以是Callable对象
其次是返回值不同:execute没有返回值;submit可以返回一个Future对象,从这个对象中可以拿到线程运行结果
还有一点不同是:submit能够控制异常,在主线程中通过 Future 的 get 方法捕获线程中的异常;execute不可以
了解Executors创建线程池吗
了解过,Excutors是JDK提供的一个可以创建线程池的工具类,它可以创建4 种线程池
但是用它创建的线程池有的没有限制最大线程数,有的没有限制阻塞队列的长度,这样的话,极大可能导致OOM
因此我们公司不允许我们使用它,而是使用自己传递参数的方式创建线程池
如何确定线程池的核心线程数
线程池的核心线程数跟任务的性质有很大关系
- 对于CPU密集型时,任务可以少配置线程数,推荐配置为CPU核数+1,这样可以使得每个线程都在执行任务
- IO密集型时,即该任务需要大量的IO,大部分线程都阻塞,则需要多配置线程数,推荐配置为CPU核数的2倍
MySQL
内连接和外连接的区别
内连接和外连接都是数据库进行多表联查时使用的连接方式,区别在于二者获取的数据集不同
内连接指的是使用左表中的每一条数据分别去连接右表中的每一条数据,仅仅显示出匹配成功的那部分
外连接有分为左外连接和右外连接
- 左外连接: 首先要显示出左表的全部,然后使用连接条件匹配右表,能匹配中的就显示,匹配不中的显示为null
- 右外连接: 首先要显示出右表的全部,然后使用连接条件匹配左表,能匹配中的就显示,匹配不中的显示为null
drop、delete与truncate区别
这个关键字都是MySQL中用于删除的关键字,区别在于:
- delete语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作
- drop 主要用于删除数据表、表中的列、索引等结构
- truncate 是直接把表删除,然后再重建表结构
这三种方式在效率方面drop 最高、truncate 其次、delete最低,但是drop和truncate 都不记录日志,无法回滚
char和varchar的区别
char和varchar是MySQL中的字符串类型,区别在于下面几方面:
- 最大长度:char最大长度是255字符,varchar最大长度是65535个字节
- 占用长度:char是定长的,不足的部分用隐藏空格填充,varchar是不定长的
- 空间使用:char会浪费空间,varchar会更加节省空间
- 查找效率:char查找效率会很高,varchar查找效率会更低
因此我们如果存储固定长度的列,例如身份证号、手机号建议使用char
其它不定长度的建议使用varchar,使用varchar的时候也要尽量让声明长度贴近实际长度
注意:varchar(50)中50的涵义是最多存放50个字符,varchar(50)和varchar(200)存储hello所占空间一样
事务的四大特性
事务的四大特性指的是原子性、一致性、隔离性、持久性
- 原子性:事务是最小的执行单位,不允许分割,同一个事务中的所有命令要么全部执行,要么全部不执行
- 一致性:事务执行前后,数据的状态要保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的
- 隔离性:并发访问数据库时,一个事务不被其他事务所干扰,各并发事务是独立执行的
- 持久性:一个事务一旦提交,对数据库的改变应该是永久的,即使系统发生故障也不能丢失
并发事务带来的问题
并发事务下,可能会产生如下的问题:
- 脏读:一个事务读取到了另外一个事务没有提交的数据
- 不可重复读:一个事务读取到了另外一个事务修改的数据
- 幻读(虚读):一个事务读取到了另外一个事务新增的数据
事务隔离级别
事务隔离级别是用来解决并发事务问题的方案,不同的隔离级别可以解决的事务问题不一样
- 读未提交: 允许读取尚未提交的数据,可能会导致脏读、幻读或不可重复读
- 读已提交: 允许读取并发事务已提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
- 可重复读: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
- 可串行化: 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,该级别可以防止脏读、不可重复读以及幻读。
上面的这些事务隔离级别效率依次降低,安全性依次升高,如果不单独设置,MySQL默认的隔离级别是可重复读
索引的分类
索引是数据库中用于提供查询效率的一种手段
-
从物理存储角度上分为聚集索引和非聚集索引
聚集索引指的是数据和索引存储在同一个文件中,InnoDB存储引擎使用的是此类索引存储方式
非聚集索引指的是数据和索引存储在不同的文件中,MyISAM存储引擎使用的是此类索引存储方式
-
从逻辑角度上分为普通、唯一、主键和联合索引,它们都可以用来提高查询效率,区别点在于
唯一索引可以限制某列数据不出现重复,主键索引能够限制字段唯一、非空
联合索引指的是对多个字段建立一个索引,一般是当经常使用某几个字段查询时才会使用,它比对这几个列单独建立索引效率要高
索引的创建原则
索引可以大幅度提高查询的效率,但不是所有的字段都要加,也不是加的越多越好,因为索引会占据磁盘空间,也会影响增删改的效率
我们在建立索引的时候应该遵循下面这些原则:
- 主键字段、外键字段应该添加索引
- 经常作为查询条件、排序条件或分组条件的字段需要建立索引
- 经常使用聚合函数进行统计的列可以建立索引
- 经常使用多个条件查询时建议使用组合索引代替多个单列索引
除此之外,下面这些情况,不应该建立索引
- 数据量小的表不建议添加索引
- 数据类型的字段是TEXT、BLOB、BIT等数据类型的字段不建议建索引
- 不要在区分度低的字段建立索引,比如性别字段、年龄字段等
索引失效的情况
索引失效指的是虽然在查询的列上添加了索引,但是某些情况下,查询的时候依旧没有用到索引,常见的情况有
- 使用like关键字时,模糊匹配使用%开头将导致索引失效
- 使用连接条件时,如果条件中存在没有索引的列会导致索引失效
- 在索引列上进行计算、函数运算、类型转换将导致索引失效
- 使用 !=、not in、is null、is not null时会导致索引失效
- 使用联合索引时,没有遵循最左匹配原则会导致索引失效
如何知道索引是否失效
MySQL中自带了一个关键字叫explain,它可以加在一个sql的前面来查看这条sql的执行计划
在执行计划中,我们主要观察两列的结果,一列是type,一列是extra
第一个type是重要的列,显示本次查询使用了何种类型,常见的值从坏到好依次为:all、index、range、ref、eq_ref 、const、system
- all表示全表扫描数据文件返回符合要求的记录
- index表示全表扫描索引文件返回符合要求的记录
- range表示检索指定范围的行,常见于使用>,<,between,in,like等运算符的查询中
- ref表示两表查询时,驱动表可能返回多行数据,也就是查询条件在主表中是加了一个普通索引
- eq_ref表示两表查询时,驱动表只返回一行数据,也就是查询条件在主表中是唯一的
- const表示索引一次就能得到结果,一般是使用唯一索引或者主键作为查询条件
- system表示表中仅有一行数据,很少见到
我们在优化的时候尽量优化到range级别以上
除了type之外我们需要关注一下extra列,它表示执行状态说明
-
要保证此列不要出现using filesort、using temporary等使用临时表或外部文件的情况
-
如果出现using index最好了,它表示列数据仅仅使用了索引中的信息而没有回表查询
MyISAM和InnoDB的区别
MyISAM和InnoDB是目前MySQL中最为流行的两种存储引擎,它们的区别有这几方面:
- MyISAM不支持事务,每次操作都是原子的;InnoDB支持事务,支持事务的四种隔离级别
- MyISAM不支持外键,InnoDB支持外键
- MyISAM仅仅支持表级锁,即每次操作是对整个表加锁;InnoDB支持行级锁,因此可以支持写并发
- MyISAM属于非聚集性索引,它的数据和索引不在同一个文件中;InnoDB属于聚集性索引,它的数据和索引在同一个文件中
- MyISAM中主键和非主键索引的数据部分都是存储的文件的指针;InnoDB主键索引的数据部分存储的是表记录,非主键索引的数据部分存储的是主键值
查询语句执行流程
一条查询语句到达MySQL数据库之后,数据库中的各个组件会按照顺序执行自己的任务
- 首先是连接器,他会负责建立连接、检查权限等操作
- 连接成功之后,会查询缓存,如果缓存中有结果会直接返回;如果缓存中没有结果,会将sql交给分析器处理
- 分析器负责检查sql的词法、语法,如果没有问题,再将sql交给优化器处理
- 优化器会决定用哪个索引,决定表的连接顺序等,然后将优化之后的sql交给执行器
- 执行器根据存储引擎类型,调用存储引擎接口
- 存储引擎负责最后数据的读写
索引的数据结构是什么
在MySQL中索引使用的数据结构是B+Tree,B+树是基于B树的变种,它具有B树的平衡性,而且树的高度更低
- B+树非叶子节点不存在数据只存索引,因此其内部节点相对B树更小,树的高度更小,查询产生的I/O更少
- B+树查询效率更高,B+树使用双向链表串连所有叶子节点,区间查询效率更高
- B+树查询效率更稳定,B+树每次都必须查询到叶子节点才能找到数据,而B树查询的数据可能不在叶子节点,也可能在,这样就会造成查询的效率的不稳定
数据库中的锁有哪些
MySQL中的锁从不同维度可以分为不同的种类
-
从锁的粒度上可以分为表锁和行锁
表锁指的是会锁定修改数据所在的整个表,开销小,加锁快,锁定粒度大,发生锁冲突概率高
行锁指的是会锁定修改数据所在的行记录,开销大,加锁慢,锁定粒度小,发生锁冲突概率低
-
从锁的排他性上分为共享锁和排他锁
共享锁指的是当一个事务针对同一份数据加上共享锁之后,另一个事务也可以再往上加一把共享锁,也可以读数据,但是不能改
对索引列加共享锁,锁定的是一行数据;对非索引列加共享锁,锁定的是整表数据
排他锁指的的是当一个事务针对同一份数据加上排他锁之后,另一个事务只能读数据,不能改,也不能再上其它任务锁
-
还有两种概念上的锁是悲观锁和乐观锁
悲观锁是指一个事务在修改数据的时候,总是认为别人也会修改此数据,所以强制要使用锁来保证数据安全
乐观锁是指一个事务在修改数据的时候,总是认为别人不会修改此数据,因为不加任何锁
这种情况下万一在当前事务修改的时候,数据被其它事务也修改了,机会出现问题,此时常用的方案是:
给数据表中添加一个version列,每次更新后都将这个列的值加1,读取数据时,将版本号读取出来
在执行更新的时候,会先比较版本号,如果相同则执行更新,如果不相同,说明此条数据已经发生了变化,就放弃更新或重试
MySQL日志类型
MySQL的很多功能都是依靠日志来实现的,比如事务回滚,数据备份,主从复制等等,常见的日志有下面几个
-
binlog归档日志
负责记录对数据库的写操作,一般用在主从复制过程中记录日志,从库拷贝此日志做重放实现数据同步
-
redolog重做日志
用于确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘
在重启 mysql 服务的时候,根据 redo log 进行重做,从而达到事务的持久性这一特性
-
undo log 回滚日志
保存了事务发生之前的数据的一个版本,可以用于回滚
MySQL主从复制的流程
主从复制用于MySQL主从集群的主节点向从节点同步数据,主要是依靠MySQL的binLog实现的,大体流程分为三步:
- Master 主库在事务提交时,会把数据变更记录在二进制日志文件 BinLog中
- 从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 RelayLog
- slave重做中继日志中的事件,将改变反映它自己的数据
谈谈你对sql的优化的经验
我在企业中优化Sql大体分为三步:
- 查找问题sql,主要手段是开启mysql的慢查询日志,它会将执行时间较长的sql记录记录下来
- 找到sql之后,我会分析出现问题的原因,原因很多,主要字段类型选择错误、sql语句效率低、索引失效等等
- 根据问题不同,我会再去定具体的解决方案
简单给您说几个常见的把
-
确定选择的引擎是否合适
myisam适合于查询为主,增删较少,无事务要求的数据表
Innodb适用于有事务处理,或者包括很多的更新和删除的数据表
-
表设计是否合理
单表不要有太多字段,建议在20以内
合理的加入冗余字段可以提高查询速度
-
确定字段的数据类型是否合适
数值型字段的比较比字符串的比较效率高得多,字段类型尽量使用最小、最简单的数据类型
设置合适的字符串类型(char和varchar)char定长效率高,varchar可变长度,效率稍低,varchar的长度只分配真正需要的空间
尽量使用TIMESTAMP而非DATETIME,尽量设计所有字段都得有默认值,尽量避免null
-
确定sql的书写是否有的题
SELECT语句务必指明字段名称,避免直接使用select *
SQL语句中IN包含的值不应过多
可以用内连接,就尽量不要使用外连接
使用连接连接查询来代替子查询
适用联合(UNION)来代替手动创建的临时表
-
表数据比较多的时候是否添加了合适的索引
表的主键、外键必须有索引
经常出现在where子句中的字段,特别是大表的字段,应该建立索引
经常用于排序、分组的字段,应当建立索引
加上索引之后,还应该使用Explain来确认索引是否生效
-
如果上面的几项都没有问题,那可能就是因为服务器性能或者数据量过大导致的查询慢,此时可以考虑读写分离
也就是我们搭建一个MySQL的主从集群,让1个主节点负责写入数据,多个从节点负责查询数据,已分摊查询压力
Redis
为什么用Redis
我们项目中之所以选择Redis,主要是因为Redis有下面这些优点:
- 操作速度快:Redis的数据都保存在内存中,相比于其它硬盘类的存储,速度要快很多
- 数据类型丰富:Redis支持 string,list,set,Zset,hash 等数据类型,基本满足我们开发中的各种使用场景
- 使用场景丰富:Redis可用于缓存,消息队列,按 key 设置过期时间,过期后将会自动删除
Redis的数据类型有哪些
Redis最常见的数据类型有5种,分别是String、List、Hash、Set、ZSet,下面给您详细介绍一下:
-
String:简单的 key-value 类型,最大能存储512MB数据。场景:计数、缓存文章标题、微博内容等
-
List:底层是链表,特点是:增删容易,随机访问困难。场景:发布与订阅或者说消息队列
-
Hash:类似于Java中的HashMap,适合存储对象。场景:系统中对象数据的存储
-
Set:是一种无序集合,可以方便的求交、并、差集。 场景:共同关注、共同粉丝、共同喜好等功能
-
ZSet:相比于set来讲,多了1个权重参数 score,元素会按照score进行排序。场景:各种排行榜,弹幕消息
Redis为什么这么快
Redis之所以运行速度比较快,主要是由于这样一些原因:
-
纯内存操作:Redis的绝大部分请求是纯粹的内存操作,非常快速
-
单线程:Redis的核心部分是单线程运行的,避免了不必要的上下文切换,也不存在线程切换导致的 CPU消耗
-
使用 I/O 多路复用模型和非阻塞 IO
什么是 I/O 多路复用
markdownI/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源 目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能 其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器 在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
Redis的过期删除策略有哪些
Redis的过期删除策略指的是当Redis中的key过期之后在什么时候进行删除的处理方案,常用的删除策略就两个:
- 惰性删除:只会在取出 key 的时候才对数据进行过期检查,过期了就删除
- 定期删除:每隔一段时间抽取一批 key执行删除过期 key 操作
两者相比,定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是定期删除+惰性/懒汉式删除。
Redis的内存淘汰策略有哪些
Redis的内存淘汰策略指的是当Redis的内存已经存满,又有新的数据需要保存时的处理方案,官方提供了8种淘汰策略:
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
- volatile-lfu:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰
- allkeys-lru:在所有的数据集中选择最近最少使用的数据淘汰
- allkeys-random:在所有的数据集中任意选择数据淘汰
- allkeys-lfu:在所有的数据集中选择最不经常使用的数据淘汰
Redis的RDB和AOF区别
Redis是一个基于内存的数据存储,为了保证数据安全,需要将内存中的数据备份到磁盘上,官方提供了两种数据持久化的方式,分别是RDB和AOF
-
RDB采用的是定期更新的方式,它会定期将Redis中的数据生成的快照同步到磁盘上,磁盘上保存的就是Redis的内存快照
优点是数据文件的大小相比于AOF较小,数据恢复速度较快
缺点是比较耗时,存在丢失数据的风险
-
AOF是将Redis所执行过的所有写指令都记录到磁盘上,在下次Redis重启时,只需要将指令重写一遍就可以了
优点是数据丢失的风险大大降低了
缺点是数据文件的大小相比于rdb较大,而且数据恢复的时候速度较慢
在我是同时开启RDB和AOF 持久化机制的,这样做的好处是:
- 在Redis重启时先使用AOF日志进行恢复,然后再使用RDB快照进行备份
- 而且将AOF的
appendfsync
参数为everysec
,保证每秒将AOF缓冲区中的写操作同步到 AOF 文件中,提高数据的持久化能力 - 定期进行RDB快照的备份,以便在需要时进行全量数据的恢复
这样的配置可以充分利用RDB和AOF两种持久化机制的优势,提高数据的可靠性和恢复能力
Redis集群有哪些方案
在Redis中提供的集群主要有三种,分别是主从、哨兵和分片集群
-
主从集群主要用来解决Redis的并发问题,一般是一个主节点负责数据写入,多个从节点负责数据读取,主节点的数据会实时同步给从节点
-
哨兵集群主要用来解决Redis的高可用问题,哨兵会监控集群中节点的状态,并在主节点出现问题时进行重新选主
-
分片集群主要用来解决Redis的海量数据存储问题,它要求有多个主节点,然后数据写入的数据会经过计算落到其中一个上
在这个计算的过程中Redis引入了哈希槽的概念,Redis集群有16384个哈希槽,每个 key通过CRC16校验后对16384取模来决定放置哪个槽
而分片集群的每个节点负责一部分 hash 槽,这样就可以计算出一个key会出现在哪个节点上了,查询的时候也是同时的方式来定位即可
如何保存Redis数据与MySQL一致
保证Redis和MySQL数据一致性的方案有很多,最常见的有三种
- 同步双写,即在程序更新完MySQL之后后立即同步更新redis
- 异步监听,即通过Canal监听MySQL的binlog日志变化,然后再通过程序将变化的数据更新数据到 Redis
- MQ异步,即程序在更新完MySQL后,发送一条消息到MQ中,然后在通过一个程序监听MQ,获取到消息,然后更新Redis
什么是缓存穿透, 怎么解决
在我们的项目中会将缓存放到数据库前面,查询的时候先查缓存,缓存有了就不用再去查数据库了,这样可以大大减轻数据库的访问压力
而缓存穿透指的是请求一直在查询一个数据库中不存在的数据,这样缓存中没有,请求就会到达数据库,而数据库也没有,也就没法缓存
所以每一次请求都会直接到数据库中查询,这就极有可能导致数据库被压垮
常用的解决方案有两个:
-
查询返回的数据为空,仍把这个空结果进行缓存,但过期时间尽量设置稍短一些
-
使用布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对DB的查询
什么是缓存击穿,怎么解决
在我们的项目中会将缓存放到数据库前面,查询的时候先查缓存,缓存有了就不用再去查数据库了,这样可以大大减轻数据库的访问压力
缓存击穿指的是对于一个设置了过期时间的key,在其缓存失效的瞬间,有大量的请求访问这个它,这些请求在缓存找不到就会直接到数据,导致数据库被压垮
常用的解决方案有两个:
-
使用互斥锁:当缓存失效时,不立即去数据库查询,而是先去获取一把全局锁,那个线程获取到了,就去数据库查询,获取不到的就等待重试查询缓存
-
修改设置key有效期的逻辑,大体如下:
在设置key的时候,不给它设置过期时间,而是单独设置一个过期时间字段一块存入缓存中
当查询的时候,从redis取出数据后判断时间是否过期,如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据
两种方案对比:
解决方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | 没有额外的内存消耗 ,保证一致性 | 线程需要等待,性能受影响 |
逻辑过期 | 线程无需等待,性能较好 | 不保证一致性,有额外内存消耗 |
什么是缓存雪崩,怎么解决
在我们的项目中会将缓存放到数据库前面,查询的时候先查缓存,缓存有了就不用再去查数据库了,这样可以大大减轻数据库的访问压力
缓存雪崩指的是大量的key在某一时刻同时失效,这样大量的请求全部转发到DB,DB 瞬时压力过重雪崩
解决方案也很简单,就是在设置key的过期时间的时候,尽量加一些随机值,这样缓存过期时间的重复率就会降低
用过Redis的事务吗
Redis中本身是没有事务的概念的,但是他有几个命令组合起来能实现类似于事务的效果。也就是说,Redis事务的本质是一组命令的集合。
这里用到的命令主要有5个,分别是:
- MULTI:用来组装一个事务
- EXEC:执行一个事物
- DISCARD:取消一个事务
- WATCH:用来监视一些key,一旦这些key在事务执行之前被改变,则取消事务的执行
- UNWATCH:取消 WATCH 命令对所有key的监视
总结说:Redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。Reids中,单条命令式原子性执行的,但事务不保证原子性,且没有回滚。
框架
Mybatis中#{}和${}的区别
在Mybatis中#{}和${}都可以用于在sql语句中拼接参数,但是在使用方面有很多的区别
1、处理方式不同: 表示的是字符串拼接, M y b a t i s 在处理它时,会直接将 {}表示的是字符串拼接,Mybatis在处理它时,会直接将 表示的是字符串拼接,Mybatis在处理它时,会直接将{}替换成变量的值
而#{}是预编译处理,Mybatis在处理它时,会将sql中的#{}替换为?号,然后底层使用JDBC的预编译对象来赋值
2、安全性不同:${}存在SQL注入问题,#{}可以有效的防止SQL注入
3、效率不同:${}处理的sql到数据库每次都要重新编译,而#{}处理的sql只需要编译一次
总之,在实际使用过程中尽量使用#{},而避免使用 ,当然这也不是说 {},当然这也不是说 ,当然这也不是说{}就没有使用场景
比如:如果sql中需要动态传递表名或者字段名,那就只能使用${}了
MyBatis动态SQL有哪些
MyBatis常用的动态SQL标签有:
- 条件判断标签:if、choose、when、otherwise 当条件成立时才执行其中的 SQL 语句
- 格式整理标签:trim、where、set 它可以在生成的SQL语句中调整格式,去除多余的关键字和符号
- 循环遍历标签:foreach 它用于遍历一个集合并将集合中的元素添加到 SQL 语句中
ResultType和ResultMap的区别
resultType和resultMap都用于查询结果的映射,区别点在于:
-
resultType :当数据库结果集中的列名和要封装实体的属性名完全一致的时候使用
-
resultMap :当数据库结果集中的列名和要封装实体的属性名有不一致的使用使用,它允许自定义映射规则
谈谈你对IOC的理解
IOC,也叫控制反转,是Spring用来解耦的一种设计思想,它的做法就是将对象的控制权由程序员手中反转到Spring手中
具体来说呢就是,在没有IOC之前,对象都是程序员在类中主动去创建,需要哪个创建哪个
有了IOC之后,对象会交给Spring容器创建和管理,如果哪个对象中需要其它对象属性,Spring也会自动完成依赖注入
总之一句话,IOC可以将对象的创建和对象之间依赖关系的维护交给Spring自动完成。
你用过哪些Spring注解
我们常用的Spring注解主要分类下面几大类:
1、创建对象:@Component、@Controller、@RestController、@Service
它们都可以标注在自己开发的类上,Spring会使用注解标注的类创建出对象,然后放入容器
2、依赖注入:@Autowired
标注在属性或者属性对应的set方法上,Spring会根据被标注属性的类型自动对属性进行赋值
3、依赖注入:@Qualifier
和@Autowired一块使用,在同一类型的bean有多个的情况下Spring会根据name进行选择注入
4、配置类:@Configuration、@Bean
主要标注在配置类中,用于声明配置类和向Spring容器中放入一些配置有关的对象
5、接收请求参数
@RequestBody: 接收请求体中的json数据
@PathViriable:接收请求路径中的参数
@RequestHeader:接收请求头中的参数
Spring中的bean线程安全吗
Spring中的Bean主要分为单例和多例
-
多例对象每次获取都会创建新实例,也就是说线程之间不存在Bean共享问题,也就不存在线程安全问题
-
单例对象是所有线程共享一个实例,因此就可能会存在线程安全问题。但是单例对象又分为无状态和有状态
无状态Bean是指只对对象的成员变量进行查询操作,不会修改成员变量的值,因此不存在线程安全问题
有状态Bean需要对Bean中的成员变量进行数据更新操作,因此就可能存在线程安全问题
所以,最终我们得出结论,在Spring中,只有有状态的单例Bean才会存在线程安全问题
如果要解决单例对象的线程安全问题,可以将需要的可变成员变量保存在ThreadLocal中, ThreadLocal本身就具备线程隔离的特性
这就相当于为每个线程提供了一个独立的变量副本,每个线程只需要操作自己的线程副本变量,从而解决线程安全问题。
谈谈你对AOP的理解
AOP,又叫面向切面编程,核心思想是将那些与业务无关,却为业务模块所共同调用的逻辑(例如事务、日志)封装起来,然后再动态插入到业务中
使用AOP可以减少系统的重复代码,降低模块间的耦合度, 我们在开发中用到AOP的主要使用场景有:事务管理、日志、性能监视、安全检查等
Spring的通知类型有哪些
通知是个在方法执行前或执行后要做的动作,实际上是程序执行时要通过SpringAOP框架触发的代码段
Spring切面可以应用五种类型的通知:
- 前置通知:在某切点之前执行的通知
- 返回后通知:在某切点正常完成后执行的通知
- 抛出异常后通知:在某切点抛出异常退出时执行的通知
- 后置通知:在某切点退出的时候执行的通知(不论是正常返回还是异常退出)
- 环绕通知:包围一个切点的通知
Spring事务传播行为有几种
事务传播行为是为了解决业务层方法之间互相调用的事务问题,当事务方法被另一事务方法调用时,应当指定事务应该如何传播。
例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
Spring支持7个种事务传播行为:
-
必须事务:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
-
必须新事务:创建一个新的事务,如果当前存在事务,则把当前事务挂起
-
强制事务:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
-
支持事务:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
-
不支持事务:以非事务方式运行,如果当前存在事务,则把当前事务挂起
-
强制无事务:以非事务方式运行,如果当前存在事务,则抛出异常
-
嵌套事务:如果当前存在事务,则创建一个当前事务的嵌套事务来运行;如果当前没有事务,则创建一个事务
嵌套事务是已存在事务的一个子事务,嵌套事务开始执行时,将取得一个保存点,
如果这个嵌套事务失败,将回滚到此保存点
嵌套事务是外部事务的一部分,只有外部事务结束后它才会被提交
Spring中的设计模式有哪些
- 工厂模式:Spring使用工厂模式通过 BeanFactory和 ApplicationContext创建 bean 对象
- 单例模式: Spring 中的 bean 默认都是单例的
- 代理模式:Spring 的 AOP 功能用到了 JDK 的动态代理和 CGLIB 字节码生成技术
- 模板方法:用来解决代码重复的问题。比如 RestTemplate、jdbcTemplate、 JpaTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式
- 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如 Spring 中 listener 的实现 ApplicationListener。
Spring如何处理统一异常
Spring的异常处理底层是通过AOP实现的,它的核心思想是将异常处理的代码和业务逻辑代码分离开来
使用它之后,我们在自己的业务代码中不需要在处理异常,有异常直接就上抛到框架中
框架就会将异常交给自定义的全局异常处理器中统一处理,自定义全局异常处理器,会用到两个注解:
- @RestControllerAdvice 标注在类上,声明被标注的类是一个用于专门处理异常的类
- @ExceptionHandler 标注在异常处理类中的方法上,声明被标注的方法可以处理哪些异常
SpringBoot的自动装配
自动装配就是自动地把其他组件中的Bean装载到IOC容器中,不需要开发人员再去配置文件中添加大量的配置
我们只需要在SpringBoot的启动类上添加一个@SpringBootApplication的注解,就可以开启自动装配
SpringBootApplication底层最重要的一部分是@EnableAutoConfiguration这个注解来实现的,它作用是:
- 读取所有jar包/META-INF/spring.factories文件中EnableAutoConfiguration键对应的值
- 这些值必须声明为Spring的配置类,也就是在类中需要向Spring容器放入对象
- 为了防止非当前所需的组件进入到容器,配置类中需要使用@Conditional注解来声明配置成立的必要条件
bootstrap.yml和application.yml有何区别
这是SpringBoot支持的两个核心配置文件,区别点在于
-
boostrap比applicaton优先加载,在应用程序上下文的引导阶段生效,且里面的属性不能被覆盖
一般来说我们在SpringCloud Config或者Nacos中会用到它
-
application用于SpringBoot项目的自动化配置,一般来说我们会将自己项目的业务配置项写在这里面
SpringBoot中如何解决跨域问题
跨域是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制
当浏览器从一个域名的网页去请求另一个域名的资源时,出现域名、端口、协议任一不同,都属于跨域
SpringBoot解决跨域很简单,执行添加一个配置类实现WebMvcConfigurer接口然后重写addCorsMappings方法即可
java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")//允许跨域访问的路径
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")//运行跨越的请求方式
.maxAge(3600);
}
}
拦截器和过滤器的区别是什么
拦截器和过滤器都可以实现请求的拦截处理,不同点有下面几个:
- 技术栈所属不同:过滤器属于JavaWeb技术,依赖Servlet容器;而拦截器是属于Spring的技术
- 实现原理不同:拦截器是基于Java的反射机制,而过滤器是基于函数回调
- 拦截范围不同:过滤器可以拦截所有请求,而拦截器主要是针对发往controller请求
- 拦截位置不同:过滤器在前端控制器前拦截行,而拦截器在前端控制器后拦截
SpringCloud组件有哪些
我们公司使用的是国内比较流行的SpringCloudAlibba框架,主要用到了下面几个组件:
-
Nacos:注册中心/配置中心
-
Ribbon:负载均衡
-
OpenFeign:服务调用
-
Sentinel:服务保护
-
Gateway:服务网关
JVM
JVM的主要组成部分有哪些
JVM主要分为下面几部分
-
类加载器:负责将字节码文件加载到内存中
-
运行时数据区:用于保存java程序运行过程中需要用到的数据和相关信息
-
执行引擎:字节码文件并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎将字节码翻译成底层系统指令
-
本地库接口:会被执行引擎调用参与字节码的翻译
在这里面最主要的部分是运行时数据区,它又由五部分构成,分别是:堆、方法区、栈、本地方法栈、程序计数器
- 堆是对象实例存储的主要区域
- 方法区可以认为是堆的一部分,用于存储已被虚拟机加载的信息,比如常量、静态变量等等
- 栈是程序方法运行的主要区域,栈里面存的是栈帧,栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息
- 本地方法栈与栈功能相同,区别在于本地方法栈执行的是本地方法,即一个Java调用非Java代码的接口
- 程序计数器主要存放的是当前线程所执行的字节码的行号,用于记录正在执行的字节码指令的地址
堆栈的区别是什么
堆和栈都是JVM的主要组成部分,不同点在于:
- 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的
- 堆会GC垃圾回收,而栈不会
- 栈内存是线程私有的,而堆内存是线程共有的
- 两者异常错误不同,栈空间不足:java.lang.StackOverFlowError,堆空间不足:java.lang.OutOfMemoryError
JVM的类加载器有哪些
类加载器的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。根据各自加载范围的不同,主要划分为四种类加载器:
-
启动类加载器(BootStrap ClassLoader):用于加载JAVA_HOME/jre/lib目录下的类库
-
扩展类加载器(ExtClassLoader):用于加载JAVA_HOME/jre/lib/ext目录中的类库
-
应用类加载器(AppClassLoader):用于加载classPath下的类,也就是加载开发者自己编写的Java类
-
自定义类加载器:开发者自定义类继承ClassLoader,实现自定义类加载规则
什么是双亲委派模型
双亲委派模型是Java中的一种类加载机制。
在双亲委派模型中,类加载器之间形成了一种层次继承关系,从顶端开始依次是:启动类加载器->扩展类加载器->应用类加载器->自定义类加载器
当一个类加载器需要加载某个类时,它首先会委派给其上层类加载器去尝试加载该类。如果父类加载器无法加载该类,子类加载器才会尝试加载。
这种层次关系形成了一个从上到下的委派链。
双亲委派模型的主要目的是保证Java类的安全性和避免类的重复加载。当一个类加载器收到加载请求时,它会首先检查自己是否已经加载了该类。
如果已经加载,则直接返回该类的Class对象;如果未加载,则将加载请求委派给父类加载器。
父类加载器也会按照同样的方式进行检查,直到顶层的启动类加载器。如果顶层的启动类加载器无法加载该类,那么子类加载器会尝试自己加载。
这样可以避免同一个类被不同的类加载器加载多次,确保类的唯一性。
双亲委派模型的优势在于能够保证类的一致性和安全性。
通过委派链的机制,可以避免恶意代码通过自定义的类加载器加载替换系统核心类,从而提高了Java程序的安全性。
此外,通过双亲委派模型,可以实现类的共享和重用,减少内存占用和加载时间,提高了系统的性能。
说一下类加载器的执行过程
类从被加载到虚拟机内存中开始,直到卸载出内存为止,整个生命周期包括了7个阶段:加载、验证、准备、解析、初始化、使用、卸载
- 加载: 这个阶段会在内存中生成一个代表这个类的java.lang.Class对象
- 验证: 这个阶段的主要目的是为了确保Class文件包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
- 准备: 这个阶段正式为类变量分配内存并设置类变量的初始值,注意这里的初始值指的是默认值,而不是代码=后的实际值
- 解析: 这个阶段将符号引用替换为直接引用,比如方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接引用方法
- 初始化: 这个阶段是执行类构造器方法的过程,是类加载的最后一步,到了这一步Java虚拟机才开始真正执行类中定义的Java程序代码(字节码)
- 使用: 这个节点程序在运行
- 卸载: 这个阶段类Class对象被GC
怎么判断对象是否可以被回收
在堆中存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事就是要确定哪些对象是要回收的
JVM认为不被引用的对象就是可以被回收的对象,而它确认对象是否还在被引用的算法主要有两种:引用计数法和可达性分析算法
-
引用计数法
在对象头处维护一个counter,每增加一次对该对象的引用,计数器自加,如果对该对象的引用失联,则计数器自减
当counter为0时,表明该对象已经被废弃,不处于存活状态,
但是此方法存在问题,假设两个对象相互引用始终无法释放counter,则永远不能GC
-
可达性分析算法
通过一系列为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链
当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可用的
可以作为GC Roots的对象一般有栈中引用的对象 、方法区中类静态属性引用的对象以及
JVM的垃圾回收算法有哪些
目前JVM中的垃圾回收算法主要有四个,分别是:标记清除算法、标记-整理算法、复制算法和分代收集算法
-
标记清除算法是将垃圾回收分为2个阶段,分别是标记和清除
它会先使用根据可达性分析算法找到垃圾资源进行标记,然后对这些标记为可回收的内容进行垃圾回收
这种算法的主要不足有两个:
-
效率问题,标记和清除阶段都要遍历多有对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的
-
空间问题,对象被回收之后会产生大量不连续的内存碎片,当需要分配较大对象时,由于找不到合适的空闲内存而不得不再次触发垃圾回收动作
-
-
标记整理算法也是将垃圾回收分为2个阶段,分别是标记和整理清除
它的第一阶段也是会先将存活的对象先标记出来
不一样的地方在于第二阶段,它会将所有存活的对象向前移动放在一起,然后将无用空间回收,这样就会出现连续的可用空间了
所以它解决了空间碎片问题,但是效率低的问题依旧存在
-
复制算法,将原有的内存空间一分为二,每次只用其中的一半
在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将当前内存空间清空,交换两个内存的角色,完成垃圾的回收。
这种算法的缺点在于分配2块内存空间,在同一个时刻,只能使用一半,内存使用率较低
-
分代收集算法,它会将整个堆内存分成几部分空间,每个空间中放入不同类型的对象,然后各自适合的算法回收
在JDK8时,堆被分为了两份:新生代和老年代,默认空间比例为1:2
对于新生代,内部又被分为了三个区域:Eden区,S0区,S1区,,默认空间比例为8:1:1
它的基本工作机制是:
当创建一个对象的时候,这个对象会被分配在新生代的Eden区,当Eden区要满了时候,触发MinorGC
当进行MinorGC后,此时在Eden区存活的对象被移动到S0区,并且当前对象的年龄会加1,清空Eden区
当再一次触发MinorGC的时候,会把Eden区中存活下来的对象和S0中的对象,移动到S1区中,这些对象的年龄会加1,清空Eden区和S0区
当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和S1中的对象,移动到S0区中,这些对象的年龄会加1,清空Eden区和S1区
对象的年龄达到了某一个限定的值(默认15岁),那么这个对象就会进入到老年代中,除此之外,大对象也会直接放入老年代空间
当老年代满了之后,触发FullGC**。**FullGC同时回收新生代和老年代
在上述过程中,新生代中的对象存活率比较低,所以选用复制算法;老年代中对象存活率高,所以使用标记-整理算法
小细节:
-
当对新生代产生GC:MinorGC,老年代代产生GC:Major GC ,新生代和老年代产生FullGC
-
Minor GC非常频繁,一般回收速度也很快,Major GC一般会伴随一次Minor GC,Major GC的速度要慢很多,一般要比Minor GC慢10倍
-
占用内存较大的对象,对于虚拟机内存分配是一个坏消息,虚拟机提供了一个-XX:PretenureSizeThreshold让大于这个设置的对象直接存入老年代
-
虚拟机给每个对象定义了一个Age年龄计数器,对象在Eden中出生并经过第一次Minor GC后仍然存活,年龄+1,此后每熬过一次Minor GC则年龄+1,
当年龄增加到一定程度(默认15岁),就会晋升到老年代。可通过参数设置晋升年龄 -XX:MaxTenuringThreshold
JVM的垃圾回收器都有哪些
JVM中常见的一些垃圾回收器有:
-
新生代回收器:Serial、ParNew、Parallel Scavenge
-
老年代回收器:Serial Old、Parallel Old、CMS
-
整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低
较低
-
分代收集算法,它会将整个堆内存分成几部分空间,每个空间中放入不同类型的对象,然后各自适合的算法回收
在JDK8时,堆被分为了两份:新生代和老年代,默认空间比例为1:2
对于新生代,内部又被分为了三个区域:Eden区,S0区,S1区,,默认空间比例为8:1:1
外链图片转存中...(img-K3qTtDzY-1724376063679)
它的基本工作机制是:
当创建一个对象的时候,这个对象会被分配在新生代的Eden区,当Eden区要满了时候,触发MinorGC
当进行MinorGC后,此时在Eden区存活的对象被移动到S0区,并且当前对象的年龄会加1,清空Eden区
当再一次触发MinorGC的时候,会把Eden区中存活下来的对象和S0中的对象,移动到S1区中,这些对象的年龄会加1,清空Eden区和S0区
当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和S1中的对象,移动到S0区中,这些对象的年龄会加1,清空Eden区和S1区
对象的年龄达到了某一个限定的值(默认15岁),那么这个对象就会进入到老年代中,除此之外,大对象也会直接放入老年代空间
当老年代满了之后,触发FullGC**。**FullGC同时回收新生代和老年代
在上述过程中,新生代中的对象存活率比较低,所以选用复制算法;老年代中对象存活率高,所以使用标记-整理算法
小细节:
-
当对新生代产生GC:MinorGC,老年代代产生GC:Major GC ,新生代和老年代产生FullGC
-
Minor GC非常频繁,一般回收速度也很快,Major GC一般会伴随一次Minor GC,Major GC的速度要慢很多,一般要比Minor GC慢10倍
-
占用内存较大的对象,对于虚拟机内存分配是一个坏消息,虚拟机提供了一个-XX:PretenureSizeThreshold让大于这个设置的对象直接存入老年代
-
虚拟机给每个对象定义了一个Age年龄计数器,对象在Eden中出生并经过第一次Minor GC后仍然存活,年龄+1,此后每熬过一次Minor GC则年龄+1,
当年龄增加到一定程度(默认15岁),就会晋升到老年代。可通过参数设置晋升年龄 -XX:MaxTenuringThreshold
JVM的垃圾回收器都有哪些
JVM中常见的一些垃圾回收器有:
-
新生代回收器:Serial、ParNew、Parallel Scavenge
-
老年代回收器:Serial Old、Parallel Old、CMS
-
整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低
老年代回收器一般采用的是标记-整理的算法进行垃圾回收