Java面试必备八股文
Posted -半度
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java面试必备八股文相关的知识,希望对你有一定的参考价值。
文章目录
- 一、Java基础篇
- 1.1)Java有哪几种数据类型
- 1.2)JVM、JRE和JDK的关系
- 1.3)Switch支持的数据类型?
- 1.4)为什么float=3.4报错
- 1.5)final 有什么用?
- 1.6)String有哪些特性
- 1.6)Stringbuffer和 Stringbuilder有什么不同?
- 1.7)== 和 equals 的区别
- 1.8)hashCode和equals
- 1.9)方法重载和方法重写区别
- 1.10)面向对象和面向过程的区别
- 1.11)ArrayList 和LinkedList 的区别
- 1.12)说一说HashMap数据结构
- 1.13)ConcurrentHashMap原如何保证的线程安全?
- 1.14)抽象类和接口的区别
- 二、多线程篇
- 2.1) *简述线程、进程的基本概念。以及他们之间关系是什么
- 2.2)线程池
- 2.2)线程、进程、协程的区别
- 2.3)什么是上下文的切换
- 2.4)Java有几种创建线程的方式
- 2.5)sleep和wait区别
- 2.6)Java的内存模型
- 2.6)保证并发安全的三大特性?
- 2.7)ThreadLocal原理
- 2.8)什么是CAS锁
- 2.9)Synchronized锁原理和优化
- 2.10)synchronized和ReentrantLock的区别:
- 2.11)为什么AQS使用的双向链表
- 2.12)有哪些常见的AQS类
- 2.13)死锁产生的4个必要条件
- 2.14)预防死锁
- 2.15)解除死锁
- 2.16)CPU密集型和IO密集型 的线程数设置
- 三、JVM篇
- 四、MYSQL篇
- 4.1)数据库三大范式是什么
- 4.2)说一说B和B+树的区别?
- 4.3)MyISAM 和 InnoDB的区别
- 4.4)什么是聚簇索引和非聚簇索引和覆盖索引
- 4.5)除了索引我们写SQL时候还有那些优化?
- 4.6)NLJ BNL 算法
- 4.7)为什么小表作为驱动表
- 4.7)索引下推:
- 4.8)MySQL的事务特性
- 4.9)使用索引就一定比没有使用索引好吗?
- 4.10)redolog好处?
- 4.11)结合redolog和undolog 说一说一条sql 的执行过程
- 4.12)MySQL锁的分类
- 4.13)什么是MVCC
- 4.14)select 的执行原理(顺序)
- 4.15)char和varchar区别
- 4.16)binlog日志的用途
- 4.17)如果我们发现一条Sql执行特别慢 我们该怎么去处理
- 4.18)哪些情况索引会失效
- 五、Redis系列
- 六、Spring系列
- 6.1)你怎么理解Spring?
- 6.2)Spring中的Bean 是线程安全的吗?
- **6.3)Spring单例,为什么controller、service和dao确能保证线程安全?**
- 6.2)什么是IOC和AOP,你知道他们的原理吗?
- 6.3)bean 的生命周期?
- 6.4)Bean实例化的时候是如何解决循环依赖?
- 6.5)**Bean实例化的三级缓存了解吗?**
- 6.6)SpringMVC的工作原理
- 6.7)Spring的事务实现方式?事务隔离级别?事务传播机制?
- 6.8)**讲一下SpringBoot里面比较常用的注解?**
- 6.9)MyBatis的 # 和 $ 有什么区别?
- 6.10)SpringBoot 是如何实现自动装配的?
- 6.11)什么情况下会出现事务失效场景?
- 七、分布式
- 七、计算机网络
一、Java基础篇
1.1)Java有哪几种数据类型
- 基本数据类型:byte(1字节) short(2字节) int(4字节) long(8字节) float(4字节) double(8字节) char(2字节) boolean(1字节)
- 引用数据类型:String 类 接口 抽象类 枚举 数组
1.2)JVM、JRE和JDK的关系
JVM指的是Java的虚拟机,Java程序需要运行在虚拟机上,不同的平台都有自己的虚拟机,所以Java语言实现了跨平台。
JRE指的是Java的运行时环境,包括需要的大量的类库和Java的虚拟机。
JDK指的运行时候的需要的一些工具类和运行时环境,比如包括javac.exe ,javadoxc.exe 一系列用于编译字节码工具 打包文档的工具
1.3)Switch支持的数据类型?
jdk1.5之前 byte、short、int、char
jdk5 ~ jdk1.7 加入了enum
jdk1.7之后 加入了String
*注意 不支持long类型
1.4)为什么float=3.4报错
3.4 默认是浮点double类型的,如果赋值给float是向下转型,会出现精度缺失,,需要强制转换
1.5)final 有什么用?
用于修饰类,方法,变量(属性):
- 如果修饰类,就是不能够继承
- 如果修饰方法就是不能够重写
- 修饰变量:修饰的变量是不能够修改的,如果是基本类型那么就是数值不能修改,如果是引用类型就是地址不能够修改。
- 成员变量:必须实现赋值
- 局部变量:声明的时候可以不赋值,后续允许一次赋值,赋值之后不能够修改
1.6)String有哪些特性
首先String是 private final char[]
- 数组 所以长度不可变
- final 不能够被继承
- private 内容不能够修改
1.6)Stringbuffer和 Stringbuilder有什么不同?
首先他们都是继承AbstractStringBuilder,相对于String来说是可变的字符串,只不过Stringbuffer加了synchronized所以是线程安全的,而Stringbuilder是线程非安全的
1.7)== 和 equals 的区别
== 默认是比较两个对象的引用地址的,而equals默认情况是==比较规则,但是不同的类会重写掉Object类的equals从而改变了equals的比较规则,比如String类的equals方法就是比较他们两个的内容是否相等
1.8)hashCode和equals
两个对象相等他们的hashCode和equals一定相等,但是hashCode相等的两个对象未必相等
1.9)方法重载和方法重写区别
- 实现方式:方法重载是在一个类当中;而方法重写是在父子类中实现的
- 方法重载是方法名字相同,参数个数,参数类型不同。和访问修饰符 和 返回类型无关;方法重写是方法名字相同,参数个数,参数类型必须相同,子类的返回值,抛出的异常必须小于等于父类。子类的访问修饰符大于等于父类。
1.10)面向对象和面向过程的区别
面向过程:就是具体化的一步一步的去实现,优点 就是性能比较快因为不需要进行实例化。 缺点就是不容易进行维护,扩展和复用
面向对象:因为面向对象的三大特点就是 封装、继承和多态
- 封装就是增加了代码的安全性,开发者不用在乎细节,只知道如何使用就可以了
- 继承就是代码的复用,结果设计模式就可以达到已于扩展和复用的特点
- 多态使用比较灵活,可以设计出低耦合的系统从而便于维护
1.11)ArrayList 和LinkedList 的区别
-
数据结构:
- ArrayList是底层是数组存储的,用于一大块连续的空间进行存储的,默认的初始容量是10 一般情况下当容量不够的时候会进行1.5 倍的一个扩容,因为是数组形式长度是不变的,所以在扩容的时候需要数据的搬迁,从而导致频繁的扩容会导致性能效率,所以我们在使用时候最好指定好大小。 因为有索引所以在查找、修改数据时候都是O(1)时间复杂度,而数据的增加删除确要移动后面的元素,所以时间复杂度是O(n)。
- LinkedList是双向链表的形式存储的,可以充分用碎片化的空间进行存储,查找,修改数据的时间复杂度都是O(n),因为都是要遍历整个链表。插入和删除除非是在指定的指针后面下进行插入和删除不然还是要遍历整个链表。所以说我们通常使用ArrayList进行数据存储。
1.12)说一说HashMap数据结构
HashMap通常是key-value键值对的方式进行存储,在JDK1.7的时候就是通过数组+链表的形式存储,每个数组存储的是一个一个的Entity组成的链表,这些链表上的数字都有一个特点就是Hash值相同,当数据量比较大的时候链表的长度比较大,这个时候查找的时间复杂度就比较大所以。在JDK1.8的时候引入了红黑树概念,当一个链表上的长度大于8的时候并且数组长度大于64这个时候链表就自动转换成红黑树,如果数组长度没有达到64那么此时就对数组进行1倍的扩容。
而且在JDK1.7和1.8还有一个改动就是插入的方式,由于JDK1.7的时候插入方式是头插法,在多线程的方式下会出选循环链表情况,所以1.8改为了尾插法方式。
扩容有三个地方
- 初始化数组长度为0
- 元素个数达到了 最大个数*0.75
- 单个链表的长度大于8并且数组长度大于64
HashMap是线程不安全的,如果要保证线程安全那么可以使用ConcurrentHashMap
1.13)ConcurrentHashMap原如何保证的线程安全?
JDK 1.7 时候是使用分成16个Seagment段,每个Seagment里面存储着一个HashMap和一个锁,所以说1.7能支持最大的并发量也就是16个线程
JDK1.8采用的CAS + Synchronized,每次插入的时候判断是否是第一次插入,是就通过CAS插入,然后判断f.hash是否=-1,如果是的那么其他线程在扩容,当前线程也会参与扩容;删除方法用了synchronized修饰,保障并发下删除元素的安全
1.14)抽象类和接口的区别
继承:抽象类是只能单继承的,接口是可以多实现的
属性:属性修饰符也不一样,抽象累可以是public protect 默认不写 还有final 修饰,但是接口只能是public static final修饰
构造方法:抽象类是可以进行实例化的,而接口是不能进行实例化
方法:抽象类既可以抽象的方法,也可以具体的方法,接口只有抽象的方法,而且子类必须实现
二、多线程篇
2.1) *简述线程、进程的基本概念。以及他们之间关系是什么
- 进程:是程序的一次执行的过程,是系统运行的基本单位,其中包含着程序运行过程中一些内存空间和系统资源。进程在运行过程中都是相互独立,但是线程之间运行可以相互影响。
- 线程:是进程的更小的单位。一个进程中包含着多个线程。和进程不同的是线程是共享着进程中的内存空间和系统资源的。所以在切换线程过程中开销要比进程小的多。
2.2)线程池
*线程池有七个参数?
corePoolSize: 线程池核心线程数最大值
maximumPoolSize: 线程池最大线程数大小
keepAliveTime: 线程池中非核心线程空闲的存活时间大小
unit: 线程空闲存活时间单位
workQueue: 存放任务的阻塞队列
threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
handler: 线程池的饱和策略事件,主要有四种类型。
*线程池的执行过程?
当提交一个线程执行任务时候 ,先看有没有空闲的核心线程如果有就执行,如果没有就看阻塞队列中有没有满的状态,如果没有放满则放入队列中,否则就直接看线程数量是否大于非核心线程数量,如果没有就直接创建一个非核心线程,否则就是按照指定的拒绝策略给处理。。如果一个非核心线程在某个一段时间类是空闲的那么线程池就会把这个线程自动销毁掉。
四种拒绝策略
- AbortPolicy(抛出一个异常,默认的)
- DiscardPolicy(直接丢弃任务)
- DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
- CallerRunsPolicy(交给线程池调用所在的线程进行处理)
五种阻塞队列 (5)
- ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
- LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
- DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
- PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列;
- SynchronousQueue(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。
五种创建线程的方式池的方式:
-
newFixedThreadPool (固定数目线程的线程池)
-
newCachedThreadPool(可缓存线程的线程池)
-
newSingleThreadExecutor(单线程的线程池)
-
newScheduledThreadPool(定时及周期执行的线程池)
-
new ThreadPoolExecutor() 自定义的方式创建
使用无界队列的线程池会导致内存飙升吗?
会的,newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升,最终导致OOM。
2.2)线程、进程、协程的区别
进程:进程是操作系统分配系统资源和内存空间的最小单位。进程是独立的一块空间,所以资源和内存空间的切换是特别消耗资源的。
线程:线程也叫做轻量级的进程,是操作系统调用执行的最小单位。线程的资源是依赖于他的父进程,所以他的资源是共享的,线程的切换需要转换到内核态开销相对于小一些。
协程:协程是一种轻量级的线程,协程是直接在用户态就可以控制,具有对内核态来说是不可见的,所以协程的上下文切换更加的节约资源消耗。
2.3)什么是上下文的切换
上下文的切换指的是CPU寄存器和程序计数器存储的数据进行切换,而内存数据的改变只有在内核态才能进行。所以上下文切换对系统来说是巨大的成本。
- 先是暂停原有的进程,保存好进程中寄存器中的数据在某个地方
- 内存获取中下一个进程上下文,存储到寄存器中
2.4)Java有几种创建线程的方式
- new 一个Thread
- 继承Runnable类
- 使用Callable
- 使用线程池
2.5)sleep和wait区别
- sleep 属于 Thread类,wait属于Object类
- wait会释放掉锁,sleep不会释放锁
- wait必须在同步代码方法和同步代码块中,sleep没有这一个限制
- wait()要调用notify()或notifyall()唤醒,sleep()自动唤醒
2.6)Java的内存模型
JMM屏蔽了各种硬件和操作系统的内存访问差异,实现让Java程序在各平台都能够达到一致的内存访问效果,它定义了Java如何将程序中的变量在主存中读取
具体定义:所有变量都在主存中,主存是线程的共享区域,每个线程都有自己独有的工作内存,线程想要操作变量必须从主存中copy一份到自己的工作区域,每个独立内存区域相互隔离。
所以这个时候读写存在延迟,且不是原子操作,所以就出现了一些列的线程安全操作。比如 原子性、可见性、有序性。
2.6)保证并发安全的三大特性?
原子性:一次或多次操作在执行期间不被其他线程影响
可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道**(利用内存原子操作解决或者内存屏障或者lock前缀)**
有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排**(内存屏障)**
2.7)ThreadLocal原理
每一个线程创建变量的副本,不同线程间是不可见的,保证线程安全。每个线程都维护一个ThreadLocalMap,key为threadlocal实例,value是保存的副本。
但是使用ThreadLocal会存在内存泄漏问题,因为key 是弱引用,value是强引用,每次GC时key都会回收,而value不会被回收。所以为了解决内存泄漏问题,可以在每次使用完后删除value或者使用static修饰ThreadLocal,可以随时的获取value。
第二个会出现内存溢出问题,如果使用的是线程池的方式去使用ThreadLocal话,那么就会出现存储的value一直存在,这样就一直堆积。
2.8)什么是CAS锁
CAS锁可以保证原子性,思想是更新内存是会判断其内存的值是否被修改了,如果没有被修改就直接更新,如果被修改了,就得重新去获取值,知道更新为止。这样是有缺点的:
- 只能支持一个变量的原子操作,不能保证整个代码块的原子操作
- CAS频繁的失败会造成CPU的开销打
- 会出现ABA问题
解决ABA问题,可以通过加入版本控制
2.9)Synchronized锁原理和优化
在JDK1.6以后Synchronized引入了偏向锁、轻量级锁、重量级锁、锁的粗化、锁消除的优化。并发性能基本和Lock持平的。
- 偏向锁:是不存竞争情况下,从而在后续的代码块中没有加锁解锁的开销
- 轻量级锁:轻量级锁所适应的场景是线程交替执行同步块的场合
- 重量级锁:重量级锁首先会经过一定次数的CAS自旋操作获取锁,如果获取失败,存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。是在竞争激烈的情况下创建一个monitor对象,并且将线程挂起,而挂起就要切换到内核状态执行。从而开销非常大。
轻量级锁:当锁是偏向锁时,有另外一个线程来访问,会撤销掉偏向锁,然后升级为轻量级锁,这个线程会通过自旋方式不断获取锁,不会阻塞,提高性能
重量级锁:轻量级锁尝试去获取到锁,如果获取失败线程就会进入阻塞状态,该锁会升级为重量级锁,重量级锁时,来竞争锁的所有线程都会阻塞,性能降低
注意,锁只能升级不能降级
2.10)synchronized和ReentrantLock的区别:
- 实现方面:一个是JVM层次通过,(监视器锁)monitor实现的。一个是通过AQS实现的
- 相应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
- 锁的类型不同:synchronized 是非公平的锁,而ReentrantLock即可以是公平的也可以是非公平的
- 获取锁的方式不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。
2.11)为什么AQS使用的双向链表
因为有一些线程可能发生中断 ,而发生中断时候就需要在同步阻塞队列中删除掉,这个时候就会用到prev和next方便删除掉中间的节点
2.12)有哪些常见的AQS类
- ReentrantLock 独占锁
- Semaphore 共享锁(限流有限资源)
- CountDownLatch 共享锁(合并汇总)
- CyclicBarrier 独占锁 + 条件队列
- ReentrantReadWriteLock 读写锁
2.13)死锁产生的4个必要条件
互斥条件:进程要求对所匹配的资源进行排他性控制,即是一段时间内某资源仅为一进程所占有
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:进程已获得的资源在未使用完之前不,不能剥夺,只能在使用完时有自己释放
环路等待条件:在发生死锁时,必须存在一个进程–资源得环星链
2.14)预防死锁
资源一次性得分配:一次性分配所有得资源,这样就不会有请求了(破坏请求条件);只要一个资源得不到分配,也不给这个进程分配其他得资源(破坏请求保持条件)
可剥夺条件:即当某个进程获得部分资源,得不到其他得资源,则释放已有得资源(破坏补课剥夺得条件)
资源有序分配法:系统分配资源编号,每一个进程按编号递增的顺序请求资源,释放则反之(环路破坏)
2.15)解除死锁
**剥夺资源:**从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
**撤消进程:**可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
2.16)CPU密集型和IO密集型 的线程数设置
CPU 密集型:对于这样的任务最佳的线程数为CPU核心数的1~2倍,如果设置过多的线程,就会造成不必要的CPU上下文切换,性能相对于比较低
IO密集任务:由于IO读写是比较消耗时间的,并不会特别的消耗CPU的资源。对于这种任务最大的线程数一般大于CPU核心数的很多倍。
线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间
三、JVM篇
3.1)Java中都有哪些引用?
强引用 :发生GC时候不会被回收
软引用:发生内存满(内存溢出的时候)会被回收(通常用于缓存)
弱引用:发生下一次GC时候会被回收
虚引用:无法通过虚引用去获取对象。用途:用于gc时返回一个通知
3.2)JVM运行时数据区域(内存结构)
首先编译器通过把Java代码转换成字节码,类加载器把字节码加载到运行数据区域的方法区内,而字节码只是一套JVM规范指令,所以这个时候需要执行引擎将字节码翻译成底层系统指令,在交给CPU去执行,这个过程需要用到其他语言本地库接口
- 线程私有:
- 本地方法栈:为native修饰的本地方法提供的空间
- Java虚拟机栈:每次调用一个方法都会产生一个栈帧,每个栈帧内部有 操作数栈,局部变量表 ,动态链接,方法出口等信息
- 程序计数器:用来记录程序执行的位置记录,方便切回后能够继续执行
- 线程共享:
- 虚拟机堆空间:JVM进行对象的创建存储空间,也是垃圾回收的主要区域,分为新生代和老年代
- 方法区:存放类信息、静态变量、常量、运行时常量池等信息。JDK1.8之前用的是永久代,之后用的是元空间
3.3)JVM类加载过程
主要分为 加载 -> 链接 - > 初始化 三步骤:
-
加载:把字节码通过二进制的方式转化到方法区中的运行数据区
-
链接:分为三步骤:
-
验证:校验字节码的正确性
-
准备:给静态变量分配内存空间 和 赋值操作(赋0值 ,比如String 赋值 null int 赋值 0 ……)
注意 final修饰的变量不是这个时候赋值的而是在在编译期间就已经赋值了
-
解析:解析是将常量池中的符号引用替换成直接引用的过程
符号引用:以一组符号(可以是任何格式的字面量,无歧义即可)来描述所引用的目标。
直接引用:可以直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。 -
-
初始化:初始化阶段可以看做是执行类的构造器()方法的过程。
注意:值得注意的是:加载阶段与连接阶段部分内容是交叉进行的,但开始时间有先后顺序。
3.4)对象的创建过程
1、首先进行类的加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等
2、分配内存
对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
这个步骤有两个问题:
1.如何划分内存。
2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
划分内存的方法:
-
“指针碰撞”(Bump the Pointer)(默认用指针碰撞)
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
-
“空闲列表”(Free List)
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
解决并发问题的方法:
-
CAS(compare and swap)
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
-
本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过
XX:+/UseTLAB
参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB
), XX:TLABSize 指定TLAB大小。
3、初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4、设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)
和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
5、执行方法
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。
3.4.2)如何定位一个对象多大的空间?
在HotSpot虚拟机里对象内存布局分为3个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
对象头:
- 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位的虚拟机(未开启压缩指针) 中分别为32个比特和64个比特, 官方称它为“Mark Word”。
- 另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
如果对象是一个 java 数组,那么在对象头中还有一块用于记录数组长度的数据。
案例:
1)创建一个maven项目,引入依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
2)新建一个类
@Data
public class BaseEntity
private long id;
private double amount;
private int updateUserId;
private float f;
private char c;
private byte b;
private boolean bb;
private short ss;
private long[] list; //4byte
private String s; //4byte
private Long count; //4byte
3)测试
public class ObjectHeaderTest
public static void main(String[] args)
BaseEntity baseEntity = new BaseEntity();
String toPrintable = ClassLayout.parseInstance(baseEntity).toPrintable();
System.out.println(toPrintable);
打印结果
site.sunlong.obj.BaseEntity object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c143
12 4 int BaseEntity.updateUserId 0
16 8 long BaseEntity.id 0
24 8 double BaseEntity.amount 0.0
32 4 float BaseEntity.f 0.0
36 2 char BaseEntity.c
38 2 short BaseEntity.ss 0
40 1 byte BaseEntity.b 0
41 1 boolean BaseEntity.bb false
42 2 (alignment/padding gap)
44 4 long[] BaseEntity.list null
48 4 java.lang.String BaseEntity.s null
52 4 java.lang.Long BaseEntity.count null
Instance size: 56 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total
4)测试结果解读
数据第1行(object header: mark):这个是Mark Word占用的字节数,64位机器上占用8个字节,32位机器上占用4个字节。
数据第2行(object header: class) :类型指针,在开启指针压缩的情况下占4个字节,未开启的情况下占8个字节,jdk1.6之后默认开启指针压缩。
数据第3-14行(除(alignment/padding gap)):BaseEntity对象属性类型占用字节数,一共占用39个字节。
其他数据行:对齐填充2个字节,由于Mark Word(8个字节)+类型指针(4个字节)+对象字节数(42个字节)=54个字节,54不是8的倍数,所以要填充2个字节凑够8的倍数。如果字节数之和刚好是8的倍数,则不需要对齐填充。
3.4.3)JVM类初始化顺序
父类静态代码块和静态成员变量->子类静态代码块和静态成员变量->父类代码块和普通成员变量->父类构造方法->子类代码块和普成员变量->子类构造方法
3.5)什么是内存泄漏?
就是不再使用的对象或者变量一直占用着内存中,而且GC也无法去回收。这种情况是因为长生命周期的对象持有短生命周期的对象的引用就会发生内存泄漏情况。
3.5.2)什么情况下会内存溢出?
堆内存溢出:(1)当对象一直创建而不被回收时(2)加载的类越来越多时(3)虚拟机栈的线程越来越多时
栈溢出:方法调用次数过多,一般是递归不当造成
3.6)简述一下Java的垃圾回收机制?
在Java中,程序员不需要去指定的释放对象。而是有JVM虚拟机自行的执行,有一个垃圾回收线程(守护线程)他的优先级比较低,他不会去主动的去执行,只有虚拟机空闲
或者当前的堆空间满
的时候,才会去触发,去扫描回收掉没有用的对象。
当创建一个对象的时候GC就会进行监控这个对象的地址、大小以及使用情况 。GC通过有向图的方式记录管理堆,通过这种方式来判断那些可达,哪些不可达,从而来判定是否回收。
程序员可以手动的进行System.gc()来通知gc运行,但是GC并不能保证GC一定会执行
3.7)如何判断是否能够回收?
两种方式:
- 引用计数器法:但是不能避免循环引用问题
- 可达性分析算法:当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
3.8)垃圾回收算法有哪几种?
-
标记清除: 标记需要清除的对象,对标记的对象进行清楚操作。缺点:产生了大量的不连续碎片化对象,不利于分配新的对象。
-
标记整理:标记需要清除的对象,对标记的对象进行清楚操作过程中,也将没有清除的对象进行整理到内存的一端。优点:解决了标记-清理算法存在的内存碎片问题 缺点:需要局部的移动对象,降低的效率
-
复制算法:将内存空间分称等大的两部分,每次遍历一个区域将存货的对象放入另一个区域,然后清除掉掉这个区域所有的对象,以此类推循环。 优点:效率高速度快,也没有内存碎片的问题 缺点:堆内存大小要求高
-
分代算法:就是把内存空间分配成不同的年轻代、老年代和永久代,根据不同的年代使用不同的算法。
比如说 年轻代空间大而且垃圾回收发生频繁 所以采用
复制算法
,老年代就采用标记整理算法
3.9)垃圾收集器
Serial收集器:色而瑞
新生代采用复制算法,老年代采用标记-整理算法。单线程
,所以在发生GC时候需要暂停所有线程的工作。
Parallel Scavenge收集器:帕尔雷尔
新生代采用复制算法,老年代采用标记-整理算法。多线程
,Parallel Scavenge收集器关注点是**吞吐量(**高效率的 利用CPU)
ParNew收集器:帕尔new(jdk8默认设置的垃圾收集器)
新生代采用复制算法,老年代采用标记-整理算法。和Parallel 相似主要用于配合CMS收集器使用。
CMS收集器:
CMS收集器是一种 “标记-清除”算法实现的。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户 体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用 户线程(基本上)同时工作。
- 初始标记: 暂停所有的其他线程(STW),并记录下gc roots
直接能引用
的对象,速度很快。 - 并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但
是不需要停顿用户线程
, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。 - 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对
象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。 - 并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑
色不做任何处理(见下面三色标记算法详解)。 - 并发重置:重置本次GC过程中的标记数据。
3.10)内存对象的分配策略?
对象的内存分配通常是在 Java堆
上分配(随着虚拟机优化技术的诞生,某些场 景下也会在栈
上分配,
后面会详细介绍),对象主要分配在新生代的 Eden
区, 如果启动了本地线程缓冲,将按照线程优先在
TLAB
上分配。少数情况下也会直 接在老年代
上分配。总的来说分配规则不是百分百固定的,其细节取
决于哪一种 垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵 循以下几种
-
对象优先在 Eden 区分配
多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行 分配时,虚拟机将会发
起一次 Minor GC。如果本次 GC后还是没有足够的空 间,则将启用分配担保机制在老年代中分配内存。年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间,如果空间不够的话就直接进行FullGC
-
大对象直接进入老年代
所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导 致在内存还有不少空间
的情况下提前触发 GC 以获取足够的连续空间来安置新对 象。前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象 直接在新生代分配就会导
致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。 -
长期存活对象将进入老年代
虚拟机采用分代收集的思想来管理内存,那 么内存回收时就必须判断哪些对象应 该放在新生代,哪些对
象应该放在老年代。因此虚拟机给每个对象定义了一个对 象年龄的计数器,如果对象在 Eden 区出生,
并且能够被 Survivor 容纳,将被 移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区
中每「熬 过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升 到老年代。 -
空间分配担保
-
动态年龄判定
3.11)说一说如何理解双亲委派模型
类加载器主要有:
- 引导类加载器(BootstrapClassloader):负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如
rt.jar、charsets.jar等 - 扩展类加载器(ExtceptClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR
类包 - 应用程序类加载器(ApplicationClssloader):负责加载ClassPath路径下的类包,主要就是加载你自己写的那
些类
双亲委派模型就是子加载器向父级加载器向上委托它加载,如果父加载器能加载那么子加载器就不用加载直接返回,如果不能加载就有子加载器进行加载。
这样做的好处就是:
- 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
- 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
**如何打破双亲委派模型:**重写loadClass(String, boolean)
方法
四、mysql篇
4.1)数据库三大范式是什么
第一范式:每个列都不可以再拆分。
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我 们经常会为了性能而妥协数据库的设计。
4.2)说一说B和B+树的区别?
B树的数据即存在非叶子节点上也存在于叶子节点上,而B+树所有的数据都是存在叶子节点。就是因为这个原因所以B+树存储的数据比较多,因为一个页的大小是16KB而B树的非叶子节点又存储了数据data,所以一个页锁存储的节点少一些,故如果使用B树那么层数会相应提高很多。
B+树的叶子节点还采用了双向链表的结构,这样也可以可以方便使用范围查找和所有数据的遍历。而B树有的数据存储在非叶子节点就做不到这一点,必须经过
Java~大厂面试八股文~强烈推荐视频
Java面试~八股文
1. 集合类-设计模式-并发-虚拟机-框架(黑马程序员)
黑马程序员Java面试必考真题
链接: 最全Java面试题视频教程,大厂Java面试突击技巧,工作几年和应届生必看的黑马程序员Java面试必考真题.
2. 高并发架构(高并发高可用)-消息队列-缓存
中华石杉老师的《Java 面试突击第一季》
链接: 视频地址.
链接: 配套笔记在线阅读地址.
3. 图灵学院的《Java 常见面试题详解系列》
链接: 视频地址.
Java 面试常考题,涵盖 Java 核心知识、数据库以及常见框架。
拿数据库和缓存来说:数据库以面试常问的 MySQL 为例介绍了索引、锁、事务、主从同步、MyISAM 和 InnoDB 的区别、分库分表、慢查询处理等面试题。缓存以面试常问的 Redis 为例介绍了 Redis 常见数据库结构、缓存过期策略、 缓存穿透、缓存击穿、缓存雪崩、数据库和缓存一致性保证、Redis 高可用等面试题。
4. 享学的《Java 面试全解析系列》
链接: 视频地址.
内容比较杂,可以挑选自己比较感兴趣面试题学习。可以重点看看数据库这块,对于常见的 MySQL 面试题比如 MySQL 索引数据结构介绍的比较详细.
5. 图灵的《分布式面试核心面试题系列》
链接: 视频地址.
主要是分布式相关的内容,涵盖负载均衡、分布式 ID、分布式事务、Dubbo、Zookeeper 、Redis。
6. 尚硅谷周阳老师的 《Java 面试题第三季》
Java 培训领域比较出名的周阳老师的作品,内容涵盖算法、Java 核心知识、数据库以及常见框架。
可以重点看看并发和 Spring 这块,比其他老师讲的要深入和好理解很多。
链接: 视频地址.
以上是关于Java面试必备八股文的主要内容,如果未能解决你的问题,请参考以下文章