拓展面试

JDK基础面试题

问:String是最基本的数据类型吗?
参考答案:
  不是,Java基本数据类型只有8种,byte、int、char、long、float、double、boolean和short。

问:Collection 和 Collections的区别?
参考答案:

​ Collection是集合类的上级接口,继承与他的接口主要有Set 和List.
  Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

问:String对象的intern()是指什么?
参考答案:
intern()方法会首先从常量池中查找是否存在该常量值,如果常量池中不存在则现在常量池中创建,如果已经存在则直接返回. 比如 String s1=“aa”; String s2=s1.intern(); System.out.print(s1==s2);//返回true

问:final、finally、finalize的区别是什么?
参考答案:
  final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。
finally是异常处理语句结构的一部分,表示总是执行。
finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。

问:error和exception有什么区别?
参考答案:
  error通常表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出、不可能指望程序能处理这样的情况;
exception通常表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况;

问:运行时异常与受检异常有何异同?
参考答案:
运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生;
受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。Java编译器要求方法必须声明抛出可能发生的受检异常,但是并不要求必须声明抛出未被捕获的运行时异常。

问:列出一些你常见的运行时异常?
参考答案:

ArithmeticException(算术异常)

ClassCastException (类转换异常)

IllegalArgumentException (非法参数异常)

IndexOutOfBoundsException (下标越界异常)

NullPointerException (空指针异常)

SecurityException (安全异常)

问:static都有哪些用法?
参考答案:

静态变量
静态方法
静态块,多用于初始化
静态内部类.
静态导向,即import static.import static是在JDK 1.5之后引入的新特性,可以用来指定导入某个类中的静态资源,并且不需要使用类名.资源名,可以直接使用资源名,比如:

import static java.lang.Math.sin;
public class Test{
public static void main(String[] args){
//System.out.println(Math.sin(20));传统做法
System.out.println(sin(20)); // 静态导包
}
}

Java面向对象面试题

问:抽象的(abstract)方法是否可同时是静态的(static),是否可同时是本地方法(native),是否可同时被synchronized修饰?为什么?
参考答案:
都不能。
抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的;
本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的;
synchronized和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的;

问:java.lang.Class类的newInstance方法和 java.lang.reflect.Constructor类的newInstance方法有什么区别?
参考答案:
• Class类的newInstance只能触发无参数的构造方法创建对象,而构造器类的newInstance能触发有参数或者任意参数的构造方法来创建对象。
• Class类的newInstance需要其构造方法是public的或者对调用方法可见的,而构造器类的newInstance可以在特定环境下调用私有构造方法来创建对象。
• Class类的newInstance抛出类构造函数的异常,而构造器类的newInstance包装了一个InvocationTargetException异常。
说明:Class类本质上调用了反射包Constructor中无参数的newInstance方法,捕获了InvocationTargetException,将构造器本身的异常抛出

问:Java 中的final关键字有哪些用法?
参考答案:
(1)修饰类:表示该类不能被继承;
(2)修饰方法:表示方法不能被重写;(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)

问:关键字swtich 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上?
参考答案:
在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。从Java 5开始,Java中引入了枚举类型,expr也可以是enum类型,从Java 7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。

问:面向对象的特征有哪些?
参考答案:
  1.抽象:
  抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。抽象包括两个方面,一是过程抽象,二是数据抽象。
  2.继承:
  继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。
  3.封装:
  封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。
  4. 多态性:
  多态性是指允许不同类的对象对同一消息作出响应。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。

问:代码中如何实现多态?
参考答案:
 实现多态主要有以下三种方式:

接口实现
继承父类重写方法
同一类中进行方法重载

问:overload和overide的区别?
参考答案:

Override 可以翻译为覆盖,从字面就可以知道,它是覆盖了一个方法并且对其重写,以求达到不同的作用。对我们来说最熟悉的覆盖就是对接口方法的实现,在接口中一般只是 对方法进行了声明,而我们在实现时,就需要实现接口声明的所有方法。除了这个典型的用法以外,我们在继承中也可能会在子类覆盖父类中的方法。在覆盖要注意 以下的几点:
覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配,才能达到覆盖的效果;
覆盖的方法的返回值必须和被覆盖的方法的返回值一致;
覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;
被覆盖的方法不能为 private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。
Overload 可以翻译为重载,它是指我们可以定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法,然后再调用时,VM就会根据不同的参数样式,来选择合适的方法执行。在使用重载要注意以下的几点: 5. 在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序(当然,同一方法内的几个参数类型必须不一样,例如可以是 fun(int,float),但是不能为 fun(int,int)); 5. 不能通过访问权限、返回类型、抛出的异常进行重载; 5. 方法的异常类型和数目不会对重载造成影响; 5. 对于继承来说,如果某一方法在父类中是访问权限是 private,那么就不能在子类对其进行重载,如果定义的话,也只是定义了一个新方法,而不会达到重载的效果。
总之,重写(Override) 是父类与子类之间多态性的一种表现,重载 (Overload) 是一个类中多态性的一种表现。很重要的一点就是,Overloaded 的方法是可以改变返回值的类型。
另外:在Java语言规范中,一个方法的特征仅包括方法的名字,参数的数目和种类,而不包括方法的返回类型,参数的名字以及所抛出来的异常。在Java编译 器检查方法的重载时,会根据这些条件判断两个方法是否是重载方法。但在Java编译器检查方法的置换时,则会进一步检查两个方法(分处超类型和子类型)的 返还类型和抛出的异常是否相同。

问:使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变?
参考答案:
使用 final 关键字修饰一个变量时,是指引用变量不能变,引用变量所指向的对象中的内容还是可以改 变的。例如,对于如下语句:
final StringBuffer a=new StringBuffer(“immutable”);
执行如下语句将报告编译期错误:
a=new StringBuffer("");
但是,执行如下语句则可以通过编译:
a.append(" broken!");
有人在定义方法的参数时,可能想采用如下形式来阻止方法内部修改传进来的参数对象:
public void method(final StringBuffer param){
}
实际上,这是办不到的,在该方法内部仍然可以增加如下代码来修改参数对象:
param.append(“a”);

问:静态变量和实例变量的区别?
参考答案:
在语法定义上的区别:静态变量前要加 static 关键字,而实例变量前则不加。
在程序运行时的区别:实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。 总之,实例变量必须创建对象后才可以通过这个对象来使用,静态变量则可以直接使用类名来引用。
例如,对于下面的程序,无论创建多少个实例对象,永远都只分配了一个 staticVar 变量,并且每创建一个实例对象,这个 staticVar就会加1;但是,每创建一个实例对象,就会分配一个 instanceVar,即可能分配多个 instanceVar,并且每个 instanceVar 的值都只自加了 1 次。

public class VariantTest{

public static int staticVar = 0;
    
public int instanceVar = 0;
    
public VariantTest(){
	staticVar++;
	instanceVar++;
	System.out.println("staticVar=" + staticVar + ",instanceVar=" + instanceVar);
}
}

问:&和&&的区别?
参考答案:
&运算符有两种用法:(1)按位与;(2)逻辑与。
&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是null而且不是空字符串,应当写为:username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的equals比较,否则会产生NullPointerException异常。注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。

问:父类的静态方法能否被子类重写?
参考答案:
不能。重写只适用于实例方法,不能用于静态方法,而子类当中含有和父类相同签名的静态方法,我们一般称之为隐藏,调用的方法为定义的类所有的静态方法。

问:什么是不可变对象?
参考答案:
不可变对象指对象一旦被创建,状态就不能再改变。任何修改都会创建一个新的对象,如 String、Integer及其它包装类。

问:能否创建一个包含可变对象的不可变对象?
参考答案:
可以,不过需要谨慎一点,不要共享可变对象的引用就可以了,如果需要变化时,就返回原对象的一个拷贝。
最常见的例子就是对象中包含一个String对象的引用.

Java进阶面试题

问:为什么反射的性能较差?有没有什么方法可以让他变快?
参考答案: java反射要解析字节码,将内存中的对象进行解析,包括了一些动态类型,JVM难以优化,而且在调用时还需要拼接参数,执行步骤也更多。因此,反射操作的效率要更低

​ 常用的改进性能方法主要有:

m.setAccessible(true);
由于JDK的安全检查耗时较多.所以通过setAccessible(true)的方式关闭安全检查就可以达到提升反射速度的目的;

用缓存将反射得到的元数据保存起来;
利用一些高性能的反射库,如ReflectASM ReflectASM 使用字节码生成的方式实现了更为高效的反射机制。执行时会生成一个存取类来 set/get 字段,访问方法或创建实例。一看到 ASM 就能领悟到 ReflectASM 会用字节码生成的方式,而不是依赖于 Java 本身的反射机制来实现的,所以它更快,并且避免了访问原始类型因自动装箱而产生的问题。
问:java当中的四种引用分别指什么?
参考答案:

强引用,软引用,弱引用,虚引用。不同的引用类型主要体现在GC上:

强引用,如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象;

软引用,在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收;

弱引用,具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象;

虚引用,顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收;

问:为什么要有不同的引用类型?
参考答案:

Java语言有时需要我们适当的控制对象被回收的时机,因此就诞生了不同的引用类型,可以说不同的引用类型实则是对GC回收时机不可控的妥协。比如说以下应用场景:

利用软引用和弱引用解决OOM问题:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题;

问:虚拟机是如何实现多态的?
参考答案:

动态绑定技术(dynamic binding),执行期间判断所引用对象的实际类型,根据实际类型调用对应的方法.

问:静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同?
参考答案:

Static Nested Class是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。而通常的内部类需要在外部类实例化后才能实例化

问:内部类的作用
参考答案:

内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。在单个外围类当中,可以让多个内部类以不同的方式实现同一接口,或者继承同一个类.

创建内部类对象的时刻不依赖于外部类对象的创建.内部类并没有令人疑惑的”is-a”关系,它就像是一个独立的实体。内部类提供了更好的封装,除了该外围类,其他类都不能访问

问:3*0.1==0.3返回值是什么
参考答案:

false,因为有些浮点数不能完全精确的表示出来。

问:a=a+b与a+=b有什么区别吗?
参考答案:

+=操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,而a=a+b则不会自动进行类型转换。

举个例子,如: byte a = 23; byte b = 22; b = a + b;//编译出错 而 b += a; // 编译OK

问:int 和Integer谁占用的内存更多?
参考答案:

Integer 对象会占用更多的内存。Integer是一个对象,需要存储对象的元数据。但是int是一个原始类型的数据,所以占用的空间更少;

问:JVM、JRE、JDK及 JIT 之间有什么不同
参考答案:

java 虚拟机 (JVM),是实现java语言平台独立性的基础,可以理解伪代码字节码,提供对多个平台的良好支持,在用户和操作系统之间建立了一层枢纽。

java 运行时环境 (JRE),是JVM 的一个超集。JVM 对于一个平台或者操作系统是明确的,而 JRE 确实一个一般的概念,他代表了完整的运行时环境。在 jre 文件夹中的jar 文件和可执行文件都会变成运行时的一部分。事实上,运行时 JRE 变成了 JVM。所以对于一般情况时候使用 JRE,对于明确的操作系统来说使用 JVM。当你下载了 JRE 的时候,也就自动下载了 JVM。

java 开发工具箱 (JDK),java 开发工具箱指的是编写一个 java 应用所需要的所有 jar 文件和可执行文件。事实上,JRE 是 JDK 的一部分。如果你下载了 JDK,你会看到一个名叫 JRE 的文件夹在里面。JDK 中要被牢记的 jar 文件就是 tools.jar,它包含了用于执行 java 文档的类还有用于类签名的 jar 包。

即时编译器 (JIT),即时编译器是种特殊的编译器,它通过有效的把字节码变成机器码来提高 JVM 的效率。JIT 这种功效很特殊,因为他把检测到的相似的字节码编译成单一运行的机器码,从而节省了 CPU 的使用。这和其他的字节码编译器不同,因为他是运行时编译(从字节码到机器码)而不是在程序运行之前。正是因为这些,动态编译这个词汇才和 JIT 有那么紧密的关系。

问:Java 泛型类在什么时候确定类型?
参考答案:

在编译期间确定变量类型。类型擦除。

问:引用类型是占用几个字节?
参考答案:

hotspot在64位平台上,占8个字节,在32位平台上占4个字节。

问:JDK主流版本的差异
参考答案:

Java 5(2004年发行),影响很大的一个版本;

1、泛型。
2、Metadata,元数据,描述数据的数据。
3、自动装箱和拆箱,也就是基本数据类型(如 int)和它的包装类型(如 Integer)自动帮你转换(其实背后是相关的方法帮你做了转换工作)。
4、枚举。
5、可变参数,一个函数可以传入数量不固定的参数值。
6、增强版的 for 循环。
7、改进了 Java 的内存模型,提供了 java.util.concurrent 并发包。
Java 6(2006年发行),这个版本的 Java 更多是对之前版本功能的优化,增强了用户的可用性和修复了一些漏洞,

1、提供动态语言支持。
2、提供编译 API,即 Java 程序可以调用 Java 编译器的 API。
3、Swing 库的一些改进。
4、JVM 的优化。
5、微型 HTTP 服务器 API ;
Java 7(2011年发行)

1、放宽 switch 的使用,可以在 switch 中使用字符串;
2、try-resource-with 语句,帮助自动化管理资源,如打开文件,对文件操作结束后,JVM 可以自动帮我们关闭文件资源,当然前提是你要用 try-resource-with 语句。
3、加入了类型推断功能,比如你之前版本使用泛型类型时这样写 ArrayList userList= new ArrayList();,这个版本只需要这样写 ArrayList userList= new ArrayList<>();,也即是后面一个尖括号内的类型,JVM 帮我们自动类型判断补全了。
4、简化了可变参数的使用。
5、支持二进制整数,在硬件开发中,二进制数更常用,方便人查看。
6、支持带下划线的数值,如 int a = 100000000;,0 太多不便于人阅读,这个版本支持这样写 int a = 100_000_000,这样就对数值一目了然了吧。
7、异常处理支持多个 catch 语句。
8、NIO 库的一些改进,增加多重文件的支持、文件原始数据和符号链接。
Java 8(2014年发行)

1、Lambda 表达式,简化代码
2、注解功能的增强。重复注解和注解扩展,现在几乎可以为任何东西添加注解:局部变量、泛型类、父类与接口的实现,就连方法的异常也能添加注解。
3、新的时间和日期 API,在这之前 Java 的时间和日期库被投票为最难用的 API 之一,所以这个版本就改进了。
4、JavaFX,一种用在桌面开发领域的技术(也是和其他公司竞争,这个让我们拭目以待吧)。
5、静态链接 JNI 程序库(这个做安卓开发的同学应该熟悉)。
6、接口默认方法和静态方法
7、函数式接口
8、方法引用
9、java.util.stream
10、HashMap的底层实现有变化
11、JVM内存管理方面,由元空间代替了永久代。
Java 9 (2017年发行)

1、模块化(这点也是向其他语言学习的,如 JavaScript)。
2、Java Shell(这点也是向其他语言学习的,如 Python),在这之前总有人说 Java 太麻烦,写个 Hello Word 都要新建个类,有时候测试个几行的代码都不方便,Java Shell 推出后,Java 开发者不用眼馋其他语言的 Shell 了;
3、即时编译功能的增强。
4、XML Catalogs,XML 文件自动校验。
Java 10(2018年发行)

1、局部变量的类型推断 var关键字
2、GC改进和内存管理 并行全垃圾回收器 G1
3、垃圾回收器接口
4、线程-局部变量管控
5、合并 JDK 多个代码仓库到一个单独的储存库中
Java 11(2018年发行)

1、本地变量类型推断
2、字符串加强
3、集合加强
4、Stream 加强
Optional 加强
5、InputStream 加强
6、HTTP Client API
7、化繁为简,一个命令编译运行源代码
问:final有哪些用法
参考答案:

被final修饰的类不可以被继承
被final修饰的方法不可以被重写
被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变.
被final修饰的方法,JVM会尝试将其内联,以提高运行效率
被final修饰的常量,在编译阶段会存入常量池中.

Map类面试题

问:说说List,Set,Map三者的区别?
参考答案:

List(对付顺序的好帮手):List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象;
Set(注重独一无二的性质):不允许重复的集合。不会有多个元素引用相同的对象;
Map(用Key来搜索的专家):使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。

问:poll()方法和remove()方法区别?
参考答案:

poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。

问:为何Map接口不继承Collection接口?
参考答案:

尽管Map接口和它的实现也是集合框架的一部分,但Map不是集合,集合也不是Map。因此,Map继承Collection毫无意义,反之亦然。

如果Map继承Collection接口,那么元素去哪儿?Map包含key-value对,它提供抽取key或value列表集合的方法,但是它不适合“一组对象”规范。

问:我们能否使用任何类作为Map的key?
参考答案:

我们可以使用任何类作为Map的key,然而在使用它们之前,需要考虑以下几点:

(1)如果类重写了equals()方法,它也应该重写hashCode()方法。

(2)类的所有实例需要遵循与equals()和hashCode()相关的规则。请参考之前提到的这些规则。

(3)如果一个类没有使用equals(),你不应该在hashCode()中使用它。

(4)用户自定义key类的最佳实践是使之为不可变的,这样,hashCode()值可以被缓存起来,拥有更好的性能。不可变的类也可以确保hashCode()和equals()在未来不会改变,这样就会解决与可变相关的问题了。

比如,我有一个类MyKey,在HashMap中使用它。

//传递给MyKey的name参数被用于equals()和hashCode()中 MyKey key = new MyKey(‘Pankaj’); //assume hashCode=1234 myHashMap.put(key, ‘Value’); // 以下的代码会改变key的hashCode()和equals()值 key.setName(‘Amit’); //assume new hashCode=7890 //下面会返回null,因为HashMap会尝试查找存储同样索引的key,而key已被改变了,匹配失败,返回null myHashMap.get(new MyKey(‘Pankaj’)); 那就是为何String和Integer被作为HashMap的key大量使用。

问:Map接口提供了哪些不同的集合视图?
参考答案:

Map接口提供三个集合视图:

(1)Set keyset():返回map中包含的所有key的一个Set视图。集合是受map支持的,map的变化会在集合中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。

(2)Collection values():返回一个map中包含的所有value的一个Collection视图。这个collection受map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个collection时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。

(3)Set<Map.Entry<K,V>> entrySet():返回一个map钟包含的所有映射的一个集合视图。这个集合受map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移除操作,以及对迭代器返回的entry进行setValue外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。

问:如何决定选用HashMap还是TreeMap?
参考答案:

对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。

问:TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素?
参考答案:

TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。Collections工具类的sort方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java中对函数式编程的支持)。

问:HashMap ,LinkedHashMap ,TreeMap,WeakHashMap,ConcurrentHashMap,IdentityHashMap的区别?
参考答案:

HashMap基于散列表来的实现,即使用hashCode()进行快速查询元素的位置,显著提高性能。插入和查询“键值对”的开销是固定的。可以通过设置容量和负载因子,以调整容器的性能。
LinkedHashMap, 类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入的次序,只比HashMap慢一点。而在迭代访问时反而更快,因为它使用链表维护内部次序。
TreeMap, 是基于红黑树的实现。实现了SortedMap,SortedMap 可以确保键处于排序状态。所以查看“键”和“键值对”时,所有得到的结果都是经过排序的,次序由Comparable或Comparator决定。SortedMap拥有其他额外的功能,如:Comparator comparator()返回当前Map使用的Comparator或者null. T firstKey() 返回Map的第一个键,T lastKey() 返回最后一个键。SortedMap headMap(toKey),生成一个键小于toKey的Map子集。SortedMap tailMap(fromKey) 也是生成一个子集。TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树
WeakHashMap表示弱键映射,允许释放映射所指向的对象。这是为了解决某类特殊问题而设计的,如果映射之外没有引用指向某个“键”,则“键”可以被垃圾收集器回收。
ConcurrentHashMap一种线程安全的Map,它不涉及同步加锁。
IdentityHashMap使用==代替equals() 对“键”进行比较的散列映射。专为解决特殊问题而设计。
问:HashMap 和 HashSet区别是什么?
参考答案:

如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。

HashMap HashSet
实现了Map接口 实现Set接口
存储键值对 仅存储对象
调用 put()向map中添加元素 调用 add()方法向Set中添加元素
HashMap使用键(Key)计算Hashcode HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,
问:HashSet如何检查重复?
参考答案:

当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。

问:WeakHashMap与HashMap的区别是什么?
参考答案:

WeakHashMap 的工作与正常的 HashMap 类似,但是使用弱引用作为 key,意思就是当 key 对象没有任何引用时,key/value 将会被回收。

List与Set类面试题

问:数组(Array)和列表(ArrayList)的差别是什么?
参考答案:

Array可以容纳基本类型和对象,而ArrayList只能容纳对象;

Array 是静态的,一旦创建就无法更改它的大小,ArrayList 是Java集合框架类的一员,可以称它为一个动态数组。

问:ArrayList和Vector有何异同点?
参考答案:

相同点:
(1)两者都是基于索引的,内部由一个数组支持。

(2)两者维护插入的顺序,我们可以根据插入顺序来获取元素。

(3)ArrayList和Vector的迭代器实现都是fail-fast的。

(4)ArrayList和Vector两者允许null值,也可以使用索引值对元素进行随机访问。

不同点:
(1)Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。

(2)ArrayList比Vector快,它因为有同步,不会过载。

(3)ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。

问:EnumSet是什么?
参考答案:

java.util.EnumSet是使用枚举类型的集合实现。当集合创建时,枚举集合中的所有元素必须来自单个指定的枚举类型,可以是显示的或隐示的。EnumSet是不同步的,不允许值为null的元素。

问:Java集合类中的Iterator和ListIterator的区别?
参考答案:

iterator()方法在set和list接口中都有定义,但是ListIterator()仅存在于list接口中(或实现类中);
ListIterator有add()方法,可以向List中添加对象,而Iterator不能;
ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以;
ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能;
都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改;

集合类进阶面试题

问:Java集合框架是什么?说出一些集合框架的优点?
参考答案:

每种编程语言中都有集合。集合框架的部分优点如下:
(1)使用核心集合类降低开发成本,而非实现我们自己的集合类。
(2)随着使用经过严格测试的集合框架类,代码质量会得到提高。
(3)通过使用JDK附带的集合类,可以降低代码维护成本。
(4)复用性和可操作性。

问:集合框架中的泛型有什么优点?
参考答案:

Java1.5引入了泛型,所有的集合接口和实现都大量地使用它。

泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException,因为你将会在编译时得到报错信息。泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符。它也给运行时带来好处,因为不会产生类型检查的字节码指令。

问:Java集合框架的基础接口有哪些?
参考答案:

Collection为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个接口任何直接的实现。

Set是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副牌。

List是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List更像长度动态变换的数组。

Map是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。

一些其它的接口有Queue、Dequeue、SortedSet、SortedMap和ListIterator。

问:为何Collection不从Cloneable和Serializable接口继承?
参考答案:

克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。因此,应该由集合类的具体实现来决定如何被克隆或者是序列化。

问:Iterator是什么?
参考答案:

Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素。

问:迭代器的优点
参考答案:

如果用的是for循环,就用集合自带的remove(),而这样就改变了集合的Size()循环的时候会出错。但如果把集合放入迭代器,既iterator迭代可以遍历并选择集合中的每个对象而不改变集合的结构,而把集合放入迭代器,用迭代器的remove()就不会出现问题

问:Enumeration和Iterator接口的区别?
参考答案:

Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。

问:Iterater和ListIterator之间有什么区别?
参考答案:

(1)我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。

(2)Iterator只可以向前遍历,而LIstIterator可以双向遍历。

(3)ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

问:哪些集合类是线程安全的?
参考答案:

Vector、HashTable、Properties和Stack是同步类,所以它们是线程安全的,可以在多线程环境下使用。Java1.5并发API包括一些集合类,允许迭代时修改,因为它们都工作在集合的克隆上,所以它们在多线程环境中是安全的。

问:并发集合类是什么?
参考答案:

Java1.5并发包(java.util.concurrent)包含线程安全集合类,允许在迭代时修改集合。迭代器被设计为fail-fast的,会抛出ConcurrentModificationException。一部分类为:CopyOnWriteArrayList、 ConcurrentHashMap、CopyOnWriteArraySet。

问:Collections类是什么?
参考答案:

Java.util.Collections是一个工具类仅包含静态方法,它们操作或返回集合。它包含操作集合的多态算法,返回一个由指定集合支持的新集合和其它一些内容。这个类包含集合框架算法的方法,比如折半搜索、排序、混编和逆序等。

问:如何保证线程安全又效率高?
参考答案:

Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。

ConcurrentHashMap将整个Map分为N个segment(类似HashTable),可以提供相同的线程安全,但是效率提升N倍,默认N为16。

问:怎么确保一个集合不能被修改?
参考答案:

(1)Java中提供final关键字,对基本类型进行修饰,当第一次初始化后,该变量就不可被修改 (2)Collections`工具类中的UnmodifiableList(不可修改的List、Map、Set等)

问:Comparator和Comparable的区别?
参考答案:

相同点
都是用于比较两个对象“顺序”的接口

都可以使用Collections.sort()方法来对对象集合进行排序

不同点
Comparable位于java.lang包下,而Comparator则位于java.util包下

Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序

总结
使用Comparable接口来实现对象之间的比较时,可以使这个类型(设为A)实现Comparable接口,并可以使用Collections.sort()方法来对A类型的List进行排序,之后可以通过a1.comparaTo(a2)来比较两个对象;

当使用Comparator接口来实现对象之间的比较时,只需要创建一个实现Comparator接口的比较器(设为AComparator),并将其传给Collections.sort()方法即可对A类型的List进行排序,之后也可以通过调用比较器AComparator.compare(a1, a2)来比较两个对象。

可以说一个是自己完成比较,一个是外部程序实现比较的差别而已。

用 Comparator 是策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。

比如:你想对整数采用绝对值大小来排序,Integer 是不符合要求的,你不需要去修改 Integer 类(实际上你也不能这么做)去改变它的排序行为,这时候只要(也只有)使用一个实现了 Comparator 接口的对象来实现控制它的排序就行了。

两种方式,各有各的特点:使用Comparable方式比较时,我们将比较的规则写入了比较的类型中,其特点是高内聚。但如果哪天这个规则需要修改,那么我们必须修改这个类型的源代码。如果使用Comparator方式比较,那么我们不需要修改比较的类,其特点是易维护,但需要自定义一个比较器,后续比较规则的修改,仅仅是改这个比较器中的代码即可。

问:为何Collection不从Cloneable和Serializable接口继承?
参考答案:

克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。因此,应该由集合类的具体实现来决定如何被克隆或者是序列化。

问:哪些集合类提供对元素的随机访问?
参考答案:

ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。

问:与Java集合框架相关的有哪些最好的实践?
参考答案:

(1)根据需要选择正确的集合类型。比如,如果指定了大小,我们会选用Array而非ArrayList。如果我们想根据插入顺序遍历一个Map,我们需要使用TreeMap。如果我们不想重复,我们应该使用Set。

(2)一些集合类允许指定初始容量,所以如果我们能够估计到存储元素的数量,我们可以使用它,就避免了重新哈希或大小调整。

(3)基于接口编程,而非基于实现编程,它允许我们后来轻易地改变实现。

(4)总是使用类型安全的泛型,避免在运行时出现ClassCastException。

(5)使用JDK提供的不可变类作为Map的key,可以避免自己实现hashCode()和equals()。

(6)尽可能使用Collections工具类,或者获取只读、同步或空的集合,而非编写自己的实现。它将会提供代码重用性,它有着更好的稳定性和可维护性。

Java内存结构与类加载机制

问:可以不可以自己写个String类?
参考答案:

不可以,因为 根据类加载的双亲委派机制,会去加载父类,父类发现冲突了 String 就不再加载了;

问:什么是类加载器?
参考答案:

负责读取 Java 字节代码,并转换成java.lang.Class类的一个实例。

问:程序计数器作用?
参考答案:

记录当前线程锁执行的字节码的行号。

程序计数器是一块较小的内存空间。
处于线程独占区。
执行java方法时,它记录正在执行的虚拟机字节码指令地址。执行native方法,它的值为undefined
该区域是唯一一个没有规定任何OutOfMemoryError的区域
问:如果判断两个类是否“相同”
参考答案:

类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。

通俗一点来讲,要判断两个类是否“相同”,前提是这两个类必须被同一个类加载器加载,否则这个两个类不“相同”。 这里指的“相同”,包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof关键字等判断出来的结果。

问:Java虚拟机如何结束生命周期?
参考答案:

执行System.exit()方法;
程序正常执行结束;
程序执行遇到异常或Error终止;
操作系统出错而导致java虚拟机运行终止。
问:何时触发初始化?
参考答案:

为一个类型创建一个新的对象实例时(比如new、反射、序列化);
调用一个类型的静态方法时(即在字节码中执行invokestatic指令);
调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式;
调用JavaAPI中的反射方法时(比如调用java.lang.Class中的方法,或者java.lang.reflect包中其他类的方法);
初始化一个类的派生类(子类)时(Java虚拟机规范明确要求初始化一个类时,它的超类必须提前完成初始化操作,接口例外);
JVM启动包含main方法的启动类时。
注意:通过子类引用付了的静态字段,不会导致子类初始化

问:JAVA热部署实现
参考答案:

首先谈一下何为热部署(hotswap),热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可以创建该类的实例。默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最根本的方式是修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 升级埋下了一个大坑。

另一种友好的方法是创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。

问: String s1 = new String(“abc”);这句话创建了几个字符串对象?
参考答案:

将创建 1 或 2 个字符串。如果池中已存在字符串文字“abc”,则池中只会创建一个字符串“s1”。如果池中没有字符串文字“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。

验证:

String s1 = new String(“abc”);// 堆内存的地址值
String s2 = “abc”;
System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
System.out.println(s1.equals(s2));// 输出 true
结果:

false
true
String 类和常量池
String 对象的两种创建方式:

String str1 = “abcd”;//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
String str2 = new String(“abcd”);//堆中创建一个新的对象
String str3 = new String(“abcd”);//堆中创建一个新的对象
System.out.println(str1str2);//false
System.out.println(str2
str3);//false
这两种不同的创建方法是有差别的。

第一种方式是在常量池中拿对象;
第二种方式是直接在堆内存空间创建一个新的对象。
记住一点:只要使用 new 方法,便需要创建新的对象。

String 类型的常量池比较特殊。它的主要使用方法有两种:

直接使用双引号声明出来的 String 对象会直接存储在常量池中。
如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。
String s1 = new String(“计算机”);
String s2 = s1.intern();
String s3 = “计算机”;
System.out.println(s2);//计算机
System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象,
System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象
字符串拼接:

String str1 = “str”;
String str2 = “ing”;

String str3 = “str” + “ing”;//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = “string”;//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。

问:说说Java对象创建过程
参考答案:

Java创建对象的过程.png

Step1:类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

Step2:分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 **“指针碰撞”**和 **“空闲列表”**两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

内存分配的两种方式:(补充内容,需要掌握)

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的

内存分配的两种方式.jpeg

内存分配并发问题

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

CAS+失败重试:CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
**TLAB:**为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
Step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

Step4:设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 **这些信息存放在对象头中。**另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

Step5:执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

问:Java对象结构
参考答案:

Java对象由三个部分组成:对象头、实例数据、对齐填充。

对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。

实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)

对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)

问:Java对象的定位方式
参考答案:

句柄和直接指针两种:

句柄:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
直接指针:如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

问:JDK8中MetaSpace代表什么?
参考答案:

MetaqSpace是JDK8才诞生的名词,它是一种新的内存空间,中文译为:元空间;JDK8 HotSpot中移除了永久代(PermGen Space),使用MetaSpace来代替,MetaSpace是使用本地内存来存储类元数据信息。内存容量取决于操作系统虚拟内存大小,通过参数MaxMetaspaceSize来限制MetaSpace的大小。

JVM与垃圾回收机制

问:Java 虚拟机结束生命周期的几种场景?
参考答案:

执行了System.exit()方法;
程序正常执行结束;
程序在执行过程中遇到了异常或错误而异常终止;
由于操作系统出现错误而导致 Java 虚拟机进程终止;
问:为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace)?
参考答案:

整个永久代有一个 JVM 本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到 java.lang.OutOfMemoryError。你可以使用 -XX:MaxMetaspaceSize。 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。 -XX:MetaspaceSize调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

问:JVM 中的 Class 只有满足什么条件被回收?
参考答案 :

该类所有的实例都已经被 GC,也就是 JVM 中不存在该 Class 的任何实例。
加载该类的 ClassLoader 已经被 GC。
该类的 java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
问:新生代、老年代、持久代都存储哪些东西?
参考答案:

新生代:方法中new一个对象,就会先进入新生代。

老年代:

新生代中经历了N次垃圾回收仍然存活的对象就会被放到老年代中。
大对象一般直接放入老年代。
当Survivor空间不足。需要老年代担保一些空间,也会将对象放入老年代。
永久代: 指的就是方法区。

问:什么时候进行MinorGC和FullGC
参考答案:

MinorGC:当Eden区满时,触发Minor GC.

FullGC:

调用System.gc时,系统建议执行Full GC,但是不必然执行
老年代空间不足
方法区空间不足
通过Minor GC后进入老年代的平均大小大于老年代的剩余空间
堆中分配很大的对象,而老年代没有足够的空间
问:MinorGC和FullGC的区别
参考答案:

Minor GC通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收速度比较快,一般采用复制-回收算法
Full GC/Major GC 发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,所采用的是标记-清除算法
问:Java中的引用是如何分类的?
参考答案:

强引用:GC时不会被回收
软引用:描述有用但不是必须的对象,在发生内存溢出异常之前被回收
弱引用:描述有用但不是必须的对象,在下一次GC时被回收
虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用PhantomReference实现虚引用,虚引用用来在GC时返回一个通知。
问:空间分配担保是什么?
参考答案:

在Minor GC之前,jvm会检查老年代最大可用连续空间是否大于新生代所有对象的空间。如果成立,则Minor GC是安全的。否则:

1、jvm设置不允许担保失败则立刻Full GC

\2. jvm设置允许担保失败则检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行一次冒险的Minor GC 否则Full GC

PS: Full GC 其实是为了让老年代腾出更多空间

问:堆内存不足的原因和解决方法
参考答案:

这种场景最为常见,报错信息

java.lang.OutOfMemoryError: Java heap space
原因:

1、代码中可能存在大对象分配

2、可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。

解决方法:

1、检查是否存在大对象的分配,最有可能的是大数组分配

2、通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题

3、如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存

4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性

问:永久代/元空间溢出的原因和解决方法
参考答案:

报错信息:

java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Metaspace
原因:

永久代是 HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。

JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:1、字符串常量由永久代转移到堆中;2、和永久代相关的JVM参数已移除;

出现永久代或元空间的溢出的原因可能有如下几种:

1、在Java7之前,频繁的错误使用String.intern方法;

2、生成了大量的代理类,导致方法区被撑爆,无法卸载;

3、应用长时间运行,没有重启。

解决方法:

永久代/元空间 溢出的原因比较简单,解决方法有如下几种:

1、检查是否永久代空间或者元空间设置的过小;

2、检查代码中是否存在大量的反射操作;

3、dump之后通过mat检查是否存在大量由于反射生成的代理类;

4、放大招,重启JVM。

问:GC overhead limit exceeded的原因和解决方法
参考答案:

这个异常比较的罕见,报错信息:

java.lang.OutOfMemoryError:GC overhead limit exceeded
原因:

这个是JDK6新加的错误类型,一般都是堆太小导致的。

Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。

解决方法:

1、检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。

2、添加参数-XX:-UseGCOverheadLimit禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。

3、dump内存,检查是否存在内存泄露,如果没有,加大内存。

问:方法栈溢出的原因和解决方法
报错信息:

java.lang.OutOfMemoryError : unable to create new native Thread
原因:

出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程。

解决方法:

1、通过 *-Xss *降低的每个线程栈大小的容量

2、线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:

/proc/sys/kernel/pid_max

/proc/sys/kernel/thread-max

max_user_process(ulimit -u)

/proc/sys/vm/max_map_count

锁与并发面试题

问:说说并发与并行的区别?
参考答案:

并发(conurrent):

​ 在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。类似操作系统的时间片分时调度。打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作。那么,就可以说听音乐和打游戏是并发的。

并行(Parallel):

​ 当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。所以,**并发是指在一段时间内宏观上多个程序同时运行。并行指的是同一个时刻,多个任务确实真的在同时运行。****并发,指的是多个事情,在同一时间段内同时发生了。****并行,指的是多个事情,在同一时间点上同时发生了。**并发的多个任务之间是互相抢占资源的。并行的多个任务之间是不互相抢占资源的、只有在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。

​ 这里面有一个很重要的点,那就是系统要有多个CPU才会出现并行。在有多个CPU的情况下,才会出现真正意义上的『同时进行』。

问:说说 sleep() 方法和 wait() 方法区别和共同点?
参考答案:

两者最主要的区别在于:sleep()方法没有释放锁,而 wait()方法释放了锁。

两者都可以暂停线程的执行。

wait()通常被用于线程间交互/通信,sleep()通常被用于暂停执行。

wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒。

问: 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?
参考答案:

​ JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

​ 1. 偏向锁:无竞争条件下,消除整个同步互斥,连CAS都不操作。消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。即在无竞争的情况下,把 整个同步都消除掉。这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁 没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。

​ 2.轻量级锁:无竞争条件下,通过CAS消除同步互斥,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

3.自旋锁:为了减少线程状态改变带来的消耗,不停地执行当前线程;

4.自适应自旋锁:自旋的时间不固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 如果一个锁对象,自旋等待刚刚成功获得锁,并且持有锁的线程正在运行,那么虚拟机认 为这次自旋仍然可能成功,进而运行自旋等待更长的时间。 如果对于某个锁,自旋很少成功,那在以后要获取这个锁,可能省略掉自旋过程,以免浪费处理器资源。 有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会 越来越准确,虚拟机也会越来越聪明。

5.锁消除:不可能存在共享数据竞争的锁进行消除;

6.锁粗化:将连续的加锁,精简到只加一次锁。原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一个对象反复加锁和 解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。 锁粗化就是增大锁的作用域;

​ 其中,锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 img

问:自旋锁解决什么问题?自旋锁的原理是什么?自旋的缺点?
参考答案:

(1)自旋锁解决什么问题?

互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完 成,这些操作给系统的并发性带来很大压力。同时很多应用共享数据的锁定状态,只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。先不挂起线程,等一会儿。

(2)自旋锁的原理是什么?

如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放。为了让线程等待,我们只需让线程执行一个忙循环(自旋)。

(3)自旋的缺点?

自旋等待本身虽然避免了线程切换的开销,但它要占用处理器时间。所以如果锁被占用的时间 很短,自旋等待的效果就非常好;如果时间很长,那么自旋的线程只会白白消耗处理器的资 源。所以自旋等待的时间要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁, 那就应该使用传统的方式挂起线程了。

问:单机场景下常用的并发控制手段有哪些?
参考答案:

最基础的:1.同步方法synchronized;2.同步块synchronized。

进阶的:重入锁ReentrantLock,Semaphore信号量

问:什么是公平锁?什么是非公平锁?
参考答案:

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

P.S:Java主流锁的分类

img

问:volatile的作用?
参考答案:

关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义成volatile之后, 具备两种特性:

\1. 保证此变量对所有线程的可见性。当一条线程修改了这个变量的值,新值对于其他线程是 可以立即得知的。而普通变量做不到这一点。

\2. 禁止指令重排序优化。普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不 保证程序代码的执行顺序。

问:为什么基于volatile变量的运算在并发下不一定是安全的?
参考答案:

volatile变量在各个线程的工作内存,不存在一致性问题(各个线程的工作内存中volatile变 量,每次使用前都要刷新到主内存)。但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。

问:为什么使用volatile?
参考答案:

在某些情况下,volatile同步机制的性能要优于锁(synchronized关键字),但是由于虚拟机 对锁实行的许多消除和优化,所以并不是很快。

volatile变量读操作的性能消耗与普通变量几乎没有差别,但是写操作则可能慢一些,因为它 需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值