福利!福利!!!弄懂这些java面试题,面试官对你刮目相看!(该面试题带有答案)

 简言:

       2020年我们没有被疫情压倒,我们都在坚持着,努力着,为在中国而感到骄傲! 矫情完了。

 今年的经济三驾马车,投资、消费和出口,都恨不乐观。

 政府的财政投资刺激可以加码,但是加码量无法抵消消费和出口的损失。

 关键的是民间投资恐大幅下滑,企业支出也会大幅收缩。

红杉资本提醒企业家可能将面临着业务萎缩、供应链中断、部分行业的市场需求可能减弱的风险,要做好应对“黑天鹅”的准备,确保企业的健康。

当然了 国家大事不是我们关心的,我们最应该关心的是工作问题,我之所以说经济问题,是因为今年是真TMD不景气啊哈哈,欲哭无泪。

想必现在一定有很多人在面临公司的倒闭,找工作等问题,那么我来了。。。。特此送上一波福利!!!!!!

我虽然还在现在的公司奋斗着,努力着,但是我一直没有放弃,远走高飞,所以我在某个地方找到了关于一些java的面试题,我整理一下分享给大家,闲话少说上福利!!!!!!

                     

1.谈谈你对java平台的理解,java是解释执行,这句话正确吗?

答:java本身是一种面向对象的语言,最显著的特性有两个方面,一是所谓的书写一次,到处运行,能够非常容易的获取跨平台能力,另外是垃圾收集,java通过垃圾收集器回收分配内存,大部分情况下,程序员不要自己操心内存分配和回收。

我们日常会接触到JRE或者JDK,JRE就是java运行环境,包含了JVM和java类库,以及一些模块等,而JDK可以看做是JRE的一个超集,提供了更多的工具,比如编辑器,各种诊断工具。

对于java是解释执行这句话,这个说法不太准确,我们开发的java源码,首先通过javac编译成字节码,然后在运行时,通过java虚拟机,内嵌的解释器将字节码转换成为最终的机器码,但是常见的jvm,比如我们大多数情况使用的Oracle JDK提供的Hostpot JVM,都提供了jit编译器,也就是通常所说的动态编译器,JIT能够在运行时将热点代码编译成机器码,这种情况下部分热点代码就属于编译执行,而不是解释执行了。

 

2.请对比Exception 和 error,另外运行时异常与一般异常有什么区别?

答:Execption 和 error都是集成可Throwable类,在java中只有Throwable类型的实例才可以被抛出或者捕获,它是异常机制的基本组成类型。

Exeception 和error 体现了java平台设计者对不同异常情况的分类,Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。

Error是指在正常情况下,不大可能出现情况,绝大部分的Error都会导致程序处于非正常的,不可恢复状态,既然是非正常情况,所以不便于也不需要捕获,常见的比如OutOfMemoryError类,都是error的子类。

Execption又分为可检查异常和不检查异常,可检查异常在源代码中必须要显示地进行捕获处理,这是编译期检查的一部分,不检查异常就是我们比较常见的空指针异常,数组下标越界异常,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会再编译期强制要求。

 

3.谈谈final finally finalize有什么不同?

final 可以用来修饰类,方法,变量,分别有不同的意义,final修饰的class代表不可以继承扩展,final的变量是不可以修改的,而final的方法也是不可以重写的。

Finally 则是java保证重点代码一定要被执行的一种机制,我们可以使用try-finally或者try-catch-finally来进行类似的关闭jdbc连接,

Finalize是基础类,java.lang.object的一个方法,他的设计目的是保证对象在被垃圾收集前完成特定资源的回收,finalize机制现在已经不推荐使用,并且在jdk9开始被标记为deprecated。

 

4.强引用,软引用,弱引用,虚引用有什么区别,具体使用场景是什么?

不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响。

强引用:就是我们常见的普通对象引用,只要还有一个强引用指向一个对象,就表明这个对象还活着,垃圾收集器不会碰这种对象,对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应强引用赋值为null,就可以被垃圾收集了,当然具体回收时机还是要看垃圾收集策略。
软引用:相对强引用弱化了一些,可以让对象豁免一些垃圾收集,只有当虚拟机认为内存不足时,才会试图回收软引用指向的对象,虚拟机会确保在抛出内存溢出之前清理软引用指向的对象,软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

弱引用:并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径,这就可以用来构建一种没有也定约束的关系,比如维护一种非强制性的映射关系,如果试图获取时对象还在,就是用它,否则重新实例化,它同样是很多缓存实现的选择。

虚引用:你不能通过它访问对象,虚引用仅仅是提供了一种确保对象被finalize以后,做某些事情的机制,比如,通常用来做所谓的PostMortem清理机制,也可以利用虚引用监控对象的创建和销毁。

 

5.理解java字符串,String,StringBuffer,StringBuilder有什么区别?

String是java语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的Immutable类,被声明成为final class,所有属性也都是final的。也由于它的不可变性,类似拼接,裁剪字符串等动作,都会产生新的String对象,由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。

StringBuffer是为了解决上面提供到拼接产生太多中间对象的问题而提供的类,我们可以用append或者add方法把字符串添加到已有序列的末尾或者指定位置。

StringBuffer本质是一个线程安全的可修改的字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者也就是StringBuilder.

StringBuilder是java1.5中新增的,在能力上和Stringuffer没有本质区别,但是它去掉了线程安全的部分,减小了开销,是绝大部分情况下进行字符串拼接的首选。

 

6.谈谈反射机制,动态代理是基于什么原理?

反射机制是java语言提供的一种基础功能,赋予程序在运行时自省的能力,通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。

动态代理是一种方便运行时动态构建代理,动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装RPC调用,面向切面编程。

实现动态代理的方式有很多,比如JDK自身提供的动态代理,就是主要利用了上面提供的反射机制,还有其他的实现方式,比如利用传说中更高性能得字节码操作机制,类似ASM,cglib,javassist等。

 

7.Int和integer有什么区别,integer的值缓存范围?

   Int是我们常说的整形数字,是java的八种基本数据类型之一,java语言虽然号称一切都是对象,但是原始数据类型是例外。

   Integer是int对应的包装类,他有一个int类型的字段存储数据,并且提供了基本操作,比如数字运算,int和字符串之间转换等,在java5中,引入了自动装箱和自动拆箱功能,java可以根据上下文,自动进行转换,极大的简化了相关编程。

关于integer的值缓存,这涉及java5中的另一个改进,构建Integer对象的传统方式是直接调用构造器,直接new一个对象,但是根据实践,我们发现大部分数据操作都是集中在有限的,较小的值范围,因而,在java5中新增了静态工厂方法valueOf,在调用他的时候利用一个缓存机制,带来了明显的性能改进,按照javaDoc,这个值默认缓存是-128 ---  127之间。

 

8.对比vector,ArrayList,linkedList有何区别?

这三者都是实现集合框架中的List,也就是所谓的有序集合,因此具体功能也比较近似,比如都提供按照位置定位,添加或者删除操作。都提供迭代器以便利其内容等,但因为具体的设置区别,在行为,性能,线程安全上表现不同。

Vector是java早期提供的线程安全的动态数组,如果不需要线程安全,并不建议使用,毕竟同步是有额外开销的,Vector内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有的数组。

Arraylist是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多,与Vector近似。ArrayList也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector在扩容时会提高一倍。而ArrayList则是增加50%。

LinkedList顾名思义是java提供的双向链表,所以它不需要像上面两种那样调整容量,他不是线程安全的。

 

9.对比Hashtable,HashMap,TreeMap有什么不同。

  HashTable,hashmap,treeMap都是最常见的一些Map实现,是以键值对的形式存储和操作数据的容量类型。

HashTable是早期java类提供的一个哈希表实现,本身是同步的,不支持null键值对,由于同步导致的开销,所以现在不长使用。

hashMap是应用更加广泛的哈希表实现,行为上大致与hashTable一致,主要区别在于hashMap不是同步的,支持null键值对,通常情况下HashMap进行put或者get操作,可以达到常数时间的性能,所以他是绝大部分利用键值对存储场景的首选。

TreeMap是基于红黑树的一种提供顺序访问的Map,和HashMap不同,他的get,put之类操作都是0的时间复杂度,具体顺序可以由指定的Comparator来决定,或者根据键的自然顺序来判断。

 

10. 如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?

Java 提供了不同层面的线程安全支持。在传统集合框架内部,除了 Hashtable 等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),我们可以调用 Collections 工具类提供的包装方法,来获取一个同步的包装容器(如 Collections.synchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下。

另外,更加普遍的选择是利用并发包提供的线程安全容器类,它提供了:

  1. 各种并发容器,比如 ConcurrentHashMapCopyOnWriteArrayList。
  2. 各种线程安全队列(Queue/Deque),如 ArrayBlockingQueueSynchronousQueue。
  3. 各种有序容器的线程安全版本等。

具体保证线程安全的方式,包括有从简单的 synchronize 方式,到基于更加精细化的,比如基于分离锁实现的 ConcurrentHashMap 等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远优于早期的简单同步实现。

 

11. Java提供了哪些IO方式? NIO如何实现多路复用?

Java IO 方式有很多种,基于不同的 IO 抽象模型和交互方式,可以进行简单区分。

首先,传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。

java.io 包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。

很多时候,人们也把 java.net 下面提供的部分网络 API,比如 SocketServerSocketHttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。

第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 ChannelSelectorBuffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。

第三,在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIOAsynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。

 

12. Java有几种文件拷贝方式?哪一种最高效?

1)利用 java.io 类库,直接为源文件构建一个 FileInputStream 读取,然后再为目标文件构建一个 FileOutputStream,完成写入工作。

public static void copyFileByStream(File source, File dest) throws

        IOException {

    try (InputStream is = new FileInputStream(source);

         OutputStream os = new FileOutputStream(dest);){

        byte[] buffer = new byte[1024];

        int length;

        while ((length = is.read(buffer)) > 0) {

            os.write(buffer, 0, length);

        }

    }

 }

2)利用 java.nio 类库提供的 transferTo transferFrom 方法实现。

public static void copyFileByStream(File source, File dest) throws

        IOException {

    try (InputStream is = new FileInputStream(source);

         OutputStream os = new FileOutputStream(dest);){

        byte[] buffer = new byte[1024];

        int length;

        while ((length = is.read(buffer)) > 0) {

            os.write(buffer, 0, length);

        }

    }

 }

Java 标准类库本身已经提供了几种 Files.copy 的实现。

对于 Copy 的效率,这个其实与操作系统和配置等情况相关,总体上来说,NIO transferTo/From 的方式可能更快,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换。

 

13. 谈谈接口和抽象类有什么区别?

接口和抽象类是 Java 面向对象设计的两个基础机制。

接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。接口,不能实例化;不能包含任何非常量成员,任何 field 都是隐含着 public static final 的意义;同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。Java 标准类库中,定义了非常多的接口,比如 java.util.List

抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。Java 标准库中,比如 collection 框架,很多通用部分就被抽取成为抽象类,例如 java.util.AbstractList

Java 类实现 interface 使用 implements 关键词,继承 abstract class 则是使用 extends 关键词,我们可以参考 Java 标准库中的 ArrayList

public class ArrayList<E> extends AbstractList<E>

        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

{

//...

}

 

14. 谈谈你知道的设计模式?请手动实现单例模式,Spring 等框架中使用了哪些模式?

大致按照模式的应用目标分类,设计模式可以分为创建型模式、结构型模式和行为型模式。

  1. 创建型模式,是对对象创建过程的各种问题和解决方案的总结,包括各种工厂模式(FactoryAbstract Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)。
  2. 结构型模式,是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。常见的结构型模式,包括桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等。
  3. 行为型模式,是从类或对象之间交互、职责划分等角度总结的模式。比较常见的行为型模式有策略模式(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor)。

 

15. synchronized 和 ReentrantLock 有什么区别?有人说 synchronized 最慢,这话靠谱吗?

synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。

在 Java 5 以前,synchronized 是仅有的同步手段,在代码中, synchronized 可以用来修饰方法,也可以使用在特定的代码块儿上,本质上 synchronized 方法等同于把方法全部语句用 synchronized 块包起来。

ReentrantLock,通常翻译为再入锁,是 Java 5 提供的锁实现,它的语义和 synchronized 基本相同。再入锁通过代码直接调用 lock() 方法获取,代码书写也更加灵活。与此同时,ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制,比如可以控制 fairness,也就是公平性,或者利用定义条件等。但是,编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。

synchronized 和 ReentrantLock 的性能不能一概而论,早期版本 synchronized 在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock。

 

 

16. synchronized 底层如何实现?什么是锁的升级、降级?

在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。

现代的(OracleJDK 中,JVM 对此进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。

当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。

如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

我注意到有的观点认为 Java 不会进行锁降级。实际上据我所知,锁降级确实是会发生的,当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。

 

17. 一个线程两次调用 start() 方法会出现什么情况?谈谈线程的生命周期和状态转移。

 Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误。

关于线程生命周期的不同状态,在 Java 5 以后,线程状态被明确定义在其公共内部枚举类型 java.lang.Thread.State 中,分别是:

  1. 新建(NEW),表示线程被创建出来还没真正启动的状态,可以认为它是个 Java 内部状态。
  2. 就绪(RUNNABLE),表示该线程已经在 JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它 CPU 片段,在就绪队列里面排队。
  3. 在其他一些分析中,会额外区分一种状态 RUNNING,但是从 Java API 的角度,并不能表示出来。
  4. 阻塞(BLOCKED),这个状态和我们前面两讲介绍的同步非常相关,阻塞表示线程在等待 Monitor lock。比如,线程试图通过 synchronized 去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。
  5. 等待(WAITING),表示正在等待其他线程采取某些操作。一个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似 notify 等动作,通知消费线程可以继续工作了。Thread.join() 也会令线程进入等待状态。
  6. 计时等待(TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如 wait join 等方法的指定超时版本,如下面示例:
  7. 终止(TERMINATED),不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。

在第二次调用 start() 方法的时候,线程可能处于终止或者其他(非 NEW)状态,但是不论如何,都是不可以再次启动的。 

 

18. 什么情况下 Java 程序会产生死锁?如何定位、修复?

死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。通常来说, 我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。

定位死锁最常见的方式就是利用 jstack 等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往 jstack 等就能直接定位,类似 JConsole 甚至可以在图形界面进行有限的死锁检测。

如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决的,只能重启、修正程序本身问题。所以,代码开发阶段互相审查,或者利用工具进行预防性排查,往往也是很重要的。

 

19. Java 并发包提供了哪些并发工具类?

 我们通常所说的并发包也就是 java.util.concurrent 及其子包,集中了 Java 并发的各种基础工具类,具体主要包括几个方面:

提供了比 synchronized 更加高级的各种同步结构,包括 CountDownLatchCyclicBarrierSemaphore 等,可以实现更加丰富的多线程操作,比如利用 Semaphore 作为资源控制器,限制同时进行工作的线程数量。

种线程安全的容器,比如最常见的 ConcurrentHashMap、有序的 ConcurrentSkipListMap,或者通过类似快照机制,实现线程安全的动态数组 CopyOnWriteArrayList 等。

各种并发队列实现,如各种 BlockingQueue 实现,比较典型的 ArrayBlockingQueue SynchronousQueue 或针对特定场景的 PriorityBlockingQueue 等。

强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运行等,绝大部分情况下,不再需要自己从头实现线程池和任务调度器。 

 

20. 并发包中的 ConcurrentLinkedQueue 和 LinkedBlockingQueue 有什么区别?

 有时候我们把并发包下面的所有容器都习惯叫作并发容器,但是严格来讲,类似 ConcurrentLinkedQueue 这种“Concurrent*”容器,才是真正代表并发。关于问题中它们的区别:

Concurrent 类型基于 lock-free,在常见的多线程访问场景,一般可以提供较高吞吐量。
LinkedBlockingQueue 内部则是基于锁,并提供了 BlockingQueue 的等待性方法。

不知道你有没有注意到,java.util.concurrent 包提供的容器(QueueListSet)、Map,从命名上可以大概区分为 Concurrent*CopyOnWrite Blocking等三类,同样是线程安全容器,可以简单认为:

Concurrent 类型没有类似 CopyOnWrite 之类容器相对较重的修改开销。

但是,凡事都是有代价的,Concurrent 往往提供了较低的遍历一致性。你可以这样理解所谓的弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历。

与弱一致性对应的,就是我介绍过的同步容器常见的行为“fail-fast”,也就是检测到容器在遍历过程中发生了修改,则抛出 ConcurrentModificationException,不再继续遍历。

弱一致性的另外一个体现是,size 等操作准确性是有限的,未必是 100% 准确。

与此同时,读取的性能具有一定的不确定性。

 

21. 请介绍类加载过程,什么是双亲委派模型?

一般来说,我们把 Java 的类加载过程分为三个主要步骤:加载、链接、初始化,具体行为在Java 虚拟机规范里有非常详细的定义。

首先是加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。

加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程

第二阶段是链接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程中。这里可进一步细分为三个步骤:

验证(Verification),这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。

准备(Preparation),创建类或接口中的静态变量,并初始化静态变量的初始值。但这里的初始化和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。

解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。在Java 虚拟机规范中,详细介绍了类、接口、方法和字段等各个方面的解析。

再来谈谈双亲委派模型,简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。

 

22. 谈谈 JVM 内存区域的划分,哪些区域可能发生 OutOfMemoryError?

 通常可以把 JVM 内存区域分为下面几个方面,其中,有的区域是以线程为单位,而有的区域则是整个 JVM 进程唯一的。

首先,程序计数器(PCProgram Counter Register)。在 JVM 规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)。

第二,Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。

前面谈程序计数器时,提到了当前方法;同理,在一个时间点,对应的只会有一个活动的栈帧,通常叫作当前帧,方法所在的类叫作当前类。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,成为新的当前帧,一直到它返回结果或者执行结束。JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈。

栈帧中存储着局部变量表、操作数(operand)栈、动态链接、方法正常退出或者异常退出的定义等。

第三,堆(Heap),它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎所有创建的 Java 对象实例都是被直接分配在堆上。堆被所有的线程共享,在虚拟机启动时,我们指定的“Xmx”之类参数就是用来指定最大堆空间等指标。

理所当然,堆也是垃圾收集器重点照顾的区域,所以堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。

第四,方法区(Method Area)。这也是所有线程共享的一块内存区域,用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。

由于早期的 Hotspot JVM 实现,很多人习惯于将方法区称为永久代(Permanent Generation)。Oracle JDK 8 中将永久代移除,同时增加了元数据区(Metaspace)。

第五,运行时常量池(Run-Time Constant Pool),这是方法区的一部分。如果仔细分析过反编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各种信息,还有一项信息就是常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用,所以它比一般语言的符号表存储的信息更加宽泛。

第六,本地方法栈(Native Method Stack)。它和 Java 虚拟机栈是非常相似的,支持对本地方法的调用,也是每个线程都会创建一个。在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制 

 

23. 如何监控和诊断 JVM 堆内和堆外内存使用?

 了解 JVM 内存的方法有很多,具体能力范围也有区别,简单总结如下:  

可以使用综合性的图形化工具,如 JConsoleVisualVM(注意,从 Oracle JDK 9 开始,VisualVM 已经不再包含在 JDK 安装包中)等。这些工具具体使用起来相对比较直观,直接连接到 Java 进程,然后就可以在图形化界面里掌握内存使用情况。

以 JConsole 为例,其内存页面可以显示常见的堆内存和各种堆外部分使用状态。

也可以使用命令行工具进行运行时查询,如 jstat jmap 等工具都提供了一些选项,可以查看堆、方法区等使用数据。

或者,也可以使用 jmap 等提供的命令,生成堆转储(Heap Dump)文件,然后利用 jhat Eclipse MAT 等堆转储分析工具进行详细分析。

如果你使用的是 TomcatWeblogic Java EE 服务器,这些服务器同样提供了内存管理相关的功能。

另外,从某种程度上来说,GC 日志等输出,同样包含着丰富的信息。

这里有一个相对特殊的部分,就是是堆外内存中的直接内存,前面的工具基本不适用,可以使用 JDK 自带的 Native Memory TrackingNMT)特性,它会从 JVM 本地内存分配的角度进行解读。  

 

24. Java 常见的垃圾收集器有哪些?

实际上,垃圾收集器(GCGarbage Collector)是和具体 JVM 实现紧密相关的,不同厂商(IBMOracle),不同版本的 JVM,提供的选择也不同。接下来,我来谈谈最主流的 Oracle JDK。

Serial GC,它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。当然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式下 JVM 的默认选项。

从年代的角度,通常将其老年代实现单独称作 Serial Old,它采用了标记 - 整理(Mark-Compact)算法,区别于新生代的复制算法。

ParNew GC,很明显是个新生代 GC 实现,它实际是 Serial GC 的多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作,下面是对应参数

-XX:+UseConcMarkSweepGC -XX:+UseParNewGC

CMS(Concurrent Mark Sweep GC,基于标记 - 清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间,这一点对于 Web 等反应时间敏感的应用非常重要,一直到今天,仍然有很多系统使用 CMS GC。但是,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC,导致恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。

Parallel GC,在早期 JDK 8 等版本中,它是 server 模式 JVM 的默认 GC 选择,也被称作是吞吐量优先的 GC。它的算法和 Serial GC 比较相似,尽管实现要复杂的多,其特点是新生代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。

G1 GC 这是一种兼顾吞吐量和停顿时间的 GC 实现,是 Oracle JDK 9 以后的默认 GC 选项。G1 可以直观的设定停顿时间的目标,相比于 CMS GCG1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。

G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 regionRegion 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。

G1 吞吐量和停顿表现都非常不错,并且仍然在不断地完善,与此同时 CMS 已经在 JDK 9 中被标记为废弃(deprecated),所以 G1 GC 值得你深入掌握。考点分析

 

 

25,谈谈你的 GC 调优思路?

谈到调优,这一定是针对特定场景、特定目的的事情, 对于 GC 调优来说,首先就需要清楚调优的目标是什么?从性能的角度看,通常关注三个方面,内存占用(footprint)、延时(latency)和吞吐量(throughput),大多数情况下调优会侧重于其中一个或者两个方面的目标,很少有情况可以兼顾三个不同的角度。当然,除了上面通常的三个方面,也可能需要考虑其他 GC 相关的场景,例如,OOM 也可能与不合理的 GC 相关参数有关;或者,应用启动速度方面的需求,GC 也会是个考虑的方面。

基本的调优思路可以总结为:

理解应用需求和问题,确定调优目标。假设,我们开发了一个应用服务,但发现偶尔会出现性能抖动,出现较长的服务停顿。评估用户可接受的响应时间和业务量,将目标简化为,希望 GC 暂停尽量控制在 200ms 以内,并且保证一定标准的吞吐量。

掌握 JVM GC 的状态,定位具体的问题,确定真的有 GC 调优的必要。具体有很多方法,比如,通过 jstat 等工具查看 GC 等相关状态,可以开启 GC 日志,或者是利用操作系统提供的诊断工具等。例如,通过追踪 GC 日志,就可以查找是不是 GC 在特定时间发生了长时间的暂停,进而导致了应用响应不及时。

这里需要思考,选择的 GC 类型是否符合我们的应用特征,如果是,具体问题表现在哪里,是 Minor GC 过长,还是 Mixed GC 等出现异常停顿情况;如果不是,考虑切换到什么类型,如 CMS G1 都是更侧重于低延迟的 GC 选项。

通过分析确定具体调整的参数或者软硬件配置。

验证是否达到调优目标,如果达到目标,即可以考虑结束调优;否则,重复完成分析、调整、验证这个过程。

 

 

26,你了解 Java 应用开发中的注入攻击吗?

 注入式(Inject)攻击是一类非常常见的攻击方式,其基本特征是程序允许攻击者将不可信的动态内容注入到程序中,并将其执行,这就可能完全改变最初预计的执行过程,产生恶意效果。 

下面是几种主要的注入式攻击途径,原则上提供动态执行能力的语言特性,都需要提防发生注入攻击的可能。

首先,就是最常见的 SQL 注入攻击。一个典型的场景就是 Web 系统的用户登录功能,根据用户输入的用户名和密码,我们需要去后端数据库核实信息。

假设应用逻辑是,后端程序利用界面输入动态生成类似下面的 SQL,然后让 JDBC 执行。

Select * from use_info where username = “input_usr_name” and password = “input_pwd”

但是,如果我输入的 input_pwd 是类似下面的文本,

“ or “”=”

那么,拼接出的 SQL 字符串就变成了下面的条件,OR 的存在导致输入什么名字都是复合条件的。

Select * from use_info where username = “input_usr_name” and password = “” or “” = “”

这里只是举个简单的例子,它是利用了期望输入和可能输入之间的偏差。上面例子中,期望用户输入一个数值,但实际输入的则是 SQL 语句片段。类似场景可以利用注入的不同 SQL 语句,进行各种不同目的的攻击,甚至还可以加上“;delete xxx”之类语句,如果数据库权限控制不合理,攻击效果就可能是灾难性的。

第二,操作系统命令注入。Java 语言提供了类似 Runtime.exec(…) API,可以用来执行特定命令,假设我们构建了一个应用,以输入文本作为参数,执行下面的命令:

ls –la input_file_name

但是如果用户输入是 “input_file_name;rm –rf /*”,这就有可能出现问题了。当然,这只是个举例,Java 标准类库本身进行了非常多的改进,所以类似这种编程错误,未必可以真的完成攻击,但其反映的一类场景是真实存在的。

第三,XML 注入攻击。Java 核心类库提供了全面的 XML 处理、转换等各种 API,而 XML 自身是可以包含动态内容的,例如 XPATH,如果使用不当,可能导致访问恶意内容。

还有类似 LDAP 等允许动态内容的协议,都是可能利用特定命令,构造注入式攻击的,包括 XSSCross-site Scripting)攻击,虽然并不和 Java 直接相关,但也可能在 JSP 等动态页面中发生。

   

27. 如何写出安全的 Java 代码?

 这个问题可能有点宽泛,我们可以用特定类型的安全风险为例,如拒绝服务(DoS)攻击,分析 Java 开发者需要重点考虑的点。

DoS 是一种常见的网络攻击,有人也称其为洪水攻击”。最常见的表现是,利用大量机器发送请求,将目标网站的带宽或者其他资源耗尽,导致其无法响应正常用户的请求。

我认为,从 Java 语言的角度,更加需要重视的是程序级别的攻击,也就是利用 JavaJVM 或应用程序的瑕疵,进行低成本的 DoS 攻击,这也是想要写出安全的 Java 代码所必须考虑的。例如:

如果使用的是早期的 JDK Applet 等技术,攻击者构建合法但恶劣的程序就相对容易,例如,将其线程优先级设置为最高,做一些看起来无害但空耗资源的事情。幸运的是类似技术已经逐步退出历史舞台,在 JDK 9 以后,相关模块就已经被移除。

上一讲中提到的哈希碰撞攻击,就是个典型的例子,对方可以轻易消耗系统有限的 CPU 和线程资源。从这个角度思考,类似加密、解密、图形处理等计算密集型任务,都要防范被恶意滥用,以免攻击者通过直接调用或者间接触发方式,消耗系统资源。

利用 Java 构建类似上传文件或者其他接受输入的服务,需要对消耗系统内存或存储的上限有所控制,因为我们不能将系统安全依赖于用户的合理使用。其中特别注意的是涉及解压缩功能时,就需要防范Zip bomb等特定攻击。

另外,Java 程序中需要明确释放的资源有很多种,比如文件描述符、数据库连接,甚至是再入锁,任何情况下都应该保证资源释放成功,否则即使平时能够正常运行,也可能被攻击者利用而耗尽某类资源,这也算是可能的 DoS 攻击来源。

  

28, JVM 优化 Java 代码时都做了什么?

JVM 在对代码执行的优化可分为运行时(runtime)优化和即时编译器(JIT)优化。运行时优化主要是解释执行和动态编译通用的一些机制,比如说锁机制(如偏斜锁)、内存分配机制(如 TLAB)等。除此之外,还有一些专门用于优化解释执行效率的,比如说模版解释器、内联缓存(inline cache,用于优化虚方法调用的动态绑定)。

JVM 的即时编译器优化是指将热点代码以方法为单位转换成机器码,直接运行在底层硬件之上。它采用了多种优化方式,包括静态编译器可以使用的如方法内联、逃逸分析,也包括基于程序运行 profile 的投机性优化(speculative/optimistic optimization)。这个怎么理解呢?比如我有一条 instanceof 指令,在编译之前的执行过程中,测试对象的类一直是同一个,那么即时编译器可以假设编译之后的执行过程中还会是这一个类,并且根据这个类直接返回 instanceof 的结果。如果出现了其他类,那么就抛弃这段编译后的机器码,并且切换回解释执行。

然,JVM 的优化方式仅仅作用在运行应用代码的时候。如果应用代码本身阻塞了,比如说并发时等待另一线程的结果,这就不在 JVM 的优化范畴啦。

 

29, 谈谈常用的分布式 ID 的设计方案?Snowflake 是否受冬令时切换影响?

 首先,我们需要明确通常的分布式 ID 定义,基本的要求包括: 

全局唯一,区别于单点系统的唯一,全局是要求分布式系统内唯一。

有序性,通常都需要保证生成的 ID 是有序递增的。例如,在数据库存储等场景中,有序 ID 便于确定数据位置,往往更加高效。

Snowflake 的官方版本是基于 Scala 语言,Java 等其他语言的参考实现有很多,是一种非常简单实用的方式,具体位数的定义是可以根据分布式系统的真实场景进行修改的,并不一定要严格按照示意图中的设计。

Redis、ZooKeeperMongoDB 等中间件,也都有各种唯一 ID 解决方案。其中一些设计也可以算作是 Snowflake 方案的变种。例如,MongoDB ObjectId提供了一个 12 byte96 位)的 ID 定义,其中 32 位用于记录以秒为单位的时间,机器 ID 则为 24 位,16 位用作进程 ID24 位随机起始的计数序列。

国内的一些大厂开源了其自身的部分分布式 ID 实现,InfoQ 就曾经介绍过微信的seqsvr,它采取了相对复杂的两层架构,并根据社交应用的数据特点进行了针对性设计,具体请参考相关代码实现。另外,百度、美团等也都有开源或者分享了不同的分布式 ID 实现,都可以进行参考。

关于第二个问题,Snowflake 是否受冬令时切换影响?

我认为没有影响,你可以从 Snowflake 的具体算法实现寻找答案。我们知道 Snowflake 算法的 Java 实现,大都是依赖于 System.currentTimeMillis(),这个数值代表什么呢?从 Javadoc 可以看出,它是返回当前时间和 1970 1 1 UTC 时间相差的毫秒数,这个数值与夏 / 冬令时并没有关系,所以并不受其影响。  

 

30, 谈谈 MySQL 支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景?

 所谓隔离级别(Isolation Level),就是在数据库事务中,为保证并发数据读写的正确性而提出的定义,它并不是 MySQL 专有的概念,而是源于ANSI/ISO制定的SQL-92标准。  

每种关系型数据库都提供了各自特色的隔离级别实现,虽然在通常的定义中是以锁为实现单元,但实际的实现千差万别。以最常见的 MySQL InnoDB 引擎为例,它是基于 MVCCMulti-Versioning Concurrency Control)和锁的复合实现,按照隔离程度从低到高,MySQL 事务隔离级别分为四个不同层次:

读未提交(Read uncommitted),就是一个事务能够看到其他事务尚未提交的修改,这是最低的隔离水平,允许脏读出现。

读已提交(Read committed),事务能够看到的数据都是其他事务已经提交的修改,也就是保证不会看到任何中间性状态,当然脏读也不会出现。读已提交仍然是比较低级别的隔离,并不保证再次读取时能够获取同样的数据,也就是允许其他事务并发修改数据,允许不可重复读和幻象读(Phantom Read)出现。

可重复读(Repeatable reads),保证同一个事务中多次读取的数据是一致的,这是 MySQL InnoDB 引擎的默认隔离级别,但是和一些其他数据库实现不同的是,可以简单认为 MySQL 在可重复读级别不会出现幻象读。

串行化(Serializable),并发事务之间是串行化的,通常意味着读取需要获取共享读锁,更新需要获取排他写锁,如果 SQL 使用 WHERE 语句,还会获取区间锁(MySQL 以 GAP 锁形式实现,可重复读级别中默认也会使用),这是最高的隔离级别。

至于悲观锁和乐观锁,也并不是 MySQL 或者数据库中独有的概念,而是并发编程的基本概念。主要区别在于,操作共享数据时,悲观锁即认为数据出现冲突的可能性更大,而乐观锁”则是认为大部分情况不会出现冲突,进而决定是否采取排他性措施。

反映到 MySQL 数据库应用开发中,悲观锁一般就是利用类似 SELECT … FOR UPDATE 这样的语句,对数据加锁,避免其他事务意外修改数据。乐观锁则与 Java 并发包中的 AtomicFieldUpdater 类似,也是利用 CAS 机制,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本判断。

我认为前面提到的 MVCC,其本质就可以看作是种乐观锁机制,而排他性的读写锁、双阶段锁等则是悲观锁的实现。

有关它们的应用场景,你可以构建一下简化的火车余票查询和购票系统。同时查询的人可能很多,虽然具体座位票只能是卖给一个人,但余票可能很多,而且也并不能预测哪个查询者会购票,这个时候就更适合用乐观锁。   

希望这些面试题对你有所帮助,如果有所帮助请帮我点个赞哦!!!!

关注我,给你更多关怀,么么哒!!!!!! 

猜你喜欢

转载自blog.csdn.net/wk_beicai/article/details/106379389