Java基础
Java中的基本类型都有哪些
- 数字类型
- 4 种整数型:byte、short、int、long,int:4个字节
- 2 种浮点型:float、double
- 字符类型
- char:2个字节
- 布尔型
- boolean
- 包装类型
- 常量池技术
- 自动装箱与拆箱
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
& 和&& 的区别
&:按位与 &&:与
面向对象三大特征
- 封装
- 继承
- 多态
值传递&引用传递
- [[Java传参机制]]
- [[Java浅拷贝和深拷贝]]
Object
hashCode() 与 equals()与== 这三者的区别
- == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。
- equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1,类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“ == ”比较这两个对象。
- 情况2,类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的内容相等,则返回true(即,认为这两个对象相等)。
- hashCode() 的作用是获取哈希码,hashCode() 在散列表中才有用,在其它情况下没用。
- 为什么重写 equals() 时必须重写 hashCode() 方法?
- 因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
- 如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
- 重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题?
- hashCode() 在散列表中才有用,在其它情况下没用。
- 散列表,指的是:Java集合中本质是散列表的类,如HashMap,Hashtable,HashSet。
重载和重写
- 重载:同名方法不同参数数量
- 重写:子类重写父类的同名方法,目的:提高扩展性
泛型
- 泛型中extends和super的区别
String、StringBuffer、StringBuilder
- String 为什么是不可变的?
- 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
- String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
- 线程安全性
- String 线程安全
- StringBufffer线程安全
- StringBuilder非线程安全
- 性能
- 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
- 操作少量的数据: 适用 String 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
参数
- 可变长参数
泛型
- 泛型类、泛型接口、泛型方法。
反射
- 优点
- 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
- 缺点
- 让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
- 代码可读性
- 应用场景
- 框架中以及动态代理
注解
- 编译期直接扫描
- 运行期通过反射处理
- 自定义注解
异常
Throwable
- Error
- Exception
try-catch-finally
- finally 中的代码一定会执行吗
- 不一定的!在某些情况下,finally 中的代码不会被执行。 就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。
- finally 中的代码一定会执行吗
OOM
- jstack
- heap
- dump
I/O
序列化
- 将数据结构或对象转换成二进制字节流的过程
反序列化
- 将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
Java 序列化中如果有些字段不想进行序列化,怎么办?
- 对于不想进行序列化的变量,使用 transient 关键字修饰。
字节流,为什么还要有字符流?
- 不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
- 字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
- 不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
Stream
- 函数式接口
集合类
List
- ArrayList
- LinkedLst
HashTable
- hashmap和hashtable的区别
哈希计算方法不同
- HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取模。
- Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模。
- 负载因子:0.75
- 尽量不使用,推荐使用ConcurrentHashMap
Map
- HashMap
- 重写equals不重写hashCode会怎样?
- HashMap如何实现线程安全?
- HashMap本身底层是非线程安全的,在多线程条件下,容易导致死循环,具体表现为CPU使用率100%。实现线程安全方法
- 使用 java.util.Hashtable 类,此类是线程安全的。【通过方法上添加synchronized实现线程安全,建议使用ConcurrentHashMap】
- 使用 java.util.concurrent.ConcurrentHashMap,此类是线程安全的。【put元素时使用synchronized关键字修饰】
- 使用 java.util.Collections.synchronizedMap() 方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作。【方法返回SynchronizedMap】
- 自己在程序的关键方法或者代码段加锁,保证安全性,当然这是严重的不推荐。
- HashMap本身底层是非线程安全的,在多线程条件下,容易导致死循环,具体表现为CPU使用率100%。实现线程安全方法
- ConcurrentHashMap
- ConcurrentHashMap与HashMap的区别
- 基本概念不同;ConcurrentHashMap是一个支持高并发更新与查询的哈希表;而HashMap是基于哈希表的Map接口的实现。
- 底层数据结构不同;HashMap的底层数据结构主要是:数组+链表,确切的说是由链表为元素的数组。ConcurrentHashMap的底层数据结构是:Segments数组+HashEntry数组+链表。ConcurrentHashMap是线程安全的数组,它采用分段锁保证安全性。容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。而HashMap不是线程安全,没有锁机制,在多线程环境下会导致数据覆盖之类的问题,所以在多线程中使用HashMap是会抛出异常的。
- 线程安全属性不同;
- 对整个桶数组的处理方式不同。
- 谈谈ConcurrentHashMap的扩容机制
- ConcurrentHashMap与HashMap的区别
Java 并发
- 线程的生命周期?线程有几种状态
- Java里面synchronized锁的底层实现原理?
- 对象锁[[synchronized 实现原理]]
- 介绍一下悲观锁、乐观锁、自旋锁,适用场景?
- 对于传统的重量级锁(synchronized)提供一系列优化操作——自旋与自适应自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)。
- sleep()、 wait()、 join()、 yielo()红超间的的区别
- 对线程安全的理解
- 线程安全可以理解为内存安全,堆是内存共享,可以被所有的内存访问。 当多个线程访问同一个对象时,如果不进行额外的同步控制或者其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。
- 为什么出现现场不安全?
- 其中一个线程修改了堆内存区域的对象属性,或者某个值,导致其他线程拿到错误的对象属性
- Thread和Runable的区别
- 对守护线程的理解
- ThreadLocal的底层原理
- 每个
Thread
都维护自己的一个ThreadLocalMap
,所以是线程隔离的。
- 每个
- 并发、并行、串行之间的区别
- Java死锁如何避免?
- 死锁的四个必要不充分条件
- 禁止抢占
- 持有和等待
- 互斥
- 循环等待
- 死锁的四个必要不充分条件
- 如何理解volatile关键字
- 为什么用线程池?解释下线程池参数?(7个线程池参数)
- corePoolSize:核心线程数。
- maximumPoolSize:最大线程数。
- keepAliveTime:空闲线程存活时间。
- TimeUnit:时间单位。
- BlockingQueue:线程池任务队列。
- ThreadFactory:创建线程的工厂。
- RejectedExecutionHandler:拒绝策略。(4个拒绝策略)
- AbortPolicy:拒绝并抛出异常。
- CallerRunsPolicy:使用当前调用的线程来执行此任务。
- DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
- DiscardPolicy:忽略并抛弃当前任务。
- 最大任务数量=maximumPoolSize+BlockingQueue
- 线程池的底层工作原理
- 线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?
- 线程池中线程复用原理
- 线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。
- ReentrantLock中的公平锁和非公平锁的底层实现
- ReentrantLock中tryLocko和lock0方法的区别
- CountDownLatch和Semaphore的区别和底层原理
- Sychronized的偏向锁、轻量级锁、重量级锁
- Sychronized和ReentrantLock的区别
- 谈谈你对AQS的理解,AQS如何实现可重入锁?
JVM
- 一个JVM进程的内存布局空间?
- 线程共享内存区
- Java堆:
- 虚拟机启动时创建,用来存放对象实例
- 新生代 1.
- 老年代
- 方法区
- 永久代(元空间),大多是静态方法
- Java堆:
- 线程私有内存区
- Java虚拟机栈
- Java虚拟机栈也是线程私有的,虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的时候都会创建一个栈帧,用于存放局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直到执行完成的过程都对应着一个栈帧在虚拟机中的入栈到出栈的过程。我们平时把内存分为堆内存和栈内存,其中的栈内存就指的是虚拟机栈的局部变量表部分。局部变量表存放了编译期可以知道的基本数据类型(boolean、byte、char、short、int、float、long、double),对象引用(可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置),和返回后所指向的字节码的地址。其中64 位长度的long 和double 类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。当递归层次太深时,会引发java.lang.StackOverflowError,这是虚拟机栈抛出的异常。
- 本地方法栈
- native方法栈
- 程序计数器
- Java虚拟机栈
- 线程共享内存区
- 函数返回的时候,return语句的执行过程?
- Java中有哪些类加载器
- 引导(Bootstrap)类加载器
- 扩展(Extensions)类加载器
- Apps类加载器(也称系统类加载器)
- 自定义类加载器
- 类加载器之间的关系
- 启动类加载器,由C++实现,没有父类。
- 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
- 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
- 自定义类加载器,父类加载器肯定为AppClassLoader。
- 说说类加载器双亲委派模型
- 其工作原理的是:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成。
- 作用:
- 避免类的重复加载;
- 防止核心API库被随意篡改
- GC如何判断对象可以被回收
- 引用计数法
- 优点:引用计数收集器执行简单,判定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。
- 缺点:难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以Java语言并没有选择这种算法进行垃圾回收。
- 可达性分析法
- 可达性分析是用来判断对象是否存活,通过"GC Roots"作为起点,从这个节点往下搜索,如果有有引用,则这个对象是存活的,如果没有则判定可回收的对象。
- 引用计数法
- 垃圾回收算法
- 标记-清除算法
- 标记-清除算法对根集合进行扫描,对存活的对象进行标记。标记完成后,再对整个空间内未被标记的对象扫描,进行回收。
- 优点:实现简单,不需要进行对象进行移动。
- 缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。
- 复制算法
- 它将内存区域划分成相同的两个内存块。每次仅使用一半的空间,
JVM
生成的新对象放在一半空间中。当一半空间用完时进行GC
,把可到达对象复制到另一半空间,然后把使用过的内存空间一次清理掉。 - 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
- 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
- 它将内存区域划分成相同的两个内存块。每次仅使用一半的空间,
- 标记-整理算法
- 标记-整理算法 采用和 标记-清除算法 一样的方式进行对象的标记,但后续不直接对可回收对象进行清理,而是将所有的存活对象往一端空闲空间移动,然后清理掉端边界以外的内存空间。
- 优点:解决了标记-清理算法存在的内存碎片问题。
- 缺点:仍需要进行局部对象移动,一定程度上降低了效率。
- 分代收集算法
- 总结
- JDK8堆内存一般是划分为年轻代和老年代,不同年代 根据自身特性采用不同的垃圾收集算法。
- 对于新生代,每次GC时都有大量的对象死亡,只有少量对象存活。考虑到复制成本低,适合采用复制算法。因此有了From Survivor和To Survivor区域。
- 对于老年代,因为对象存活率高,没有额外的内存空间对它进行担保。因而适合采用标记-清理算法和标记-整理算法进行回收。
- 标记-清除算法
- JVM中哪些是线程共享区
- 堆区和⽅法区是所有线程共享的,栈、本地⽅法栈、程序计数器是每个线程独有的
- 堆: 不用多说了,放对象的地方
- 方法区: 类定义的成员变量丶常量丶静态变量丶方法都在这里
- 栈: 程序运行才有的,会把运行时的方法压入栈,里面有局部变量等东西
- 本地方法栈: 操作系统方法
- 程序计数器: 标记代码走到哪里了
- 堆区和⽅法区是所有线程共享的,栈、本地⽅法栈、程序计数器是每个线程独有的
- 你们项目如何排查JVM问题
- 一个对象从加载到JVM,再到被GC清除,都经历了什么过程?
- 什么是三色标记?
- CMS怎么进行垃圾回收的?
- 官方名称:最大-并发-标记-清除-垃圾收集器
- 作用范围:老年代
- 算法:并发标记清除算法。
- 阶段
- 初始标记(CMS initial mark)
- STW
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
- 初始标记(CMS initial mark)
- JVM参数有哪些? 1. -Xms:设置初始分配大小,默认为物理内存的1/64 2. -Xmx:最大分配内存,默认为物理内存的1/4 3. -XX:+PrintGCDetails:输出详细的GC处理日志 4. -XX:+PrintGCTimeStamps:输出GC的时间戳信息 5. -XX:+PrintGCDateStamps:输出GC的时间戳信息(以日期的形式) 6. -XX:+PrintHeapAtGC:在GC进行处理的前后打印堆内存信息 7. -Xloggc:(SavePath):设置日志信息保存文件 8. 在堆内存的调整策略中,基本上只要调整两个参数:-Xms和-Xmx
- GC具体过程
- 当现在有一个新的对象产生,JvM需要为该对象进行内存空间的申请
- 先判断Eden区是否有内存空间,如果有,直接将新对象保存在Eden区
- 如果Eden区的内存空间不足,会自动执行一个MinorGC操作,将Eden区的无用内存空间进行清理
- 清理Eden区之后继续判断Eden区内存空间情况,如果充足,则将新对象直接保存在Eden区
- 如果执行了Minor GC之后发现Eden区的内存依然不足,那就判断存活区的内存空间,并将Eden区的部 分活跃对象保存在存活区
- 活跃对象迁移到存活区后,继续判断Eden区内存空间情况,如果充足,则将新对象直接保存在Eden区
- 如果存活区也没有空间了,则继续判断老年区,如果老年区充足,则将存活区的部分活跃对象保存在老 年区
- 存活区的活跃对象迁移到老年区后,则将Eden区的部分活跃对象保存在存活区
- 活跃对象迁移到存活区后,继续判断Eden区内存空间情况,如果充足,则将新对象直接保存在Eden区
- 如果老年区也满了,这时候产生Major GC (Ful GC)进行老年区的内存清理
- 如果老年区执行了 Major GC之后发现无法进行对象保存,会产生OutofMemoryError异常
- GC的区别
1、部分收集(Partial GC):只针对部分区域进行垃圾收集。其中又分为:
1.1、新生代收集(Minor GC/Young GC):只针对新生代的垃圾收集。具体点的是Eden区满时触发GC。
Survivor满不会触发Minor GC 。
1.2、老年代收集(Major GC/Old GC):只针对 老年代的垃圾收集。
目前,只有CMS收集器会有单独收集老年代的行为。
注意,很多时候,Major GC 会和Full GC混淆使用,需要具体分辨是老年代的回收还是整堆回收。
1.3、混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。
目前只有G1收集器会有这种行为。
2、整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。