注意:只整理了与Java相关的问题
第一轮
1. Java四种引用类型、内存模型
四种引用类型
判定对象是否存活都和“引用”离不开关系,在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种。
-
强引用是最传统的“引用”定义,是指在程序代码之中普遍存在的引用赋值,即类似
Object obj = new Object()
这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 -
软引用是用来描述一些还有用,但非必须的对象。只要被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出的异常。在JDK1.2版之后提供了
SoftReference
类来实现软引用。 -
弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象智能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2版之后提供了
WeakReference
类来实现弱引用。 -
虚引用也被称为“幽灵引用”或“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存空间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK1.2版之后提供了
PhantomReference
类来实现虚引用。
内存模型
- 程序计数器
- 程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
- 在Java虚拟机的概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖它来完成。
- 每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,称这类内存区域为“线程私有”的内存。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(native)方法,这个计数器值则应为空(Undefined)。
- 此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
- Java虚拟机栈
- Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,其生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
- 局部变量表存放了编译器可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,1个变量槽占用多少比特有具体的虚拟机自行决定。
-
本地方法栈
虚拟机栈为虚拟机执行Java方法(也就是字节码)提供服务,本地方法栈为虚拟机使用到的本地方法服务。
有的虚拟机(如HotSpot)直接将本地方法栈和虚拟机栈合二为一。 - 堆
- Java堆(Java Heap)是虚拟机所管理的内存中最大的一块,被所有线程共享,在虚拟机启动时创建。
- Java堆的唯一目的就是存放对象实例,Java世界“几乎”所有的对象实例都在这里分配内存。
- Java堆是垃圾收集器管理的内存区域,也被称作“GC堆”(Garbage Collected Heap)。
- 根据《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。
- 方法区
- 方法区(Method Area)也是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
- 运行时常量池
- 运行时常量池(Runtime Constant Pool)是方法区的一部分。
- Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池表(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
- 直接内存
- 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分。
- 在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提升性能,因为避免了在Java堆和Native堆中来回复制数据。
- 本机直接内存的分配不会受到Java堆大小的限制。
2. http常见响应码、与https的区别
http常见响应码
- 1XX:信息,服务器接受到请求,需要请求者继续执行操作(100:Continue。客户端应继续其请求)
- 2XX:成功,操作被成功接受并处理(200:Ok正常)
- 3XX:重定向,需要进一步的操作以完成请求(301:Moved Permanently,永久重定向,302:Found,暂时重定向)
- 4XX:客户端错误,请求包含语法错误或者无法完成请求 (400:Bad Request, 404:Not Found,403:Forbidden)
- 5XX:服务器错误,服务器在处理请求的过程中发生了错误。(500:Internal Server Error,502:Bad Gateway:作为网关或者代理服务器尝试执行请求时,从远程服务器接受到了无效的响应)
http与https的区别
一个明文传输,一个密文传输,并且http的端口号为80,https的端口号为443。
3. 哈希映射(HashMap)的实现原理
–》JDK 1.7 : Table数组+ Entry链表;
–》JDK1.8 : Table数组+ Entry链表/红黑树;(为什么要使用红黑树?)
- HashMap 通过key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这⾥的 n 指的是数组的⻓度),
- 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
- 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
- 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
4. Java中锁的类型
5. 常见设计模式,写一个单例,其中volatile的作用
- 工厂模式:定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子类工厂内。
-
单例模式:一个类只能创建一个实例。
双重校验锁实现对象单例(线程安全)
public class Singleton {
public volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
//先判断对象是否已经实例化过,没有实例化过才进入加锁代码
if (instance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
由于JVM具有指令重排的特性,指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。使用volatile
可以禁止JVM的指令重排,保证在多线程环境下也能正常运行。
- 原型模式:利用一个原型对象来执行我们所要创建对象的实例,通过复制这个对象的方法来获得与该对象一模一样的对象实例。
- 代理模式:就是一个人或者机构代表另一个人或者机构采取行为。
- 适配器模式:将一个类的接口转换成客户希望的另一个接口,使得原来由于接口不兼容而不能一起工作的那些类可以一起工作。
6. 手撕算法 两个数组中最大公共子数组的长度
leetcode718题 https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/
1.暴力法;2.动态规划法
class Solution {
public int findLength(int[] A, int[] B) {
int n = A.length, m = B.length;
int[][] dp = new int[n + 1][m + 1];
int ans = 0;
for (int i = n - 1; i >= 0; i--) {
for (int j = m - 1; j >= 0; j--) {
dp[i][j] = A[i] == B[j] ? dp[i + 1][j + 1] + 1 : 0;
ans = Math.max(ans, dp[i][j]);
}
}
return ans;
}
}
7. 在京东研发做的主要工作
8. synchronized关键字工作原理
synchronized
关键字解决的是多个线程之间访问资源的同步性, synchronized
关键字可以保证被它修饰的⽅法或者代码块在任意时刻只能有⼀个线程执⾏。synchronized
同步语句块经过Javac编译后,会在同步块的前后分别形成 monitorenter
和 monitorexit
指令。这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。根据《Java虚拟机规范》的要求,在执行 monitorenter
指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行 monitorexit
指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。
9. try-catch中try里有return,此时如果能走到return,finally块中还会不会执行
-
try
块:用于捕获异常,其后可接零个或多个catch
块,如果没有catch
块,则必须跟一个finally
块。 -
catch
块:用于处理try
捕获到的异常。 -
finally
块:无论是否捕获或处理异常,finally
块的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。
在以下3种特殊情况下,finally块
不会被执行:
- 在
try
或finally
块中用了System.exit(int)
提出程序。但是,如果System.exit(int)
在异常语句之后,finally
还是会被执⾏。 - 程序所在的线程死亡。
- 关闭CPU。
第二轮
1. http头部信息、如何实现断点续传
HTTP的头域包括通用头,请求头,响应头和实体头四个部分。
request Header:
- GET /sample.Jsp HTTP/1.1 //请求行
- Host: www.uuid.online/ //请求的目标域名和端口号
- Origin: http://localhost:8081/ //请求的来源域名和端口号 (跨域请求时,浏览器会自动带上这个头信息)
- Referer: https:/localhost:8081/link?query=xxxxx //请求资源的完整URI
- User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36 //浏览器信息
- Cookie: BAIDUID=FA89F036:FG=1; BD_HOME=1; sugstore=0 //当前域名下的Cookie
- Accept: text/html,image/apng //代表客户端希望接受的数据类型是html或者是png图片类型
- Accept-Encoding: gzip, deflate //代表客户端能支持gzip和deflate格式的压缩
- Accept-Language: zh-CN,zh;q=0.9 //代表客户端可以支持语言zh-CN或者zh(值得一提的是q(0~1)是优先级权重的意思,不写默认为1,这里zh-CN是1,zh是0.9)
- Connection: keep-alive //告诉服务器,客户端需要的tcp连接是一个长连接
Response Header:
- HTTP/1.1 200 OK // 响应状态行
- Date: Mon, 30 Jul 2018 02:50:55 GMT //服务端发送资源时的服务器时间
- Expires: Wed, 31 Dec 1969 23:59:59 GMT //比较过时的一种验证缓存的方式,与浏览器(客户端)的时间比较,超过这个时间就不用缓存(不和服务器进行验证),适合版本比较稳定的网页
- Cache-Control: no-cache // 现在最多使用的控制缓存的方式,会和服务器进行缓存验证,具体见博文”Cache-Control“
- etag: "fb8ba2f80b1d324bb997cbe188f28187-ssl-df" // 是服务器发来的对一些数据的签名,下次请求时,如果服务器上这些数据发生了变化,和这个etag不相同就返回新的资源内容
- Last-Modified: Fri, 27 Jul 2018 11:04:55 GMT //是服务器发来的当前资源最后一次修改的时间,下次请求时,如果服务器上当前资源的修改时间大于这个时间,就返回新的资源内容
- Content-Type: text/html; charset=utf-8 //如果返回是流式的数据,我们就必须告诉浏览器这个头,不然浏览器会下载这个页面,同时告诉浏览器是utf8编码,否则可能出现乱码
- Content-Encoding: gzip //告诉客户端,应该采用gzip对资源进行解码
- Connection: keep-alive //告诉客户端服务器的tcp连接也是一个长连接
2. 进程与线程的区别
-
根本区别:进程是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,系统运⾏⼀个程
序即是⼀个进程从创建,运⾏到消亡的过程;线程是⼀个⽐进程更⼩的执⾏单位,⼀个进程在其执⾏的过程中可以产⽣多个线程,与进程不同的是同类的多个线程共享同⼀块内存空间和⼀组系统资源。 - 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
- 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
- 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
- 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
3. JVM常见的GC算法
三种GC算法
-
标记-清除算法
- 算法分为“标记”和“清除”两个阶段:首先标记处所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。
- 缺点:1.执行效率不稳定,随对象数量增长而降低;2.内存空间的碎片化问题,较大对象无法找到足够的连续空间。
-
标记-复制算法
- 为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,1969年Fenichel提出了一种称为“半区复制”的垃圾回收算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。优点:实现简单,运行高效;缺点:可用内存只有一半,空间浪费太多。
- IBM曾有一项专门研究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1:1的比例来划分新生代的内存空间。
- 在1989年,Andrew Appel针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略,现在称为“Appel式回收”。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存时只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,也即每次新生代中可用内存空间为整个新生代容量的90%。只有10%的空间被浪费掉。
- 任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其它内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代。
-
标记-整理算法
- 标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
- 针对老年代对象的存亡特征,1974年Edward Lueders提出标记-整理算法,不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
- 是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,但是从整个程序的吞吐量来看,移动对象会更划算。(这里的吞吐量是赋值器与收集器的效率总和)
- HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的;而关注延迟的CMS收集器则是基于标记-清除算法的;另外,还有一种“和稀泥式”解决方法可以不再内存分配和访问上增加太大额外负担,做法是让虚拟机平时多数时间采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。前面提到的基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。