Java面试指南

121 阅读13分钟

01、String 为什么是 final


  1、String 类是一个不可变类,被 final 修改的类不能被继承,这样提高了 String 类使用的安全性。
  2、String 类的主要变量 value[]都被设计成private final 的,这样在多线程时,对 String对象的访问是可以保证安全。
  3、JVM 对 final 修饰的类进行了编译优化,设计成 final,JVM 不用对相关方法在虚函数表中查询,直接定位到 String 类的相关方法调用,提高了执行效率。


02、Class.forName 和 和 ClassLoader 的区别


  Class.forNameClassLoader 都是用来装载类的,对于类的装载一般为分三个阶段加载、链接、编译,它们装载类的方式是有区别。
  首先看一下 Class.forName(..),forName(..)方法有一个重载方法 forName(className,boolean,ClassLoader)。 它有三个参数,第一个参数是类的包路径,第二个参数是 boolean类型,为 true 地表示 Loading 时会进行初始化,第三个就是指定一个加载器;当你调用class.forName(..)时,默认调用的是有三个参数的重载方法,第二个参数默认传入 true,第三个参数默认使用的是当前类加载时用的加载器。
  ClassLoader.loadClass()也有一个重载方法,从源码中可以看出它默认调的是它的重载方法 loadClass(name, false),当第二参数为 false 时,说明类加载时不会被链接。 这也是两者之间最大区别,前者在加载的时候已经初始化,后者在加载的时候还没有链接。如果你需要在加载时初始化一些东西,就要用 Class.forName 了,比如我们常用的驱动加载,实际上它的注册动作就是在加载时的一个静态块中完成的。所以它不能被 ClassLoader 加载代替。


03、进程和线程的区别


image.png

  定义:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
  特点:
  1、一个进程可以拥有很多个线程,但每个线程只属于一个进程。
  2、线程相对进程而言,划分尺度更小,并发性能更高。
  3、进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
  4、线程必须依赖应用,在应用中调度,每个线程必须执行的入口、出口、执行序列,线程是不能够独立存在运行的。
  5、进程是资源分配的基本单位,线程是处理机调度的基本单位,所有的线程共享其所属进程的所有资源与代码。
  6、多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。


04、Java 的引用类型有哪几种


  Java 自从 JDK1.2 版本开始,引入四种引用的类型,它们由强到弱依次是:强引用(StongReference) 、软引用(SoftReference)、弱引用(**WeakReference **)、虚引用(PhantomReference) ,它们的各自特点及使用领域。
  强引用: 代码中最常用看到的 Object o = new Object();这里就是强引用,只要引用在,GC 就不回收它,如果 JVM 内存空间不足会抛出 OutOfMemoryError。
  软引用:通常用描述一些有用但不是必需的对象,如果 JVM 内存不足时,会被 GC,通常被用来作为一些缓存模块的设计,而且不容易 OOM。
  弱引用:比软引用还低级别的引用,软引用一般是内存不足时回收,而弱引用只要被 GC 扫描线程发现就会回收掉,即便是 JVM 内存还充足的情况下。
  虚引用:如其名,虚无般的存在,完全不会影响对象的生命周期,如果一个对象仅持有虚引用,就如同没有引用一样,可能随时被回收掉,一般会与强用队列关联使用,一般只用于对象回收的事件传递。


05、Http报文结构


image.png

image.png
  上图为请求报文,包括“请求行①②③”、“请求头部④”、“请求包体⑤”,请求行①②③之间是用空格隔开的,面试回答时捡主要的说,没必要把所有的参数都介绍一下。
  请求报文包括 “请求行”“ 请 求头部”“ 请求包体”;请求行中主要包括:请求方式、请求地址、Http 版本,它们之间用空格分开。请求头部主要包括 : Accept: 告 诉 服 务 端 客 户 端 接 受 什 么 类 型 的 响 应 ;Accept-Language:客户端可接受的自然语言;User-Agent:请求端的浏览器以及服务器类型;Accept-Encoding:客户端可接受的编码压缩格式; Accept-Charset:可接受的应答的字符集;Host:请求的主名,允许多个域名同处一个 IP 地址,即虚拟主机;connection:连接方式(close 或 keep-alive);Cookie:存储于客户端扩展字段,向同一域名的服务端发送属于该域的 cookie;空行:最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头;
  请求包体:也是请求正文,业务报文。
  响应报文包括**“状态行”** 、“响应头部”“响应包体”;状态行中包括:Http 协议版本、状态码以及状态码描述(常用状态码及描述,500:服务器内部错误,404:页面找不到,200OK:表示请求成功返回,403:服务器收到请求但拒绝服务;其它的 1xx,2xx,3xx,4xx,5xx系,大家可以网上查找一下,蛮重要,能说出来就行)。
  响应头部,Server:响应服务器类型;Content-Type:响应数据的文档类型;Cache-Control:响应输出到客户端后,服务端通过该报文头属告诉客户端如何控制响应内容的缓存。
  响应包体,真正的业务报文,也就是请求期望的返回数据。


06、Http 如何 处理长连接


  Http目前有两个版本分别是 Http1.0 和 Http1.1,Http1.0 默认是短连接,如果需要长连接支持,需要加上Connection: Keep-alive;Http1.1 的版本默认是支持长连接请求的方式,可以在抓取的请求中看到 Connection: Keep-alive,如果不想用长连接,需要在报文首部加上 Connection:close;对于默认的长连接可以通过 Keep-Alive:timeout=N 进行超时时间设置。
  [追问]对长连接数据传输完成的识别:
  第一种:通过 Content-Length 指示的大小,如果传的报文长度达到了 Content-Length,则认为传输完成。
  第二种:动态请求生成的文件中往往不包含 Content-Length,往往是通过分块传输入,服务器不能预先判断文件大小,这里要通过 Transfer-Encoding:chunked 模式来传输数据。Chunked 是按块进行数据传输的,这时候就要根据 chunked 编码来判断,chunked 编码的数据在最后有一个空 chunked 块,表明本次传输数据结束。


07、TCP三次握手和四次挥手


image.png
image.png
  图来源于网上,如果要看懂这个图,先来了解一下几个简单的概念:SYN 表示建立连接,FIN 表示关闭连接,ACK 表示响应,序号是随机产生的但作用很大,这里不详细说了,这几个关键字在面试的时候有必要先解释一下。
  TCP 建立连接和断开连接的操作有几个很重要的关键字,分别:SYN 表示请求建立连接、ACK 表示响应、FIN 表示关闭连接请求、随机序列 会 随传送报文 的 字节数增加 (SYN 、FIN 都) 算位的,即便没有字节传送,序列也会增加)
  TCP 建立连接的三次握手, 第一次握手:主机 A 发送位码为 syn=1,随机产生 seq =200的数据包到服务器,主机 B 由 SYN=1 知道,A 要求建立联机;
  第二次握手:主机 B 收到请求后要确认联机信息,向 A 发送 ack 确认序列=(主机 A的 seq+1),syn=1,ack=1,随机产生 seq=500 的包;
  第三次握手:主机 A 收到后检查 ack 确认序列是否正确,即第一次发送的 seq number+1,以及位码 ack 是否为 1,若正确,主机 A 会再发送 ack number=(主机 B 的seq+1),ack=1,主机B收到后确认 seq 值与 ack=1 则连接建立成功。
  【三次握手总结】 主机A发 syn 给主机B,主机B回 ack,syn ,主机 A 回 ack ,三次握手,连接成功。
  四次挥手, 第一次挥手:主机A发送一个FIN,用来关闭客户A到服务器B的数据传送。
  第二 次挥手:主机B收到这个FIN,它发回一个ACK,确认序号为收到的序号加 1。和SYN一样,一个FIN将占用一个序号。
  第三 次挥手:主机B关闭与主机A的连接,发送一个 FIN 给主机 A。


08、线程启动用 start 方法还是run


  下面是 JDK 中 start()方法的源码,可以看到 start()调用的是 start0(),而start0()是一个 native 方法,通过注释可以知道,它的作用主要是为线程分配系统资源的;而 run 只是一个普通的方法,所以线程的启动是通过 start 方法实现的。

public synchronized void start() {
		if (threadStatus != 0 || this != me)
			throw new IllegalThreadStateException();
		group.add(this);
		start0();
		if (stopBeforeStart) {
			stop0(throwableFromStop);
		}
	}
	private native void start0();

09、ThreadLocal 的基本原理


  下面两个问题是一个同学面试的时候遇到的,网上也能看到,问题不难但平时不留意也不太容易回答。1、每个线程的变量副本是存储在哪里的?2、threadlocal 是何时初始化的?变量副本是如何为共享的那个变量赋值的?回答这样的问题,建议大家看一下 JDK相关 threadlocal 部分的源码,下面只引用部分源码来解释说明。
  ThreadLocal 并非是线程的本地实现,而是线程的本地变量,它归附于具体的线程,为每个使用该变量的线程提供一个副本,每个线程都可以独立的操作这个副本,在外面看来,貌似每个线程都有一份变量。线程的变量存在哪里,这里可以结果 ThreadLocal 的源码说明,这里看一下 get 实现

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

  在 get 方法中,会先获得当前线程对象,然后传到 getMap()中获取 ThreadLocalMap对象,我们要的变量副本是从 ThreadLocalMap 对象中取出来的,可见每个线程的变量副本是保存在 ThreadLocalMap 对象里,而跟一下代码可以看到 ThreadLocalMap 是在 Thread中声明实现的,所以 每个线程的变量副本就保 存 在相应线程的 ThreadLocalMap 对象中。
  第二个问题,可以理解 ThreadLocal 如何把变量的副本复制并且初始化的(声明和初始化),这里看一下源码中的 set 方法实现

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

  当第一次调用 set 方法时,获取的 ThreadLocalMap 对象为空,这里会调用 createMap方法创建一个 ThreadLocalMap 对象,并且完成相应的初始,将 value 值存放进去。后面再次调用将会直接从线程中获取ThreadLocalMap 对象,然后将副本保存进去。


10、JVM内存泄露的原因有哪些


  这个问题看似简单,却用一个问题考察了 JVM 很多个相关的知识点,回答这个问题你首先要了解 JVM 的结构、对象的分配与存储、GC 的原理等,但你看到此的时候如果对上面的知识点还不是很熟悉的话,先翻开其相关知识点,之前都已经谈到过,然后这个问题就容易回答了。
  JVM 结构上一般分为堆内存、栈内存、方法区,内存泄露可能会发生在任何一个位置;在 JVM 划分上方法区通常划给堆,所以这两块可以一起。而栈内存通常是用来存放普通变量和对象引用,回收速度速度快,一般不会造成内存泄露,一旦溢出通常是栈内存大小分配不合理,或者可能显示的将对象空间分配到栈内存来追求效率造成的。下面我们重点探讨堆内存的溢出(根据面试的场景来判断有没有谈栈内存这块)。
  参考: 首先造成 Java JVM 泄露的主要原因:JVM 未及时的对垃圾进行回收造成的;当对象失去引用且持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。造成这种对象无法及时释放导致内存泄露的原因,可以简单的为归分两类。
  一是基 于设计方面 :1、对应用加载数据级别判断失误,从而导致 JVM 内存分配不合理(企业单机部署应用常见到)。2、应用请求的常连接设计,常连接会一直占用后台资源,不能及时释放。3、数据库操作时,存在很多耗时连接,导致大量资源不能释放。4、大量的监听设计等。
  二 是基于开发方面:1、大量静态变量的使用(静态变量的生成周期与应用一致),如果静态引用指向的是集合或者数据,会一直占用资源。2、不合理的方法使用,比如 jdk6之前的 substring 就可能导致内存泄露。3、数据库连接未能及时关闭,刚工作不久的同学容易忽略。4、单例模式使用,单例通常用来加载资源信息,但如果加载信息里有大量的集合、数组等对象,这些资源会一直驻留内存中,不易释放。5、在循环中创建复杂对象、一次性读取加载大量信息到内存中,都有可能造成内存泄露。


11、OO的设计原则


  面向对象设计原则通常归结为五大类,
  第 一 “单 一职责原则” (SRP):一个设计元素只做一件事,不要随意耦合,多管闲事;
  第二 “开 放 封闭 原则” (OCP):对变更关闭、对扩展开放,提倡基于接口设计,新的需要最好不要去变更已经完成的功能,可以灵活通过接口扩展新功能;
  第三 “里氏 替换原则” (LSP):子类可以替换父类并且出现在父类能够出现的任何地方,这个也是提倡面向接口编程思想的;
  第四 “依赖 倒置原则” (DIP):要依赖于抽象,不要依赖于具体,简单的说就是面对抽象编程,不要过于依赖于细节;
  第 五 “接口 隔离原则” (ISP):使用多个专门的接口比使用单个接口要好,在设计中不要把各种业务类型的东西放到一个接口中造成臃肿。


更多更新请关注........