1.说出你对面向对象的理解
答题要点
- “万物皆对象”的编程思想
- 面向对象三大特性,对应的作用
对面向对象的理解:面向对象是向现实世界模型的自然延伸,这是一种“万物皆对象”的编程思想。在现实生活中的任何物体都可以归为一类事物,而每一个个体都是一类事物的实例。面向对象的编程是以对象为中心,以消息为驱动,所以程序=对象+消息。
面向对象有三大特性,封装、继承和多态。
-
封装就是将一类事物的属性和行为抽象成一个类,使其属性私有化,行为公开化,提高了数据的隐秘性的同时,使代码模块化。这样做使得代码的
复用性
更高。 -
继承则是进一步将一类事物共有的属性和行为抽象成一个父类,而每一个子类是一个特殊的父类–有父类的行为和属性,也有自己特有的行为和属性。这样做扩展了已存在的代码块,进一步提高了代码的
复用性
。 -
如果说封装和继承是为了使代码重用,那么多态则是为了实现接口重用。多态的一大作用就是为了解耦–为了解除父子类继承的耦合度。如果说继承中父子类的关系式IS-A的关系,那么接口和实现类之之间的关系式HAS-A。简单来说,多态就是允许父类引用(或接口)指向子类(或实现类)对象。很多的设计模式都是基于面向对象的多态性设计的。
总结:如果说封装和继承是面向对象的基础,那么多态则是面向对象最精髓的理论。掌握多态必先了解接口,只有充分理解接口才能更好的应用多态。
2.JVM的内存区及其GC算法
2.1JVM的内存区
JVM在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同:
JDK 1.8 之前:
JDK 1.8 :
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
- 减少内存溢出几率:整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
当你元空间溢出时会得到如下错误:
java.lang.OutOfMemoryError: MetaSpace
你可以使用 -XX:MaxMetaspaceSize
标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize
调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
-
加载更多的类:元空间里面存放的是类的元数据,这样加载多少类的元数据就不由
MaxPermSize
控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。 -
在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。
2.1.1线程共享的:
2.1.1.1堆
所有线程共享,JVM启动时创建,存放对象实例及数组,所有new出来的对象都存储在该区域,堆区GC最频繁。
2.1.1.2方法区
存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也被称为永久代,方法区上执行GC的情况很少,执行GC条件苛刻,主要针对常量池的回收和已加载类的卸载。
2.1.1.2.1运行时常量池
方法区的一部分,用于存储编译器生成的常量和引用。
2.1.1.3直接内存
不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
2.1.2线程私有的
2.1.2.1 程序计数器
较小的内存空间,可看作是当前线程所执行的字节码的行号指示器,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储。
作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
2.1.2.2 虚拟机栈
线程私有,生命周期和线程相同,每个方法被执行时产生一个栈帧(Statck Frame),栈帧用于存储局部变量表、操作数栈、动态链接、方法出口信息,当方法被调用时,栈帧入栈,当方法调用结束时,栈帧出栈。
局部变量表中存储着方法相关的局部变量,包括各种基本数据类型
及对象的引用
等,因此他有个特点:内存空间可以在编译期间就确定,运行时不再改变。
虚拟机栈两种异常类型:StackOverFlowError(栈溢出)和OutOfMemoryError(内存溢出)。如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StackOverFlowError;不过大多数虚拟机都允许动态扩展虚拟机栈的大小,所以线程可以一直申请栈,直到内存不足时,抛出OutOfMemoryError。
扩展:那么方法/函数如何调用?
Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。
Java 方法有两种返回方式:
- return 语句。
- 抛出异常。
不管哪种返回方式都会导致栈帧被弹出。
2.1.2.3 本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误
2.2、GC算法
Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
堆空间的基本结构:
上图所示的 Eden 区、From Survivor0(“From”) 区、To Survivor1(“To”) 区都属于新生代,Old Memory 区属于老年代。
2.2.1堆内存常见分配策略
对象优先在eden区分配
大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
长期存活的对象将进入老年代
当Eden空间满了之后,会触发一个叫做Minor GC(就是一个发生在年轻代的GC)的操作,存活下来的对象移动到Survivor0区。Survivor0区满后触发 Minor GC,就会将存活对象移动到Survivor1区,此时还会把from和to两个指针交换,这样保证了一段时间内总有一个survivor区为空且to所指向的survivor区为空。经过多次的 Minor GC后仍然存活的对象(这里的存活判断是15次,对应到虚拟机参数为 -XX:MaxTenuringThreshold 。为什么是15,因为HotSpot会在对象投中的标记字段里记录年龄,分配到的空间仅有4位,所以最多只能记录到15)会移动到老年代。老年代是存储长期存活的对象的,占满时就会触发我们最常听说的Full GC,期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。
而且当老年区执行了full gc之后仍然无法进行对象保存的操作,就会产生OOM,这时候就是虚拟机中的堆内存不足,原因可能会是堆内存设置的大小过小,这个可以通过参数-Xms、-Xms来调整。也可能是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们。
Minor Gc和Full GC 有什么不同呢?
- 新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
- 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。
2.2.2 对象死亡
2.2.2.1 如何判断对象死亡
- 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。 - 可达性分析算法:这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
可作为GC Roots的对象包括下面几种:(了解)
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
2.2.2.2 引用
1.强引用(StrongReference)
内存不足也不会被GC(必不可少)
2.软引用(SoftReference)
内存不足,GC回收(可有可无)
3.弱引用(WeakReference)
所在内存区域只要触发GC,就会被回收,不管内存是否充足
4.虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
2.2.2.3 如何判断一个常量是废弃常量?
运行时常量
池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?
假如在常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池。
2.2.2.4 如何判断一个类是无用的类
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :
- 该类所有的
实例都已经被回收
,也就是 Java 堆中不存在该类的任何实例。 - 加载该类的
ClassLoader
已经被回收。 - 该类对应的 java.lang.Class 对象没有在任何地方被引用,
无法在任何地方通过反射访问该类
的方法。
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
2.2.3 垃圾收集算法
2.2.3.1 标记-清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。
其实它就是把已死亡的对象标记为空闲内存,然后记录在一个空闲列表中,当我们需要new一个对象时,内存管理模块会从空闲列表中寻找空闲的内存来分给新的对象。
这种垃圾收集算法会带来两个明显的问题:
- 效率问题
- 空间问题(标记清除后会产生大量不连续的碎片,导致了如果我们需要使用到较大的内存块时,无法分配到足够的连续内存)
2.2.3.2 复制算法
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块
,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
2.2.3.3 标记-整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
2.2.3.4 分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
3.集合框架下的各种接口和实现类有哪些,分别有啥特点
3.1 集合概述
3.1.1 集合概览
从下图可以看出,在 Java 中除了以 Map
结尾的类之外, 其他类都实现了 Collection
接口。
并且,以 Map
结尾的类都实现了 Map
接口。
3.1.2说说 List,Set,Map 三者的区别?
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。Set
(注重独一无二的性质): 存储的元素是无序的、不可重复的。Map
(用 Key 来搜索的专家): 使用键值对(kye-value)存储,类似于数学上的函数 y=f(x),“x”代表 key,"y"代表 value,Key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
3.1.3 集合框架底层数据结构总结
3.1.4 如何选用集合?
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 Map
接口下的集合,需要排序时选择 TreeMap
,不需要排序时就选择 HashMap
,需要保证线程安全就选用 ConcurrentHashMap
。
当我们只需要存放元素值时,就选择实现Collection
接口的集合,需要保证元素唯一时选择实现 Set
接口的集合比如 TreeSet
或 HashSet
,不需要就选择实现 List
接口的比如 ArrayList
或 LinkedList
,然后再根据实现这些接口的集合的特点来选用。
3.1.5为什么要使用集合?
当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端,因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。
数组的缺点是一旦声明之后,长度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。
但是集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据
4.String类有啥特点,有哪些常用的API
- 特点
- 类被final修饰,不可以被继承。
- 字符串的值在创建后不能被更改。
- 线程安全,可以被共享。
- String类对象的相等判断使用equals()方法完成,“==”实现的是地址数值的比较
- 常用api
equals:字符串是否相同
equalsIgnoreCase:忽略大小写后字符串是否相同
compareTo:根据字符串中每个字符的Unicode编码进行比较
compareToIgnoreCase:根据字符串中每个字符的Unicode编码进行忽略大小写比较
indexOf:目标字符或字符串在源字符串中位置下标
lastIndexOf:目标字符或字符串在源字符串中最后一次出现的位置下标
valueOf:其他类型转字符串
charAt:获取指定下标位置的字符
codePointAt:指定下标的字符的Unicode编码
concat:追加字符串到当前字符串
isEmpty:字符串长度是否为0
contains:是否包含目标字符串
startsWith:是否以目标字符串开头
endsWith:是否以目标字符串结束
format:格式化字符串
getBytes:获取字符串的字节数组
getChars:获取字符串的指定长度字符数组
toCharArray:获取字符串的字符数组
join:以某字符串,连接某字符串数组
length:字符串字符数
matches:字符串是否匹配正则表达式
replace:字符串替换
replaceAll:带正则字符串替换
replaceFirst:替换第一个出现的目标字符串
split:以某正则表达式分割字符串
substring:截取字符串
toLowerCase:字符串转小写
toUpperCase:字符串转大写
trim:去字符串首尾空格
5…String、StringBuilder和StringBuffer的区别?
相同点:
都可以储存和操作字符串
都使用 final 修饰,不能被继承
提供的 API 相似
区别:
String 是只读字符串,String 对象内容是不能被改变的
StringBuffer 和 StringBuilder 的字符串对象可以对字符串内容进行修改,在修改后的内存地址不会发生改变
StringBuilder 线程不安全;StringBuffer 线程安全
方法体内没有对字符串的并发操作,且存在大量字符串拼接操作,建议使用 StringBuilder,效率较高。
6.线程创建的几种方式,线程阻塞的API有哪些及其之间的区别?对线程池的理解?
6.1 线程创建的3种方式
- 重写Thread类的run方法
- 实现Runable接口,重写run方法
- 实现Callable接口,使用FutureTask类创建线程
- 使用线程池创建、启动线程
6.2 线程阻塞的API有哪些及其之间的区别?
- sleep
sleep()方法定义在Thread类中,sleep()的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待CPU的调度执行。不会释放锁 - wait
wait()是Object方法,让当前线程由“运行状态”进入到“等待(阻塞)状态”,会释放对象的同步锁。 - yield
yield()是Thread方法,在Java线程中,**yield()**方法的作用是让步,它能让当前线程由“运行状态”进入到“就绪状态”
,从而让其他具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其他具有相同优先级的线程就一定能获得执行权;也有可能是当前线程有进入到“运行状态”继续运行。不会释放对象的同步锁
yield()和wait()的比较
- wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而yield()是让线程由“运行状态”进入到“就绪状态”。
- wait()是会让线程释放它所持有的对象的同步锁,而yield()方法不会释放对象的同步锁。
- yield()是Thread方法,wait()是Object方法。
sleep()和wait()的比较
- wait()会释放对象的同步锁,而sleep()则不会释放锁。
- sleep()是Thread方法,wait()是Object方法。
7.抽象类和接口的区别?有了抽象类为啥还要接口?
7.1抽象类和接口的区别?
- 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
- 接口中除了 static、final 变量,不能有其他变量,而抽象类中则不一定。
- 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。
- 接口方法默认修饰符是 public,抽象方法可以有 public、protected 和 default 这些修饰符(抽象方法就是为了被重写所以不能使用 private 关键字修饰!)。
- 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
7.2 有了抽象类为啥还要接口?
在Java中,使用抽象类表示通用属性存在这样一个问题:每个类只能继承一个父类。假设一个类已经继承一个父类,它就不能再继承另一个类了,但每个类可以实现多个接口,总的来说,Java不支持多继承所导致上述问题
8.冒泡排序,选择排序,快速排序(了解)
- 冒泡排序
package day0515;
public class demo_sort {
public static void main(String[] args) {
//冒泡排序算法
int[] numbers=new int[]{1,5,8,2,3,9,4};
for(int i=0;i<numbers.length-1;i++)
{
for(int j=0;j<numbers.length-1-i;j++)
{
if(numbers[j]>numbers[j+1])
{
int temp=numbers[j];
numbers[j]=numbers[j+1];
numbers[j+1]=temp;
}
}
}
System.out.println("从小到大排序后的结果是:");
for(i=0;i<numbers.length;i++)
System.out.print(numbers[i]+" ");
}
}
- 比较相邻两个数据如果。第一个比第二个大,就交换两个数
- 对每一个相邻的数做同样1的工作,这样从开始一队到结尾一队在最后的数就是最大的数。
- 针对所有元素上面的操作,除了最后一个。
- 重复1~3步骤,知道顺序完成。
- 选择排序
- 在一个长度为 N 的无序数组中,第一次遍历 n-1 个数找到最小的和第一个数交换。
- 第二次从下一个数开始遍历 n-2 个数,找到最小的数和第二个数交换。
- 重复以上操作直到第 n-1 次遍历最小的数和第 n-1 个数交换,排序完成。
public static void main(String[] args) {
int[] arr = {111, 3, 5, 52, 74, 312, 75, 3, 764, 3, 2111, 7, 31};
for(int i=0; i<arr.length; i++) {
//最小数的索引
int minIndex = i;
for(int j=i; j<arr.length; j++) {
//找到最小数,并记录最小数的索引
if(arr[j] < arr[minIndex]) {
minIndex = j;
}
}
//交换符合条件的数
int tmp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = tmp;
}
System.out.println("排序后的数组:"+ selectionSort(arr));
}
- 快速排序
- 从数列中挑出一个元素作为基准。
- 重新排列数列,把所有的比基准小的放在基准前面,反之放在后面(一样大可任意一边)完成后基准处在分区的中间位置。
- 通过递归调用把小于基准元素和大雨基准元素的子序列进行排序。
9.什么是死锁?如何避免死锁
9.1线程死锁
线程死锁是指由于两个或者多个线程互相持有所需要的资源,导致这些线程一直处于等待其他线程释放资源的状态,无法继续执行,如果线程都不主动释放所占有的资源,将产生死锁。
当线程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
产生死锁必须具备以下四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
9.2如何避免线程死锁?
为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了
- 破坏互斥条件 :资源互斥是资源使用的固有特性是无法改变的。
- 破坏请求与保持条件 :方法1:
静态分配
即每个进程在开始执行时就申请他所需要的全部资源。方法2:动态分配
即每个进程在申请所需要的资源时他本身不占用系统资源。 - 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。