Java面试-基础10问

1、说说JVM内存模型、GC垃圾回收机制,你们项目用的垃圾是什么?是否参与过JVM调优?

JVM即Java虚拟机,是Java程序实现跨平台应用的关键。由开发软件(记事本、Sublime、Eclipse、IDEA)写好的.java后缀文件(程序),经过编译器编译成.class后缀文件(程序),各平台的JVM将.class文件(程序)编译成对应平台的操作指令,JVM即Java虚拟机就是 将字节码文件编译成对应平台指令的编译器。

在这里插入图片描述

JVM内存模型分为线程共享和线程独享的两大部分:
	1、线程独享:既然是线程独享,就代表每个线程都有以下三个区域,并伴随线程的开始而创建,线程的结束而销毁。
		1、虚拟机栈:是每个线程方法执行的内存模型。当前线程每开始执行一个方法,会在自己的虚拟机栈中压入一个栈帧,方法执行结束后回弹出此栈帧,每个栈帧保存 局部变量表、操作数栈、动态链接、方法出口。
		2、本地方法栈:是每个线程内部Native方法执行的内存模型。当前线程执行方法时,必不可少也会执行一些虚拟机的内部方法,这些方法我们称之为Native方法,本地方法栈就是当前线程这些方法执行的内存模型。
		3、程序计数器:是用来记录当前线程需要执行的下一条字节码指令。保证线程切换后,每一个线程都能回到正确的执行位置。
			1、当前线程执行方法时,程序计数器记录的是字节码指令的地址,执行Nativa方法时,程序计数器值为空。
			2、程序计数器不会发生OOM(内存溢出)。
	2、线程共享:既然是线程共享,就代表不随线程的生命周期而存在和销毁,所以要尽可能的优化和合理利用。
		4、堆:是虚拟机内存当中最大的内存区域,用来存放对象实例及数组。它是线程共享区域,设置大小后就要合理利用,合理的垃圾回收(GC),避免OOM(内存溢出)、ML(内存泄漏)
			1、内存溢出:俗称内存不够,指程序申请的内存大小超出了系统所能分配的内存大小。堆内存溢出:对象实例过大、对象实例过多;栈内存溢出:方法调用过多(循环、递归)
			2、内存泄漏:俗称“占着茅坑不拉屎”,指系统分配的内存空间迟迟不能回收再利用,最后也会导致内存溢出。常见的内存泄漏:静态的集合容器、各种链接,数据库、网络、IO等。

在这里插入图片描述

如上图所示,从垃圾回收的角度划分,堆可分为新生代(1/3)和老年代(2/3)两部分,新生代又分为Eden区(8/10)、From Survivor区(1/10)、To Survivor区(1/10),堆内存大小可以随着空余大小而调节,-XX:MinHeapFreeRation=来指定,当空余大小为多少是调节到最大或最小堆内存,一般情况为了避免频繁调整,Xms和Xmx都会设置成一样。
	5、方法区:是用来存放已被虚拟机加载类型信息、常量、静态变量、即时编译器编译后的代码缓存等。方法区虽然不在堆内,但也会被垃圾回收,JDK1.6、1.7、1.8的内存区域变化也主要体现在这里。

在这里插入图片描述

JDK1.6时,使用永久代来实现方法区

在这里插入图片描述

JDK1.7时,将字符串常量池放在了堆上

在这里插入图片描述

JDK1.8时,彻底去掉了永久代,在直接内存中开辟一块元空间的区域用来保存类常量池、运行时常量池,取消代永久代是因为永久代容易造成内存溢出。

GC:

GC即垃圾回收的意思(Garbage Collection),将虚拟机内存堆中无用的对象、数组进行清理,从而释放内存,避免内存溢出、内存泄漏,程序重启。

常见的垃圾回收算法有以下几种:

1、标记-清除算法:分为标记和清除两个阶段,标记阶段标记出需要回收的对象,清除阶段统一回收被标记的对象。标记和清除的效率都不高,且容易留下大量不连续的内存碎片,无法给大对象分配内存空间,内存利用率低,造成再次垃圾收集。一般用在 老年代 垃圾回收。
2、标记-整理算法:分为标记、整理、清除三个阶段,标记阶段标记出存活的对象,整理阶段将存活的对象压缩到内存的一边,清除阶段将存活边界以外的内存空间都清理一遍。一般用在 老年代 垃圾回收。
3、复制算法:分为标记、复制、清除三个阶段,标记阶段标记出存活的对象,复制阶段将存活的对象复制到一块新的内存空间,清除阶段将原来内存区域彻底清空。如果有大量存活对象,复制效率将会很低,选用复制算法一般要将内存一分为二,空间利用率不高。
	过程:一般用在新生代垃圾回收。新生代第一次垃圾回收,会将标记出的Eden区存活的对象复制到From Survivor区,然后清理Eden区;第二次垃圾回收,会将标记出的Eden区存活的对象和From Survivor区存活的对象复制到To Survivor区,然后清理Eden区和From Survivor区;第三次垃圾回收,会将标记出的Eden区存活的对象和To Survivor区存活的对象复制到From Survivor区,再清理Eden区和To Survivor区;所以每一次完整的标记、复制、清除都只清理Eden区+一个Survivor区,每经过这样一次Young GC/Minor GC后存活的对象,会将对象头里占4位的分代年龄加一,占4位所以默认最大值为15,超过15就从新生代移入老年代。
4、分代回收算法:不是指具体的一种垃圾回收算法,而是将以上算法的整合。新生代对象存活数量较少,使用复制算法;老年代对象存活数量较多,使用标记-清除、标记-整理算法。

常见的GC有以下几种:

Minor GC/Young GC:指新生代的GC过程,Eden区满了就会触发一次Minor GC/Young GC,一般使用复制算法。
Major GC/Full GC:指老年代的GC过程,新生代经过Minor GC/Young GC后,有新的存活对象进入老年代,而老年代的空间又不够用了才会触发Major GC,Minor GC/Young GC+Major GC就等同于Full GC,所以Major GC就等同于Full GC,Major GC一般使用标记-清除、标记-整理算法。
上面提到永久代,由于GC不会清理这部分内存,所以容易OOM(内存溢出),JDK1.8的直接内存是本地内存,而不在是虚拟机内存中。

判断一个对象是否存活的算法:

引用计数法:即给对象添加一个计数器,有地方引用此对象计数器就加一,引用失效计数器就减一,计数器为零就是死亡对象,不为零就代表对象依旧存活。如果有两个对象循环引用,计数器始终大于零,两个对象将永远无法回收。
可达性分析法:即以一些被称为“GC Roots”的对象为起点,从这些起点开始往下搜索,搜索所走过的路径被称为引用链。当一个对象没有和任何引用链相连,即称为该对象不可达,认为该对象死亡。

哪些对象可以作为GC Root:

1、虚拟机栈中,栈帧保存的本地变量表中引用的对象(方法执行引用的对象)
2、方法区中类静态属性引用的属性(少用静态集合容器,避免OOM)
3、方法区中常量引用的对象
4、本地方法栈中JNI引用的对象
总结就是,当前线程正在执行的方法的引用数据类型的参数、局部变量、临时值。
注意:一个对象要被回收,至少要经过两次标记,如果对象在第二次标记之前重新连接上GC Roots,那么它将在第二次标记中被移出回收队列,从而复活。

常见的垃圾回收器有以下几种:

在这里插入图片描述

串行:单个线程执行垃圾回收操作,工作线程暂停,直至垃圾回收操作结束。
并行:多核CPU多个线程同时执行垃圾回收操作,工作线程暂停,直至垃圾回收操作结束。

在这里插入图片描述

并发:多核CPU,垃圾回收线程和工作线程同时执行,从而避免STW。
STW:stop the word是指虚拟机暂停所有工作线程,只有GC线程运行的一种状态。
吞吐量:代码程序运行时间/代码程序运行时间➕垃圾回收时间
暂停时间:执行垃圾回收时,工作线程被暂停的时间。
串行回收器:单线程垃圾回收器,与工作线程交替运行
并行回收器:多线程垃圾回收器,但与工作线程交替运行
并发回收器:单/多线程垃圾回收器,与工作线程交替或并行运行

在这里插入图片描述

1、Serial垃圾收集器(串行回收器):新生代垃圾回收器,采用复制算法,单线程进行垃圾回收时会暂停所有工作线程,直到垃圾回收线程结束(STW)
2、Serial Old垃圾收集器(串行回收器):老年代垃圾回收器,采用标记-整理算法,单线程进行垃圾回收时会暂停所有工作线程,直到垃圾回收线程结束(STW)

在这里插入图片描述

3、ParNew垃圾收集器(并行回收器):Serial垃圾收集器的多线程版本,新生代垃圾回收器,采用复制算法,多线程进行垃圾回收时会暂停所有工作线程,直到垃圾回收线程结束(STW)

在这里插入图片描述

4、Parallel scavenge垃圾收集器(并行回收器):ParNew垃圾收集器的加强版,重点是可控制吞吐量,吞吐量优先的垃圾回收器,高效利用CPU,尽快完成程序任务。新生代垃圾回收器,采用复制算法,JDK1.8默认的垃圾回收器。
5、Parallel old垃圾收集器(并行回收器):Serial Old垃圾收集器的多线程版本,采用标记-整理算法,多线程进行垃圾回收时会暂停所有工作线程,直到垃圾回收线程结束(STW)

在这里插入图片描述

6、CMS垃圾收集器(并发回收器):老年代垃圾回收器,采用标记-清理算法,重点是低延迟,缩短STW的时间
	初始标记:会有短暂的STW,目的是标记出与GC Roots直接相连的对象,这个STW时间很短。
	并发标记:从GC Roots直接相连的对象开始遍历整个对象图,虽然耗时较久,但与工作线程一起并发运行。
	重新标记:由于并发标记时垃圾回收线程与工作线程一起并发运行,重新标记用来修正一部分对象的标记记录,初始标记STW时间 < 重新标记STW时间 < 并发标记STW时间,
	并发清理:将标记死亡的对象进行清理,与工作线程一起并发运行。
	优点:低延迟、并发收集
	缺点:标记-清理算法会导致内存碎片、与工作线程并发运行,所以对CPU资源较敏感,吞吐量会降低、无法处理并发标记导致的浮动垃圾

在这里插入图片描述
在这里插入图片描述

7、G1垃圾收集器(并行+并发回收器):新生代+老年代垃圾回收器,在可控的的STW时间内,还尽可能高吞吐量。
	1、首次将堆分割成不连续的Region区域,每个Region区大小在1~32MB之间的2的N次幂,不同的Region区来表示Eden、Survivor区、老年代,每个Region还有一个Remember Set用于记录其他区域对本区域的引用。
	2、G1会在后台维护一个优先列表,避免整堆扫描收集垃圾,回收垃圾最大量,回收价值最高的区域,所以叫垃圾优先收集器(Garbage First)
	3、JDK9成为默认垃圾收集器,取代了PerNew+CMS+SerialOld组合,Parallel+ParallelOld组合,但也不能完全避免STW
	4、Region与Region之间采用复制算法,整体上是标记-整理算法,所以在大内存上G1收集器更占优势(6~8G间)
	5、HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作
	过程:Eden区多线程并行回收,工作线程STW;老年代并发标记;混合回收(Mixed GC)

在这里插入图片描述

JVM调优

1、真正的虚拟机参数参数调优确实没经历过,但一些简单参数有一点了解,像Xms最小堆内存、Xmx最大堆内存、Xmn新生代内存大小、XXFreeRation堆空闲比率等,现实中最近开发的项目有的部署在K8s+Docker的联想自研Xcloud平台,每个微服务部署几个Docker节点,结合平台的可视化监控和自动预警机制,排查过一些节点重启、CPU飙高的线上问题。重启多半是因为OOM,找到重启的时间点再结合日志分析,导出数据大对象为罪魁祸首,解决方法是凡是导出数据前后端都加防重复提交,以及改进代码像JPA提供的Stream流对象。CPU飙高一般定位方法的递归调用、数据库一个事物内大量插入、sql满查询等连接超时、连接数耗尽等问题,解决办法是先杀死相应进程,再进行增大连接数、sql调优等操作。
2、最近的项目也有直接Jekins打包部署在服务器上的,很多项目都部署在同一台服务器上,上面的Xms、Xmx等参数肯定根据服务器内存大小来调整,一般都是2048MB。
3、服务器上排查问题我知道的命令有top显示每个进程的CPU使用率、vmstat监控内存和CPU、jps虚拟机具体进程查看、jstat虚拟机运行时信息查看、jinfo虚拟机配置查看、jmap内存使用记录、jstack堆栈跟踪,真确实出现Full GC频繁等问题,结合网上给出的排查指导、本地Debug、jconsole图表

2、Java的基本数据类型有哪些?String是基本数据类型吗?可以继承吗?String 、StringBuilder、StringBuffer有什么区别?== 和equals()的区别?抽象类和接口的区别?深克隆和浅克隆的区别?

Java基础数据类型:

1、byte:1字节=8Bit	二进制8位最大整数为128,byte的取值范围是[127, -128]
2、short:2字节
3、int:4字节
4、long:8字节
5、float:4字节
6、double:8字节
7、char:3字节
8、boolean:1字节

String是基本数据类型吗?可以继承吗?

String是由final修饰的最终类,不可以被继承,底层是一个char[] 数组。

String、StringBuilder、StringBuffer有什么区别?

1、String是由final修饰的最终类,不可以被继承,底层是一个char[] 数组,所以对已经实例化的字符串进行修改、重新赋值、替换等操作,都是生成一个新的实例化对象,指向旧的引用,旧的实例化对象被回收。
2、String str = “abc”; String str = new String(“abc”); 第一种赋值方式,字符串存储在方法区的字符串常量池当中(JDK1.7及之后,字符串常量池也在堆当中);第二种赋值方式,new关键字的实例化对象在堆当中,但值仍在字符串常量池当中。
3、StringBuilder和StringBuffer都是可变字符串对象,提供的方法都是原字符对象上进行操作,而不会创建新的字符对象,区别在于StringBuffer的方法有synchronized修饰,所以线程安全,而StringBuilder线程不安全。
4、扩容机制都是先 新建一个原始容量二倍加er的新数组,然后 将原始数组复制到新数组当中去,最后 将新的字符串添加进去。

== 和 equals()方法的区别?

1、== 是操作符而equals() 是Object基类的方法
2、equals()是基类方法,所以只能用在引用数据类型之间相互比较,Object类提供的equals()方法比较的是两个对象是否相等,即引用是否指向同一片内存空间,但String、Integer等类往往都重写了equals()方法,一般比较的都是值是否相等;== 对于基础数据类型比较的是值是否相等,而对于引用数据类型比较的是对象是否相等。
3、== 的运行速度要比equals()快,因为equals()仅仅比较引用是否相等。

抽象类和接口的区别?

1、抽象类仅是“不能实例化”,所以只能单继承,而接口可以多实现。
2、抽象类中即可以有抽象方法也可以有普通方法,只是抽象方法的访问修饰符不可以是private,而接口除了JDK1.8之后引入的静态方法和默认方法外,其余方法的访问修饰符都是public
3、抽象类中的属性比较随意,而接口中的属性往往都是公共的静态常量(public static final),而接口中不允许有静态代码块。

深克隆和浅克隆的区别?

1、实现克隆的前提:实现Cloneable接口;重写clone()方法
2、浅克隆:对当前对象进行克隆,并克隆该对象所包含的8种基本数据类型和String类型属性(拷贝一份该对象并重新分配内存,即产生了新的对象);但如果被克隆的对象中包含除8中数据类型和String类型外的其他类型的属性,浅克隆并不会克隆这些属性(即不会为这些属性分配内存,而是引用原来对象中的属性)。
3、深克隆:深克隆是在浅克隆的基础上,递归地克隆除8种基本数据类型和String类型外的属性(即为这些属性重新分配内存而非引用原来对象中的属性)

3、类加载器有哪些?类加载过程是怎样的?双亲委派模式是什么?对象(类)的初始化过程?

类加载器有哪些?

在这里插入图片描述

类加载器:通过类的全限定名获取该类的二进制字节流,并将其加载到虚拟机的代码块叫做类加载器
1、启动类加载器:负责加载<JAVA_HOME>\lib目录下的所有核心类库,例如rt.jar、tools.jar,将类库里的类加载虚拟机内存当中。而且只能是虚拟机识别的类库,用户无法直接识别。
2、扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录下的所有类库,是一种加载类库的扩展机制,由Java代码编写扩展类加载器也可以用来加载程序里的.class文件。
3、应用程序类加载器:负责加载classpath目录下开发程序的所有类库,在程序中如果没有自定义过自己编写的类的加载器,应用程序类加载器就是其默认的类加载器。
4、自定义加载器:想加载非classpath目录下的类文件、自己指定的类库、需要继承ClassLoader类或URLClassLoader,并至少重写其中的findClass(String name)方法,若想打破双亲委托机制,需要重写loadClass方法。
5、总结:子加载器的命名空间,包含了父加载器的命名空间,就可以保证,子加载器加载的类,可以使用父加载器加载的类。也就是说,父加载器加载的类,对子加载器可见,同级别加载器加载的类,不可见。

类加载过程是怎样的?

在这里插入图片描述

类加载过程:就是将.class文件里的信息加载到虚拟机当中,然后解析生成一个class类对象的过程。
1、加载:将.class字节码文件加载到虚拟机内存当中。每个class类经过加载后,都会生成唯一一个类对象,反射可以通过这个类对象来获取对象实例。
2、验证:将验证.class文件的格式、语义是否有错误、方法内部的逻辑和关系、符号引用是否正确。
3、准备:将为static变量分配空间,设置默认值。
4、解析:将常量池里的符号引用替换为直接引用。
5、初始化:

类的初始化过程?

父类的静态属性 -> 父类的静态代码块 -> 子类的静态属性 -> 子类的静态代码块 -> 父类的普通属性 -> 父类的普通代码块 -> 子类的普通属性 -> 子类的普通代码块 -> 父类的构造方法 -> 子类的构造方法

双亲委派模式是什么?

在这里插入图片描述

双亲委派模式:是一个类被某个类加载加载的完整过程。
1、定义一个A类,首先应用类加载器会尝试加载,应用类加载器会先判断自己缓存中是否有该类缓存,有则不再加载直接使用,没有则向上交给扩展类加载器去加载。
2、扩展类加载器会继续尝试加载,同样先判断自己缓存中是否有该类缓存,有则不再加载直接使用,没有则向上交给启动类加载器去加载。
3、启动类加载器会继续尝试加载,继续先判断自己缓存中是否有该类缓存,有则不再加载直接使用,没有则尝试从它负责的目录中去加载,加载成功加入缓存,开始使用,不在负责的目录下,交给扩展类加载器去加载。
4、扩展类加载器判断是否在自己负责的目录下,在,则开始加载,加入缓存,开始使用,不在则继续交给应用程序类加载器去加载。
5、应用程序类加载器判断是否在classpath目录下,在,则开始加载,开始使用,否则抛出ClassNotFoundException异常
优势:
1、避免类的重复加载,保证了一个类的全局唯一性。
2、保证了核心类库的安全,防止核心API被随意篡改。
劣势:
上层的加载器无法访问下层加载器所加载的类,即应用类访问核心类没有问题,但反过来核心类无法访问应用类。

如何打破双亲委派模式?

1、SPI机制:它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。(JDBC)
2、自定义类加载器:Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。

4、说说集合?

在这里插入图片描述
从图中可以看出,集合大概可以分为两个分支,一个是继承了Iterator的Collection的List、Set、Queue分支,另一个是Map分支,下面介绍几个常用的集合类。

ArrayList:

1、底层是Object数组结构;默认容量是JDK1.7是10,JDK1.8是0;扩容时先新建一个数组,然后整体复制旧数组,最后追加新元素,JDK1.7的新数组长度是1.5倍+1,JDK1.8的新数组长度是右移一位,即原长度的二倍。
2、查询快,增删慢:由于数组结构,所以ArrayList的内存连续,所以查询效率高;增删时,遇到扩容、某个位置的插入或删除,都会涉及到新建数组、复制原数组等操作,所以速度较慢。
3、线程不安全。

LinkedList:

1、底层是双向链表结构;不存在固定长度;添加时如果原集合为空,则元素为头节点,如果集合不为空,则原集合的尾节点的Next指针指向该元素。
2、查询慢,增删慢快:由于没有固定长度的限制,所以在增删时,只需要断开原先的指针,重新指向新元素即可;但查询时,需要通过指针挨个遍历,没有下标可以快速定位。
3、线程不安全。

Vector:

1、底层是Object数组结构;默认容量是10;扩容时先新建一个数组,然后整体复制旧数组,最后追加新元素,新数组长度是原长度的二倍或者原容量+制定扩容长度。
2、查询快,增删慢:由于数组结构,所以Vector的内存连续,所以查询效率高;增删时,遇到扩容、某个位置的插入或删除,都会涉及到新建数组、复制原数组等操作,所以速度较慢。
3、由于方法都有synchronized修饰,所以线程安全。
4、Collections.SynchronizedList:SynchronizedList是Collections类的静态内部类,它能把所有 List 接口的实现类利用synchronized代码块,转换成线程安全的List,比 Vector 有更好的扩展性和兼容性。
5、CopyOnWriteArrayList:添加时加锁的(ReentrantLock ,非synchronized同步锁),读操作是没有加锁。添加或者删除元素时,先加锁,再进行复制替换操作,最后再释放锁。 

HashMap:

1、底层是数组+链表的结构,默认容量是JDK1.7是16,JDK1.8是0,链表结构是为了解决哈希冲突,解决哈希冲突的方法还有再哈希法、公共溢出区、开放定址法;默认容量是16,默认的加载因子是0.75,保证不浪费默认容量的同时,元素又能在每个桶内均匀分布,达到空间和时间的平衡;JDK1.7时,是Entry[] 数组,JDK1.8时,是Node[] 数组,但两者的差异不大,都只有四个属性,K值、Value值、K的hash值、Next指针;JDK1.8时,在桶内节点大于等于8(和泊松分布有关,主要是为了寻找一种时间和空间的平衡)且容量大于等于64的情况下,链表会变为红黑树,时间复杂度会从O(n)变为O(logN)。
2、put()方法:HashMap允许key、value都为null,当key为null时,会将元素放在数组[0]位置,再存放key为null的元素时,会依次覆盖;当key不是null时,会先计算key的hash值,然后与底层数组长度减一进行按位与运算,得出相应的下标,下标位置如果没有元素,直接放入;当下标位置有元素,会与元素K的Hash值和K值进行== 和 equals() 比较,如果都相等,则替换元素,如果K值不相等,则插入链表;JDK1.7时,是头插法,即插入到链表的头节点,Next指针指向原头节点,JDK1.8时,是尾插法,解决了多线程操作扩容时导致的环形链表问题。
3、扩容时HashMap会进行resize()和reHash()操作,即先新建一个是原容量二倍的新集合,然后将原集合的元素进行重新寻址,JDK1.8 新桶下表 = 原桶下标 + 扩容长度;一般默认长度和扩容后的数组长度都是2的N次幂,这样在计算下标时,可以保证元素尽可能随机分布。
4、线程不安全。第一,会造成元素丢失问题,线程A在计算好下标后准备插入时被挂起,线程B在同一位置插入元素,线程A插入直接替换掉线程B插入的元素,造成元素丢失;第二,JDK1.7会造成环形链表;第三,在K为对象时,必须重写 hashcode 方法,否则使用重写了equals()方法的对象去get时,可能为null值。不重写equals()方法的情况下,equals相等,则hashcode一定相等,反之则不然。

LinkedHashMap:

1、底层是数组+双向链表的结构,默认容量是JDK1.7是16,JDK1.8是0,双向链表结构同样是为了解决哈希冲突,JDK1.7,JDK1.8元素都被封装成Entry类,拥有两个属性before和after指针。
2、LinkedHashMap的put()方法没有重写,沿用了父类HashMap的put()方法,但是重写了方法内部的recordAccess()、afterNodeAccess()等方法,所以无论是JDK1.7还是JDK1.8都是尾插法,而且由于每个节点都是双向链表的结构,除了在数组上有下标位置关系,节点间都有指针指向,于是在遍历上也就有了顺序,默认是插入顺序,构造方法也可以改变为访问后顺序,每次get()会将节点放在链表尾部。
3、Lru算法的实现:1)新数据插入到链表头部;2)每当缓存命中(即缓存数据被访问),则将数据移到链表头部;3)当链表满的时候,将链表尾部的数据丢弃

HashTable:

1、底层是Entry[] 数组结构,JDK1.7,JDK1.8的默认容量都是11,加载因子是0.75,区别于HashMap,HashTable的K、Value都不允许为null,元素下标定位的方法改为K的hash值与0x7FFFFFFF进行安位与运算后,再与数组的长度取模。
2、扩容时HashTable会进行reHash()操作,即先新建一个是原容量二倍加一的新集合,然后将原集合的元素进行重新寻址。
3、由于方法都有synchronized修饰,所以线程安全。

ConcurrentHashMap:

1、线程安全的HashMap,但JDK1.7,JDK1.8线程安全的实现方式不同;JDK1.8及之后,Map集合的默认容量都从16改为了0且K和Value都不能为空。
2、JDK1.7,ConcurrentHashMap内部维护了一个内部静态类Segment,继承了ReentrantLock。ConcurrentHashMap的整体结构可以理解为,外层的Segment数组,包含一个Entry数组,所以当ConcurrentHashMap执行一般方法时,会先定位桶被包涵在Segment数组哪个元素下,然后调用Segment相应方法,这也是我们常说的分段加锁模型。Segment在执行方法的前后进行tryLock()和unlock()保证线程安全。
3、JDK1.8,ConcurrentHashMap采用自旋锁 + CAS + synchronized代码块的方式来保证线程安全。首先,自旋锁并不是固定的一个类或什么的,具体的应该是一种线程不断进行CAS操作的无限循环方式,CAS操作成功,返回true,退出循环,获取到锁,ReentrantLock里简单过程就是这样,详细的后面再说。JDK1.8的ConcurrentHashMap的put()方法里也是有这样一段代码,先进入一段循环代码,然后调用Unsafe类的getObjectVolatile、compareAndSwapObject、putObjectVolatile保证线程安全,其次当桶内发生hash冲突时,synchronized代码块又起到了保证线程安全的作用,结合synchronized的锁升级,JDK1.8的ConcurrentHashMap保证线程安全锁的粒度更细,开销更小。
4、无论是JDK1.7,ConcurrentHashMap的HashEntry,还是JDK1.8,ConcurrentHashMap的Node,属性value和next都有volatile修饰。

TreeMap:

1、底层是数组+红黑树的结构(平衡二叉树),默认容量JDK1.7,1.8都是0,红黑树的结构同样是为了解决哈希冲突,JDK1.7,JDK1.8元素都被封装成Entry类,拥有K、Value,Left左指针,right右指针,Parent父节点,color = BLACK颜色六个属性。
2、比起LinkedHashMap的插入顺序和访问顺序,TreeMap的构造方法可以指定比较规则,默认构造没有比较器,这样找出最值的操作就简单多了,同样K和Value都不能为空。
3、线程不安全。

HashSet:

1、底层是HashMap的结构,add()方法是调用HashMap的put()方法,将元素封装成一个Node或Entry类,Value值是一个final修饰的静态Object常量,因此HashSet集合存储的元素都是无序且唯一的。
2、线程不安全。

LinkedHashSet:

1、底层是LinkedHashMap的结构,默认容量JDK1.7,1.8都是16,加载因子是0.75,add()方法是调用HashSet的add()方法。
2、线程不安全。

TreeSet:

1、底层是TreeMap结构,所以构造方法也可以指定比较器的比较规则,同样的K不能为空。
2、线程不安全
3、CopyOnWriteArraySet:调用的是CopyOnWriteArrayList的方法,添加时加锁(ReentrantLock锁),读操作是没有加锁。添加或者删除元素时,先加锁,再进行复制替换操作,最后再释放锁。

5、说说异常?try、catch、finally、final、finalize

在这里插入图片描述

如上图所示,以Throwable为父类可抛出的信息共分为两大类,一类是Error即错误;另一类是Exception即异常。

Error

该类代表错误,指程序无法恢复的异常情况。此类错误不受检查,也不是代码错误,所以代码程序也不应该去处理。常见的错误有虚拟机错误、AWT错误等。像 OutOfMemoryError:内存溢出误,StackOverflowError:栈溢出等都是虚拟机错误,发生此类错误,虚拟机都会终止线程。

Exception

该类代表异常,指程序本身可以捕获并且可以处理的异常。异常分为两大类,一类是编译时异常,另一类是运行时异常。
	1、编译时异常:是指在代码编写完成后,Java编译器在编译检查时就会发生的异常。比如 ClassNotFoundException(没有找到指定的类异常),IOException(IO流异常),要么通过throws进行声明抛出,交给调用方处理,要么通过try-catch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类,程序运行前我们必须处理该异常。
	2、运行时异常:是指在代码编写完成后,经过了编译检查,但是在代码程序运行期间可能出现的异常。比如NullPointerException空指针异常、ArrayIndexOutBoundException数组下标越界异常、ClassCastException类型转换错误异常、ArithmeticExecption算术异常。此类异常一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。虽然 Java 编译器不会检查运行时异常,但是我们也可以通过 throws 进行声明抛出,也可以通过 try-catch 对它进行捕获处理。如果产生运行时异常,则需要通过修改代码来进行避免。
try:用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
catch:用于捕获异常。catch用来捕获try语句块中发生的异常。
finally: finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。但finally也不一定执行,在执行try代码之前就return、直接报错、执行try代码时调用System.exit(0)主动退出,以上都不会执行finally代码。
throw:用于抛出异常。
throws: 用在方法签名中,用于声明该方法可能抛出的异常。
final:Java关键字,修饰类为最终类,不允许被继承(String 类);修饰变量为常量,不能修改,修饰方法为最终方法,不允许被重写,但可以被继承和重载。
finalize:是Java基类Object的方法,在对象销毁前会被自动调用,执行对象的销毁操作。Object类的finalize()方法体为空,且也不建议子类去重写,重写执行finalize()后,会给原对象创建一个终结引用对象,并且回收终结引用对象的线程优先级很低,其次finalize()还有可能造成对象复活。

JVM 是如何处理异常的?

在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。

6、Object类提供的方法、sleep()、yield()、join()、interrupt()

Object类提供的方法:

1、getClass():返回该实例对象的类对象。
2、hashCode():返回该实例对象的哈希码值。
3、equals():比较实例对象是否相等。
4、toString():返回该实例对象的字符串表示。
5、clone():创建并返回一个实例对象的副本。
6、wait():将当前线程加入该实例对象的等待池,只能在同步方法或同步代码块中使用,调用wait()方法后,程序不再继续运行,并释放当前线程所持有的实例对象锁,普通方法。
7、notify():唤醒任意一个该实例对象等待池里的线程,同样只能在同步方法或同步代码块中使用,调用notify()方法后,某个线程程序继续运行,普通方法。
8、notifyAll():唤醒全部该实例对象等待池里的线程,同上,调用notifyAll()方法后,所有线程程序继续运行,普通方法。
9、finalize():对象销毁前会被自动调用,执行对象的销毁操作。
锁池:所有需要竞争同步的线程都会放在锁池中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后,锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列,等待cpu资源分配。
等待池:当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify()或notifyAll()后等待线程的线程才会开始竞争锁。notify()是随机从等待池,选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中。

Thread类提供的方法:

1、sleep():让当前线程进入睡眠状态,但不会释放当前所持有的锁,也不需要在同步方法或同步代码块中才能使用,所以也就不用唤醒,sleep()方法更像控制线程的运行速度,而wait()方法直接让线程改变了状态,静态方法。
2、join():线程的合并指,将指定的线程加入到当前的线程之中,可以将两个交替执行的线程合并为顺序执行的线程,如果在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B,例如让“主线程”等待“子线程”结束之后再继续运行,方法内部还是调用了wait()方法,线程A调用线程B的join()方法,实际就是线程A加入了线程B实例对象的等待池,所以也会释放当前线程所持有的实例对象锁,普通方法。
3、yield():暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止,调用yield()方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,也就是说刚刚的那个线程还是有可能会被再次执行到,并且不会释放当前所持有的锁,静态方法。
4、interrupt():将运行的线程的中断标记设置为true,但不会停止线程。在中断标记下如果线程进去阻塞状态(即调用了sleep(),wait(),join()方法),或着线程处于以上阻塞状态时,被调用interrupt()方法,使线程的中断标记设置为true,就会产生一个InterruptedException,中断标记也会立即被改为为false,普通方法,不会释放锁。
5、isInterrupted():判断当前线程的中断标记是true还是false,调用后不会更改当前线程的中断标记,普通方法。
6、interrupted():底层调用的还是isInterrupted()方法,判断当前线程的中断标记是true还是false,如果是true,调用后会立即将该中断标记改为false,静态方法。

7、你知道的IO流有哪些?字节流、字符流有什么区别?BIO、NIO、AIO都是什么,他们又有什么区别?

IO流有哪些?字节流、字符流有什么区别

IO:输入/输出(Input/Output)流,是一个种抽象的概念,是对数据传输的总称。也就是说数据在设备间的传输称为流,流的本质是数据传输。
一、字节流
	处理的单位是字节,操作字节或字节数组,如果操作的是音频、视频、图片等文件,用字节流更好。
	1、InputStream
	2、OutputStream
二、字符流
	处理单位是字符,操作字符、字符串、字符数组等,一个字符=两个字节,如果操作的是纯文本文件,用字符流更方便。
	1、Reader
	2、Writer
三、区别
	1、首先,读取的单位不同,一个是字节,另一个是字符,字节流可以操作任何对象,而字符流只能操作字符或字符串,字节流不能直接操作字符,但可以处理字符编写的文件。
	2、其次,字节流在操作的时候不会用到缓存区,与文件本身进行直接操作,而字符流在操作时会用到缓存区,例如从磁盘A拷贝文件到磁盘B,使用字节流的过程是,实时从磁盘A获取字节,加载到工作内存后,再实时写入到磁盘B,那字符流的操作过程是,先从磁盘A获取字符后,不断追加到内存缓存当中,直到关闭后,才一并将缓存区的字符写到磁盘B。这也是为什么字符流不使用close方法的话,不会输出任何内容。

BIO、NIO、AIO都是什么,他们有什么区别

上面说的IO是对数据传输的总称,具体网络编程,又分为下面三种IO模型:BIO、NIO、AIO
一、BIO(同步阻塞IO)
	最早期也是最传统的同步阻塞模型。
	首先,客户端只要有连接请求,服务端就要启动一个线程去处理请求,如果这个连接什么都不做,那对于服务端来说,无疑是对线程的开销和浪费。
	其次,建立连接后,客户端发送的每一个请求,都要等到服务端返回后,再进行下一次请求,是一个串行的过程,同步的过程。
	最后,建立连接后,如果客户端什么都没做,没有任何请求,服务端线程将一直都是阻塞状态。
二、NIO(同步非阻塞IO)
	相比于BIO,NIO建立了一种服务端启用一个线程去处理客户端多个连接请求的模型,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
	首先,NIO有三个核心部分,selector选择器,选择器绑定了不同的管道,每个管道又对应一个缓存区,客户端请求到达服务端之后,服务端线程通过选择器轮训选择与哪个管道建立连接,并开始读写操作;channel管道,与BIO不同的是,读要用InputStream,写要用OutputStream,而管道既可以读,也可以写;Buffer缓存区,在管道的读写过程,都是在内存的一块区域上的操作,而不需要与磁盘进行实时IO,这样做速度更快效率更高。
	其次,因为服务端只启用了一个线程,所以在处理多请求时,仍然是一个串行的同步操作。
	最后,服务端只启用一个线程,避免了多线程的资源开销,也避免了线程间切换的开销,而且在连接后,只有当客户端发起情后后(基于事件),服务端才会启用线程进行轮训,所以也就避免了线程阻塞。
三、AIO(异步非阻塞IO)
	相比于BIO,AIO的read或write方法可以理解为又开启了一个异步线程进行磁盘读写,并加载到内存的缓冲区,而主线程可以立即返回,异步线程完成IO读写操作后,再调用回掉函数,服务器主线程将资源发送给客户端。
	这样,服务器的主线程和实际的IO操作线程是分开的异步操作,而且同样一个有效请求才会建立一个线程,也避免了线程阻塞问题。
四、总结
	1、BIO是一个连接一个线程,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
	2、NIO是一个请求一个线程,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
	3、AIO是一个有效请求一个线程,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是有OS先完成了再通知服务器应用去启动线程进行处理。

8、JDK1.8增加了哪些新特性?

一、Lambda表达式、对象引用

Lambda表达式允许把函数作为一个方法的参数(函数作为方法参数传递),将代码像数据一样传递,只支持函数式接口。

二、函数式接口

 如果一个接口只有一个抽象方法,则该接口称之为函数式接口,函数式接口可以使用Lambda表达式,Lambda表达式会被匹配到这个抽象方法上,@FunctionalInterface 注解检测接口是否符合函数式接口。常见的函数式接口Runnable、Function、Comparator、Comsumer、Predicate等。

三、Stream流API

把函数式编程风格引入到Java中。

四、新增Time包下的日期时间类

LocalDate、LocalTime、LocalDateTime是java8对日期、时间提供的新接口。实际使用中,计算日期就用LocalDate,计算日期加时刻用LocalDateTime,如果只有时刻就是LocalTime。

五、新增Optional容器类

六、接口新增了除抽象方法外的默认方法、静态方法

9、说说反射?

1、反射机制:是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意属性和方法;这种动态获取信息以及动态调用对象方法的功能称为 java 语言的反射机制。
2、工作原理:当一个字节码文件加载到内存的时候, jvm会对该字节码进行解剖,然后创建一个对象的Class对象,jvm把字节码文件的信息全部都存储到该Class对象中,我们只要获取到Class对象,我们就可以使用该对象设置对象的属性或者调用对象的方法等操作。
3、反射机制,我们可以实现如下的操作:
	1、程序运行时,可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的信息;
	2、程序运行时,可以通过反射创建任意一个类的实例,并访问该实例的成员;
	3、程序运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象。
4、获取Class类对象的三种方式
	1、类名.class属性
	2、对象名.getClass()方法
	3、Class.forName(全类名)方法
5、反射的应用场景有
	1、使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序。
	2、多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;
	3、面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。

10、说说编程时用到的范型?

一、定义
	泛型,即“参数化类型”,本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;
	而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
二、范型类
	尖括号 <> 中的泛型标识被称作是类型参数,用于指代任何数据类型。常见如下:
		T :代表一般的任何类。
		E :代表 Element 元素的意思,或者 Exception 异常的意思。
		K :代表 Key 的意思。
		V :代表 Value 的意思,通常与 K 一起配合使用。
		S :代表 Subtype 的意思,文章后面部分会讲解示意。
三、范型接口
四、范型方法
	当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数,只有在方法签名中声明了< T >的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法。

在这里插入图片描述
在这里插入图片描述

	上面代码中,Test< T > 是泛型类,testMethod() 是泛型类中的普通方法,其使用的类型参数是与泛型类中定义的类型参数。
而 testMethod1() 是一个泛型方法,他使用的类型参数是与方法签名中声明的类型参数。
虽然泛型类中定义的类型参数标识和泛型方法中定义的类型参数标识都为< T >,但它们彼此之间是相互独立的。也就是说,泛型方法始终以自己声明的类型参数为准。
	1. < T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。
	2. 为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。
	3. 与泛型类的类型参数定义一样,此处泛型方法中的 T 可以写为任意标识,常见的如 T、E、K、V 等形式的参数常用于表示泛型。
五、类型擦除
	泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。

在这里插入图片描述

六、Java 中 List< Object > 和原始类型 List 之间的区别?
	原始类型和 < Object > 之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对泛型类型 < Object > 进行检查。< Object > 通过使用 Object 作为类型参数,可以告知编译器可以接收任何数据类型的对象,比如 String 或 Integer。 
	这道题的考察点在于对泛型中原始类型的正确理解。它们之间的第二点区别是,你可以把任何泛型类型传递给接收原始类型 List 的方法,但却不能把 List< String > 传递给 List< Object > 的方法,因为会产生编译错误。举例如下:

在这里插入图片描述

七、Java 中 List<?> 和 List< Object > 之间的区别是什么?
	这道题跟上一道题看起来很像,实质上却完全不同。List<?> 是一个不确定的未知类型的 List,而 List< Object > 是一个确定的 Object 类型的 List。
	List<?> 在逻辑上是所有 List< T > 的父类,你可以把 List< String >、 List< Integer > 等集合赋值给 List<?> 的引用;而 List< Object > 只代表了自己这个泛型集合类,只能把 List< Object > 赋值给 List< Object > 的引用,但是 List< Object > 集合中可以加入任意类型的数据,因为 Object 类是最高父类。 举例如下:

在这里插入图片描述

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值