Java基础面试题总结

JVM

  • 类加载机制

虚拟机将class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,此为类加载机制
类加载过程包括:加载、验证、准备、解析和初始化五个阶段,其中验证、准备和解析3个部分统称为连接
注意:解析不一定在初始化前执行,也有可能在初始化后,为支持Java的运行时绑定
类加载过程
各个阶段的作用:

  1. 加载:查找并加载类的二进制文件
  2. 验证:确保被加载类的正确性
  3. 准备:为类的静态变量分配内存,并将其初始化为默认值
  4. 解析:把类中的符号引用改为直接引用
  5. 初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化
  • 双亲委派机制

双亲委派机制
当一个class文件要被加载时,首先检查应用加载器是否加载过,如果加载过,则返回,否则则向上,找父加载器,父加载器验证是否加载过,加载过则返回,否则依次递归向上找,直到BootsrapClassLoader之前,都是再验证是否加载过,而不是直接去加载,到BootsrapClassLoader加载器,这时候考虑是否自己能加载,如果自己不能加载,则向下让子类去加载,一直到底层加载器都无法加载,则会抛出异常

  • JVM内存结构

线程私有的有:程序计数器、虚拟机栈、本地方法栈
线程共享的有:堆和方法区

  1. 程序计数器:用于保存JVM下一条所要执行指令的地址
  2. Java虚拟机栈:每个线程运行需要的内存空间,每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等
  3. 本地方法栈:是一些带有native关键字的方法,需要Java去调用本地的C或C++方法
  4. Java堆:是被所有线程共享的一片区域,虚拟机启动时创建,存储对象实例,通过new关键字创建的对象都会使用堆内存
  5. 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量和即时编译后的代码等
  • 判断是否可进行垃圾回收的算法?

引用计数器和可达性分析

  1. 引用计数器,当对象增加引用时,计数器加一,减少引用时,计数器减一,计数器为0时,则可以被回收;缺点是:会出现循环引用的情况,两个对象互相引用,Java虚拟机不使用此方法
  2. 可达性分析:通过GC Roots 作为起点进行搜索,能够到达的对象都是存活的,不能到达的都是可被回收的
    GC Roots 一般包含哪些?
  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类静态属性或常量引用的对象
  • 垃圾回收算法
  • 标记清楚算法:将存活的对象进行标记,然后清理掉,未被标记的对象,缺点是:会产生大量不连续的内存碎片,导致无法给大对象分配内存
  • 标记整理算法:先采用标记清楚算法确定可回收的对象,然后整理剩余对象,将可用的对象移动到一起,使内存紧凑,优点是内存利用率高,缺点是速度慢
  • 复制算法:将内存分为两个等大小的区域from和to(to中为空),将GC Roots引用的对象,从FROM放入TO,再回收FROM中不被引用的对象,此时交换from和to。优点是,不会有碎片产生,缺点,只能利用一半的内存空间
  • 分代收集,根据对象的存活周期,将内存划分为几块,不同块采用不同的算法
    一般分为新生代(伊甸区(新产生的对象会在这里)和幸存区(经过一次Mintor GC之后会进入幸存区))和老年代,老年代是每次回收只有少量的对象需要回收(标记清楚或标记整理),而新生代每次都有大量的对象需要被回收(复制算法
    堆内存划分
  • 避免内存泄露的方法?

尽量不要使用,static成员变量,减少生命周期
及时关闭无用的资源
不用的对象,可以手动设置为null

面向对象

  • 重载和重写的区别

重载是指在同一类中可以有多个方法名相同但参数类型、参数个数或参数顺序不同的方法
重写是指在子类中,可以对父类中的方法,进行重写, 重写方法必须与被重写方法拥有相同的方法名、返回值类型和参数列表,但是可以更改访问修饰符(访问修饰符范围大于等于父类)、抛出的异常类型(抛出的异常范围小于等于父类)、方法返回值(方法返回值,小于等于父类)和方法体等。

  • 接口和抽象类的区别
  1. 所使用的关键字不同,接口定义时,所用的关键字是:interface,抽象类是abstract
  2. 接口中只能包含抽象方法、静态方法和default关键字修饰的默认方法,不能包含实例变量和构造函数
  3. 抽象类中,除了可以定义抽象方法外,还能定义非抽象方法,可以有实例变量和构造函数
  4. 从继承性来看,一个类,只能继承自一个抽象类,但可以实现多个接口,抽象类表现的是一种,“is a”的思想,而接口表达的是一种 “like a”
  5. 抽象类设计的目的是提供类的继承机制,实现代码复用;而接口是为了提供一种规约,实现类遵循特定的行为和功能
  • equals 和“==”的区别

在Java中,== 是一个比较操作符,用于比较两个变量的值是否相等,而equals是Object类中定义的方法,用于比较两个对象是否相等
== ,对于基本数据类型比较的是两个变量的值是否相等,对于引用类型,比较的是两个对象,所指向的地址值是否相等
equals 用于比较两个对象的内容是否相等,在equals没有被重写的情况下,和== 的作用是一样的,比较的是对象的地址值,但可根据,实际需要,对equals 进=进行重写,以实现自定义的比较逻辑

  • final 、finally 和finalize的区别

final是一个修饰符,修饰类,表示类不能被继承;修饰变量,表示变量不能被修改;修饰方法,表示方法不能被重写
finally 是一个关键字,通常和try……catch一起使用,表示,无论是否发生异常,被finally包裹的代码块,都会被执行,通常用于资源的释放,连接关闭
finalize,是object中定义的方法,被用于垃圾回收时,由垃圾回收器进行自动调用

  • String、StringBuilder和StringBuffer的区别

从可变性上来看,String是不可变的(底层是final修饰的字符数组),每次针对String对象的操作,都会创建新的对象;而StringBuilder和StringBuffer都是可变对象
从线程安全的角度看,String(不可变)和StringBuffer(方法被Synchronized修饰)是线程安全的,而StringBuilder不是线程安全的,多线程环境下,需要手动进行同步控制
从性能来看,String的性能是最差的,因为其不可变性,每次操作都会创建新的对象,会导致内存消耗;StringBuffer,因其可变性,针对字符串的修改,是在原有的对象上操作,性能更好;StringBuilder和StringBuffer类似,但不保证线程安全,单线程环境下性能会更好

  • Java基本数据类型?

byte、short、int、long
float、double
char
boolean

集合

  • Java中的容器有哪些?

容器包含:collection和Map两大类,collection是存储对象的集合,而Map是存储着键值对的映射表。
在这里插入图片描述

  • ArrayList和LinkedList的区别?

数据结构不同,arrayList是基于数组实现的,linkedList基于双向链表实现
查找效率不同,因arrayList是基于数组的,对集合内元素的访问,可以直接通过下标,而linkedList,则需要通过遍历获取要查找的元素
增删效率,对于,非收尾的增加和删除,linkedList的效率要高,因为arrayList需要移动和复制数组,效率要低

  • Vector 和ArrayList的区别?

两者底层都是数组
vector里面的方法(例如:add、remove)都是通过synchronized关键字修饰,所以是线程安全的,性能更低,arrayList不是线程安全的
扩容机制,vector每次扩容为原来的两倍(没有指定扩容递增值capacityIncrement的情况下),而arrayList,每次为原来的1.5倍

  • ArrayList的扩容机制?

arrayList,调用add方法的时候通过ensureExplicitCapacity 方法判断是否需要扩容
arrayList的扩容流程

		private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 扩容为原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 验证后扩容后是否满足
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            // 不满足,则验证是否大于最大的数组值(integer最大值-8)
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
  • HashMap的实现原理?

JDK1.7时,底层数据结构为数组+链表
JDK1.8时,底层数据结构为数组+链表/红黑树
hashMap通过,hash函数,计算得到hash值,通过(n-1)& hash(n为数组的长度),获取元素存放的位置,如果当前位置存在元素,则判断值(此处指的是key值)是否相同,相同则替换,不相同,则通过拉链法(将同一hash值的数据,以单链表的形式进行存储)解决hash冲突。
而JDK1.8 引入红黑树,主要解决,链表太长,所造成的查询性能问题

  • hashMap什么时候会扩容?

put元素后,验证此时,map中元素的个数+1,是否大于数组的初始容量*负载因子(默认0.75),大于则扩容,扩容变为原来的二倍
++size(map中元素个数) > threshold(数组容量*0.75)

  • hashMap中put方法的执行流程?
  1. 计算key的hash值
  2. 若数组为0,则初始化数组
  3. 如果通过hash计算出的key所对应的下标,里面没有元素,则直接放入
  4. 如果存在元素,则验证此处对应key的第一个元素key是否和待插入的key相同,相同则进行替换
  5. 如果存在元素,且第一个元素是树节点,则将此key放入树节点中
  6. 如果存在元素,且第一个元素是链表,则将其放入链表中
  7. 验证链表是否需要树化
  8. 插入元素之后验证,数组是否需要扩容
  • hashMap中get方法的执行流程?
  1. 计算key对应的hash值
  2. 找到key所在位置的第一个元素
  3. 如果是其待查找的key,则将对应值返回
  4. 若是树节点,则按照树的方式查找
  5. 若是,链表,则按照链表的方式查找
  6. 没有则返回null
  • hashMap什么时候树化?什么时候树化退化?

树化满足的条件:

  1. 链表长度超过树阈值>8
  2. 数组容量大于等于64
    当链表长度大于阈值8之后,先尝试扩容减少链表长度,如果数组已经达到64,则会树化
    树退化的两种场景:
  3. 扩容时,如果拆分树,树元素个数小于等于6,则退化为链表
  4. 移除树节点时,若root、root.left、root.right,有一个为null,则退为链表
  • hashMap的扩容因子为啥默认是0.75?

在空间占用和查询时间之间获取平衡
大于这个值,空间节省了,但是链表就会较长,而影响查询性能
小于这个值,冲突减少了,但是扩容频繁,空间占用多

  • hashMap为什么使用红黑树?

主要为了提供查询性能,红黑树本身是一种平衡二叉树,插入数据时,会通过左旋、右旋等操作,来保持平衡,解决单链表查询深度的问题

  • hashMap和hashTable的区别?
  1. hashtable,是线程安全的,底层方法是通过synchronized关键字修饰,hashMap是线程不安全的,hashMap性能更高,线程安全推荐使用ConcurrentHashMap
  2. hashMap允许key和Value为null值,hashtable不允许
  3. 扩容机制不同,不指定初始容量时,hashtable初始容量为11,每次扩容为原来的2n+1,hashmap默认初始容量为16,每次扩容为原来的两倍,如果指定初始容量,hashtable,则直接使用初始容量,而hashMap,则是,将初始容量定为指定容量的二倍
  4. 底层数据结构不同,hashmap通过链表和红黑树,来解决hash冲突,而hashtable没有

I/O

反射

异常

  • finally块中的代码什么时候会被执行

finally 块的作用,就是为了保证,无论出现什么情况,finally块中的代码都会被执行,finally中的代码,会在return之前执行

多线程

  • 线程和进程的区别

进程:内存中的运行程序,具有独立的内存空间,一个进程可以有多个线程(进程是资源分配的最小单元
线程:进程中的一个控制单元,线程拥有独立的运行栈和程序计数器,线程间共享方法区和堆内存线程是任务调度的最小单元

  • 并行和并发的区别

并发:多个任务在同一个CPU上,按照细分的时间片,交替执行
并行:单位时间内,多个CPU同时处理多个任务

  • 用户线程和守护线程的区别

用户线程运行在前台,执行具体的任务,比如程序的执行入口:main线程
守护线程,主要为用户线程提供服务的,也叫:服务线程、精灵线程,一旦所有的非守护线程退出后,守护线程也会结束
JVM中垃圾回收线程,就是典型的守护线程
守护线程设置:new Thread().setDaemon(true);

  • 线程的状态:

线程状态的几种说法?
5种状态,创建、就绪、运行,阻塞、终止
6种状态,创建、就绪、阻塞、计时状态、无限等待状态、终止
在这里插入图片描述

7种状态,创建、就绪、运行,阻塞、计时等待、无限等待、终止
- 创建:也叫初始态,刚被new出来
- 就绪:调用了start方法,等待cpu调度
- 运行:执行线程体
- 阻塞:wait等待的时间到了,或者其他线程调用了notify方法,但当前线程并没有获取到锁对象,就会进入阻塞,只要一获取到锁,就会进入就绪状态
- 计时等待:调用sleep或是wait方法,指定等多长时间或睡多久后,线程进入计时等待状态,睡眠时间到或等待的时间到,或调用了notify方法,进入就绪状态,还有一种是,即使时间到了,或者是调用了notify,但没有获取到锁,此时会进入阻塞状态
- 无限等待:调用了wait方法,无限等待,释放了锁
- 终止:死亡状态,正常执行完线程体或是,线程中出现异常,不可捕获导致终止

  • sleep和wait方法的区别?
  1. 所属的类不同,sleep是thread的静态方法,wait是object的方法
  2. sleep不会释放锁,wait会
  3. wait主要用于线程间的通信,和notify和notifyAll搭配使用,sleep只是暂停执行
  4. wait和sleep都可以实现计时等待,但是,wait时间到了之后,需要重新获取锁,没有获取到则进入阻塞,但是sleep会立马进入就绪状态,不需要重新获取锁,若cpu不空闲,则进入阻塞
  • 什么是可重入锁?

可重入性:一个线程持有锁时,当其他线程尝试获取该锁时,会被阻塞;而当前线程,尝试获取自己持有的锁时,如果可以成功,则说明,锁是可重入的,否则不是。
可重入锁,也叫递归锁,允许同一个线程在已经持有锁的情况下再次获取同一个锁,而不会被阻塞。
可重入锁,可以防止,同一线程,多次获取锁,而导致死锁的发生

  • 有哪些实现了可重入锁?

synchronized 实现可重入性

  • synchronized 经过编译后,会在同步代码块前后形成两个字节码指令monitorenter和monitorexit。针对每个锁,维护一个计数器,初始值为0,标识任意线程,都可以获取锁,并执行对应逻辑,在获取锁时,会执行monitorenter指令,如果没有线程获取到锁,讲计数器加一,执行monitorexit,计数器减一,直到,计数器为0,锁被释放,其他线程才能获取锁
    ReentrantLock实现可重入性
  • ReentrantLock,通过使用Sync内部类来管理锁,Sync又有两个实现类fairSync(公平锁)和NonFairSync(非公平锁),所以真正的获取锁,是由两个实现类控制的,ReentrantLock继承自AQS,AQS内部维护了一个计数器,来计算重入次数,避免频繁持有锁
  • 当一个线程在尝试获取锁时,先判断state是否为0,若为0,标识没有线程占用,则可获取
    若不为0,则判断当前持有锁的线程是不是自己,若是,则将计数器加一,标识重入,代码实现如下:
		protected final boolean tryAcquire(int var1) {
            Thread var2 = Thread.currentThread();
            int var3 = this.getState();
            if (var3 == 0) {
                if (!this.hasQueuedPredecessors() && this.compareAndSetState(0, var1)) {
                    this.setExclusiveOwnerThread(var2);
                    return true;
                }
                //判断是否是当前线程
            } else if (var2 == this.getExclusiveOwnerThread()) {
                int var4 = var3 + var1;
                if (var4 < 0) {
                    throw new Error("Maximum lock count exceeded");
                }
                this.setState(var4);
                return true;
            }
            return false;
        }
    }
  • 公平锁和非公平锁的区别

公平锁:指多个线程,按照申请的顺序,依次获取锁,线程直接进入队列中等待,按照队列的先进先出原则,最先进入队列的最先获取锁
非公平锁:多个线程加锁时,直接尝试获取锁,能抢到则直接占有锁,否则进入等待队列
公平锁和非公平锁的两点区别:

  • 非公平锁,在调用lock获取锁时,会直接调用AQS进行一次抢锁,如果恰巧此时没有占用,则直接获取锁,而公平锁是,直接进入tryAcquire方法,尝试获取锁
  • 非公平锁,在第一次没有抢占成功时,和公平锁一样都会进入,tryAcquire方法,尝试获取锁,非公平锁是,尝试获取锁时,如果发现state此时为0,则会再次调用AQS抢锁,而公平锁,是先判断,阻塞队列中是否有线程处于等待,有则不去抢锁
    非公平锁效率较高,但非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
  • 创建线程的方法
  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口(结合FutureTask使用)
  4. Executors线程池创建
  • 死锁
  1. 什么是死锁?
    死锁是两个或两个以上线程,由于竞争资源或是彼此通信,所造成的一种阻塞现象,若无外力作用,都将无法推进,此时系统处于死锁
    如图:资源1和资源2都只能被一个线程占有,此时线程A拥有资源2,线程B拥有资源2,线程A只有获得资源1后,才能执行完成,线程B只有拥有资源2之后才能执行完成,此时,两个线程相互等待
    死锁示例
  2. 形成死锁的四个必要条件
  • 互斥条件:线程对所分配的资源具有排他性,即一个资源只能被一个线程占用
  • 请求与保持条件:因请求被占有的资源而发生阻塞时,对已获取的资源不释放
  • 不可剥夺条件:对已获取的资源,在未使用完之前,不能被其他线程所占用
  • 循环等待条件:发生死锁时,所等待的线程必定是一个环路,死循环造成永久阻塞
  1. 如何避免死锁?(破坏四个必要条件之一即可)
  • 破坏互斥条件:多个线程共享资源,一般不可行
  • 破坏请求和保持条件:一次申请所有资源
  • 破坏不可剥夺条件:占有部分资源的线程尝试申请其他资源时,如果申请不到,就释放已占有的资源
  • 破环循环等待:按序申请资源
  • 线程池
  1. 什么是线程池?
    是将多个线程预先存储在一个“池子”(工作线程)里面,当有任务出现时,可以避免重新创建和销毁线程所带来的性能开销,只需要从池子里面,取出相应线程执行,完成对应任务即可。
  2. 线程池一般的四个组成部分
    • 线程管理器:用于创建、销毁线程池,添加新的任务
    • 工作线程:线程池中的线程,可循环执行任务,没有任务时,处于等待状态
    • 任务队列:用于存放没有处理的任务,一种缓存机制
    • 任务接口:每个任务必须实现的接口,供工作线程,任务调度的执行,主要规定了任务的开始、结束工作,以及任务状态
  3. 创建线程池的方法?
    • Executors.newFixedThreadPool():创建固定大小的线程池,可以控制并发的线程数,超出的线程会在队列中等待
    • Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数,超出,当前当前所需处理的任务,缓存一段时间后进行回收,若线程数不够,则创建新的
    • Executors.newSingleThreadExecutor:创建一个单个线程数的线程池,它可以保证先进先出的执行顺序
    • Executors.newScheduledThreadPool:创建一个可以执行延迟队列的线程池
    • Executors.newSingleThreadScheduledExecutor:创建一个单线程可以执行延迟任务的线程池
    • Executors.newWorkStealingPool:创建一个抢占式执行的线程池,任务的执行顺序不确定**(1.8添加)**
    • ThreadPoolExecutor: 最原始的创建线程池的方式,有7个参数

创建线程池的类

  • 创建线程池的7个参数:
  1. corePoolSize:核心线程数
  2. maximumPoolSize:最大线程数,当阻塞队列满之后,所允许创建的最大线程数
  3. keepAliveTime:最大线程数可以存活的时间,当线程中没有任务执行时,最大线程允许存活的时间,最终只保留核心线程数量的线程
  4. unit:和参数3一起使用,最大线程数所允许存活时间的单位(天、时、分、秒、毫秒、微秒、纳秒)
  5. workQueue:阻塞队列,用于存储线程池中用于等待执行的任务
  6. threadFactory:线程工厂,主要用来创建线程
  7. handler:拒绝策略,线程池拒绝执行任务时,所进行的操作
  • 线程池的拒绝策略

AbortPolicy:拒绝并抛出异常。
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
iscardPolicy:忽略并抛弃当前任务。
默认使用AbortPolicy

  • 线程池执行流程:
    线程池执行流程
  • 线程池中的核心线程是如何被回收的?

核心线程的回收,是由allowCoreThreadTimeOut参数控制的,默认为false,若开启,则此时线程池中的线程不管是否是核心线程,只要超过keepAliaveTime,都会被回收
注意:这样会违背线程池减少线程创建开销的目的,所以默认false

  • 线程调度算法和策略?
  • 读写锁
  • AQS
  • ThreadLocal
  • 锁消除和锁粗化
  • 什么是自旋?
  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

何一平?

你的收获就是我学习的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值