精选 Java 面试题大放送

01

Java程序是怎么执行的?

我们日常的工作中都使用开发工具(IntelliJ IDEA 或 Eclipse 等)可以很方便的调试程序,或者是通过打包工具把项目打包成 jar 包或者 war 包,放入 Tomcat 等 Web 容器中就可以正常运行了,但你有没有想过 Java 程序内部是如何执行的?其实不论是在开发工具中运行还是在 Tomcat 中运行,Java 程序的执行流程基本都是相同的,它的执行流程如下:

  • 先把 Java 代码编译成字节码,也就是把 .java 类型的文件编译成 .class 类型的文件。这个过程的大致执行流程:Java 源代码 -> 词法分析器 -> 语法分析器 -> 语义分析器 -> 字符码生成器 -> 最终生成字节码,其中任何一个节点执行失败就会造成编译失败;

  • 把 class 文件放置到 Java 虚拟机,这个虚拟机通常指的是 Oracle 官方自带的 Hotspot JVM;

  • Java 虚拟机使用类加载器(Class Loader)装载 class 文件;

  • 类加载完成之后,会进行字节码效验,字节码效验通过之后 JVM 解释器会把字节码翻译成机器码交由操作系统执行。但不是所有代码都是解释执行的,JVM 对此做了优化,比如,以 Hotspot 虚拟机来说,它本身提供了 JIT(Just In Time)也就是我们通常所说的动态编译器,它能够在运行时将热点代码编译为机器码,这个时候字节码就变成了编译执行。

Java 程序执行流程图如下:精选 Java 面试题大放送_第1张图片

 

02

Java 虚拟机是如何判定热点代码的?

Java 虚拟机判定热点代码的方式有两种:

  • 基于采样的热点判定:主要是虚拟机会周期性的检查各个线程的栈顶,若某个或某些方法经常出现在栈顶,那这个方法就是“热点方法”。这种判定方式的优点是实现简单;缺点是很难精确一个方法的热度,容易受到线程阻塞或外界因素的影响。

  • 基于计数器的热点判定:主要就是虚拟机给每一个方法甚至代码块建立了一个计数器,统计方法的执行次数,超过一定的阀值则标记为此方法为热点方法。

Hotspot 虚拟机使用的基于计数器的热点探测方法。它使用了两类计数器:方法调用计数器和回边计数器,当到达一定的阀值是就会触发 JIT 编译。

方法调用计数器:在 client 模式下的阀值是 1500 次,Server 是 10000 次,可以通过虚拟机参数:-XX:CompileThreshold=N 对其进行设置。但是JVM还存在热度衰减,时间段内调用方法的次数较少,计数器就减小。回边计数器:主要统计的是方法中循环体代码执行的次数。

 

03

以下 Integer 代码输出的结果是?

 
  1. Integer age = 10;

  2. Integer age2 = 10;

  3. Integer age3 = 133;

  4. Integer age4 = 133;

  5. System.out.println((age == age2) + ","+ (age3 == age4));

 

答:true,false题目解析:此道题目考察的是,面试者对于基础类型高频区缓存的掌握,因为 Integer 的高频区的取值是 -128-127,所以在这个区间的值会复用已有的缓存,对比的结果自然是 true,false 。

 

04

以下 StringBuffer 传值修改后的执行结果是什么?

 
  1. publicstaticvoid main(String[] args) {

  2. StringBuffer sf = newStringBuffer("hi");

  3. changeStr(sf);

  4. System.out.println(sf);

  5. }

  6. publicstaticvoid changeStr(StringBuffer sf){

  7. sf.append("laowang");

  8. }

答:hilaowang题目解析:String 为不可变类型,在方法内对 String 修改的时候,相当修改传递过来的是一个 String 副本,所以 String 本身的值是不会被修改的,而 StringBuffer 为可变类型,传递过来的参数相当于对象本身,所以打印的结果就为 hilaowang

 

05

以下数组比较的结果分别是什么?

 
  1. String[] strArr = {"dog", "cat", "pig", "bird"};

  2. String[] strArr2 = {"dog", "cat", "pig", "bird"};

  3. System.out.println(Arrays.equals(strArr, strArr2));

  4. System.out.println(strArr.equals(strArr2));

  5. System.out.println(strArr == strArr2);

答:true、 false、 false。题目解析:strArr == strArr2 为引用比较,因此结果一定是 false,而数组本身的比较也就是 strArr.equals(strArr2) 为 false 的原因是因为数组没有重写 equals 方法,因此也是引用比较。数组 equals 源码实现如下:

 
  1. publicboolean equals(Object obj) {

  2. return(this== obj);

  3. }

而 Arrays.equals 的结果之所以是 true 是因为 Arrays.equals 重写了 equals 方法。源代码实现如下:

 
  1. publicstaticboolean equals(Object[] a, Object[] a2) {

  2. if(a==a2)

  3. returntrue;

  4. if(a==null|| a2==null)

  5. returnfalse;

  6. int length = a.length;

  7. if(a2.length != length)

  8. returnfalse;

  9. for(int i=0; i

  10. Object o1 = a[i];

  11. Object o2 = a2[i];

  12. if(!(o1==null? o2==null: o1.equals(o2)))

  13. returnfalse;

  14. }

  15. returntrue;

  16. }

 

06

常用的序列化方式都有哪些?

答:常用的序列化方式有以下三种:1) Java 原生序列化方式请参考以下代码:

 
  1. // 序列化和反序列化

  2. classSerializableTest{

  3. publicstaticvoid main(String[] args) throwsIOException, ClassNotFoundException{

  4. // 对象赋值

  5. User user = newUser();

  6. user.setName("老王");

  7. user.setAge(30);

  8. System.out.println(user);

  9. // 创建输出流(序列化内容到磁盘)

  10. ObjectOutputStream oos = newObjectOutputStream(newFileOutputStream("test.out"));

  11. // 序列化对象

  12. oos.writeObject(user);

  13. oos.flush();

  14. oos.close();

  15. // 创建输入流(从磁盘反序列化)

  16. ObjectInputStream ois = newObjectInputStream(newFileInputStream("test.out"));

  17. // 反序列化

  18. User user2 = (User) ois.readObject();

  19. ois.close();

  20. System.out.println(user2);

  21. }

  22. }

  23. classUserimplementsSerializable{

  24. privatestaticfinallong serialVersionUID = 5132320539584511249L;

  25. privateString name;

  26. privateint age;

  27. @Override

  28. publicString toString() {

  29. return"{name:"+ name + ",age:"+ age + "}";

  30. }

  31. publicString getName() {

  32. return name;

  33. }

  34. publicvoid setName(String name) {

  35. this.name = name;

  36. }

  37. publicint getAge() {

  38. return age;

  39. }

  40. publicvoid setAge(int age) {

  41. this.age = age;

  42. }

  43. }

2) JSON 格式,可使用 fastjson 或 GSONJSON 是一种轻量级的数据格式,JSON 序列化的优点是可读性比较高,方便调试。我们本篇以 fastjson 的序列化为例,请参考以下代码:

 
  1. // 序列化和反序列化

  2. classSerializableTest{

  3. publicstaticvoid main(String[] args) throwsIOException, ClassNotFoundException{

  4. // 对象赋值

  5. User user = newUser();

  6. user.setName("老王");

  7. user.setAge(30);

  8. System.out.println(user);

  9.  

  10. String jsonSerialize = JSON.toJSONString(user);

  11. User user3 = (User) JSON.parseObject(jsonSerialize, User.class);

  12. System.out.println(user3);

  13. }

  14. }

  15. classUserimplementsSerializable{

  16. privatestaticfinallong serialVersionUID = 5132320539584511249L;

  17. privateString name;

  18. privateint age;

  19. @Override

  20. publicString toString() {

  21. return"{name:"+ name + ",age:"+ age + "}";

  22. }

  23. publicString getName() {

  24. return name;

  25. }

  26. publicvoid setName(String name) {

  27. this.name = name;

  28. }

  29. publicint getAge() {

  30. return age;

  31. }

  32. publicvoid setAge(int age) {

  33. this.age = age;

  34. }

  35. }

3) Hessian 方式序列化:Hessian 序列化的优点是可以跨编程语言,比 Java 原生的序列化和反序列化效率高。请参考以下示例代码:

 
  1. // 序列化和反序列化

  2. classSerializableTest{

  3. publicstaticvoid main(String[] args) throwsIOException, ClassNotFoundException{

  4. // 序列化

  5. ByteArrayOutputStream bo = newByteArrayOutputStream();

  6. HessianOutput hessianOutput = newHessianOutput(bo);

  7. hessianOutput.writeObject(user);

  8. byte[] hessianBytes = bo.toByteArray();

  9. // 反序列化

  10. ByteArrayInputStream bi = newByteArrayInputStream(hessianBytes);

  11. HessianInput hessianInput = newHessianInput(bi);

  12. User user4 = (User) hessianInput.readObject();

  13. System.out.println(user4);

  14. }

  15. }

  16. classUserimplementsSerializable{

  17. privatestaticfinallong serialVersionUID = 5132320539584511249L;

  18. privateString name;

  19. privateint age;

  20. @Override

  21. publicString toString() {

  22. return"{name:"+ name + ",age:"+ age + "}";

  23. }

  24. publicString getName() {

  25. return name;

  26. }

  27. publicvoid setName(String name) {

  28. this.name = name;

  29. }

  30. publicint getAge() {

  31. return age;

  32. }

  33. publicvoid setAge(int age) {

  34. this.age = age;

  35. }

  36. }

 

07

有哪些方法可以解决哈希冲突?

答:哈希冲突的常用解决方案有以下 4 种:

  • 开放定址法:当关键字的哈希地址 p=H(key)出现冲突时,以 p 为基础,产生另一个哈希地址 p1,如果 p1 仍然冲突,再以 p 为基础,产生另一个哈希地址 p2,循环此过程直到找出一个不冲突的哈希地址,将相应元素存入其中;

  • 再哈希法:这种方法是同时构造多个不同的哈希函数,当哈希地址 Hi=RH1(key)发生冲突时,再计算 Hi=RH2(key),循环此过程直到找到一个不冲突的哈希地址,这种方法唯一的缺点就是增加了计算时间;

  • 链地址法:这种方法的基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第 i 个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况;

  • 建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

08

JVM 内存布局是怎样的?

答:不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚拟机规范,Java 8 虚拟机规范规定,Java 虚拟机所管理的内存将会包括以下几个区域:

  • 程序计数器(Program Counter Register)

  • Java 虚拟机栈(Java Virtual Machine Stacks)

  • 本地方法栈(Native Method Stack)

  • Java 堆(Java Heap)

  • 方法区(Methed Area)

以上面试这几个知识点均来自《Java面试全解析:核心知识点与典型面试题》这门专栏。

你可能感兴趣的:(面试)