Java核心技术
1、Hash Map底层源码,数据结构
Hash Map采用由数组+链表+红黑树的存储方式。
采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。
数组的特点:寻址容易,插入和删除困难。存储空间紧凑,不适合存储稀疏数据。
链表特点:插入和删除简单,寻址困难,单位存储空间比数组高,但是适合存储稀疏数据。
所以当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升。
2.写出你用过的设计模式,并举例说明解决的实际问题
private LazySingletonFileSystem() {
}
/**
* the local filesystem instance only one,unless used reflect
*
* @return a local filesystem,the filesystem is singleton
* @throws IOException IOException
*/
public static FileSystem getLocalFs() throws IOException {
if (localFs == null) {
synchronized (LazySingletonFileSystem.class) {
if (localFs == null) {
localFs = FileSystem.getLocal(new Configuration());
}
}
}
return localFs;
}
提供一个单例模式以供参考,这是双检锁(double check)的单例模式,解决了对象的全局唯一性,即该方法提供的所有本地文件系统对象都是单例的。
3、Java创建线程的几种方式
- 继承Thread类,重写run方法。
- 实现Runnable接口,实现run方法。
- 通过线程池获取线程
- 实现Callable接口并实现call方法,创建该类的实例,使用Future Task类保证Callable对象,使用Future Task对象作为Thread对象的target创建并启用新线程。
4.请简述操作系统的线程和进程的区别
- 同一个进程可以包含多个线程,一个进程中至少包含一个线程,一个线程只能存在于一个进程中。
- 同一个进程下的所有线程能够共享该进程下的资源。
- 进程结束后,该进程下的所有线程将销毁,而一个线程的结束不会影响同一个进程下的其它线程。
- 线程是轻量级的进程,它的创建和销毁所需要的时间比进程小得多,所有操作系统的执行功能都是通过创建线程去完成的。
- 线程在执行时是同步和互斥的,因为他们共享同一个进程下资源。
- 在操作系统中,进程是拥有系统资源的独立单元,它可以拥有自己的资源。一般而言,线程不能拥有自己的资源,但是它能够访问其隶属进程的资源。
5、Java程序出现OutOfMemoryError:unable to create new native thread 的原因可能有哪些?如何分析和解决?
本质原因是创建了太多的线程,导致异常的发生(能创建的线程数是有限制的)。能创建的线程数的计算公式:
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
MaxProcessMemory 指的是一个进程的最大内存
JVMMemory JVM内存
ReservedOsMemory 保留的操作系统内存
ThreadStackSize 线程栈的大小
如果程序中有bug,导致创建大量不需要的线程或者线程没有及时回收,那么必须解决这个bug,修改参数是不能解决问题的。
如果程序确实需要大量的线程,现有的设置不能达到要求。
通过修改MaxProcessMemory,JVMMemory,ThreadStackSize这三个因素,来增加能创建的线程数:
- MaxProcessMemory 使用64位操作系统
- VMMemory 减少 JVMMemory 的分配
- ThreadStackSize 减小单个线程的栈大小
修改系统的最大进程数。
6.采用java或自己熟悉的任何语言分别实现简单版本的线性表和链表,只需实现add,remove方法即可
// 线性表
class LineTable{
int[] table;
int index = 0;
public LineTable(int len){
table = new int[100];
}
// 增加一个元素,如果满了,不接受其它元素
public void add(int num){
if(index<table.length){
table[index ++] = num;
}
}
// 从顶端删除元素,如果空了,不再删除元素。
public void remove(){
if(index > 0){
index --;
}
}
}
// 链表:使用哨兵
class LinkedTable{
private class Node{
int value;
Node next;
public Node(int num){
value = num;
}
}
Node head, tail;
public LinkedTable(){
head = tail = new Node();
}
// 尾插法,在链表尾插入一个节点。
public void add(int num){
tail.next = new Node(num);
tail = tail.next;
}
// 头删法,从头部开始删除。
public void remove(){
if(head.next != null){
head.next = head.next.next;
}
}
}
上述仅供参考
7.ArrayList和LinkedList的区别
- 数据结构不同:ArrayList是Array(动态数组)的数据结构,LinkedList是Link(链表)的数据结构。
- 效率不同:ArrayList适合随机读改操作,linkedList适合增删操作。
- 自由性不同:ArrayList便于使用,但固定容量大小,如果发生大小变化比较麻烦。LinkedList的容量动态的随数据量变化而变化,但使用比较麻烦,尤其是随机读改操作时。
- 开销不同:ArrayList开销主要在需要给List列表预留一定空间。而LinkedList开销主要在需要存储节点信息及节点指针信息。
8、jvm内存分那几个区,每个区的作用时什么?
有方法区、虚拟机栈、本地方法栈、堆、程序计数器等5个区。
方法区:
- 很少发生来垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型的卸载。
- 主要用来存储_已被虚拟机加载_的类的信息、常量、静态变量和即时编译器编译后的代码等数据。
- 该区域是被线程共享的。
- 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。
虚拟机栈(栈内存)
- 为java方法服务。
- 是线程私有的,生命周期和线程相同。
- 局部变量表中存储的是基本数据类型,返回地址类型和对象引用。
- 操作数栈是用来存储运算结果及运算的操作数,通过压栈和出栈的方式。
- 每个栈帧都有一个指向运行时常量池中所属方法的引用,是为了方法调用过程中的动态连接。
本地方法栈
和虚拟机栈类似,为本地方法服务。
堆
是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有对象实例都在这里创建,这里经常发生垃圾回收操作
程序计数器
内存空间小,各种指令发生的跳转都依赖这个计数器完成。是唯一一个java虚拟机规范没有任何OOM情况的区域。
9.Java中迭代器和集合的区别?
集合是将所有数据加载到内存,然后通过集合的方法去内存中获取,而迭代器是一个对象,实现了Iterator接口,实现了接口的hasNext和next方法。
集合是一种数据结构,集合里面的数据无序且不重复。
使用迭代器可以遍历集合中的元素。
10.HashMap 和 HashTable 区别
1)线程安全性不同
HashMap 是线程不安全的。
HashTable 是线程安全的,原因是HashTable自身使用了 synchronized关键字进行了同步处理。
在多线程并发的情况下,可以直接使用 HashTable,而使用 HashMap 时必须自己增加同步处理。
2) 是否提供 contains 方法
HashMap 只有 containsValue 和 containsKey 方法;HashTable 有 contains、containsKey和 containsValue 三个方法,其中 contains 和 containsValue 方法功能相同。
3) key 和 value 是否允许 null 值
Hashtable 中,key 和 value 都不允许出现 null 值。
HashMap 中,null 作为键,这样的键只有一个。
作为值,可以有一个或多个键所对应的值为 null。
4) 数组初始化和扩容机制
HashTable 在不指定容量的情况下的默认容量为 11,而 HashMap 为 16。
Hashtable 不要求底层数组的容量一定要为 2 的整数次幂,而 HashMap 则要求一定为 2 的整数次幂。
Hashtable 扩容时,将容量变为原来的 2 倍加 1,而 HashMap 扩容时,将容量变为原来的 2 倍。
11.线程池使用注意哪些方面?
线程池分为单线程线程池,固定大小线程池,可缓冲的线程池。
线程池中重要的配置
-
corePoolSize : 核心线程数量
-
workQueue : 等待队列
-
maximumPoolSize : 最大线程数量
提交任务时,判断的顺序为 corePoolSize --> workQueue -->maximumPoolSize。
当线程数小于核心线程数时,创建核心线程。
当线程大于等于核心线程数,且任务队列未满时,将任务放入队列。
当线程数大于核心线程数,且任务队列已满时,检查最大线程数是否已满,若未满,创建非核心线程,若满,根据拒绝策略抛出异常拒绝任务。
拒绝策略—RejectedExecutionHandler
-
AbortPolicy : 直接抛出异常,这是默认策略
-
CallerRunsPolicy : 用调用者所在线程来执行任务
-
DiscardOldestPolicy : 丢弃阻塞队列中最靠前的任务,并执行当前任务
-
DiscardPolicy : 直接丢弃任务
遇到的坑!!!
任务提交后长时间没有执行
任务进入了队列,线程还在执行之前的任务。提交的任务还在排队等待执行中
线程执行任务中无故消失
- 线程拒绝策略配置为CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy时 ,线程池满了不会抛出异常。建议将拒绝策略配置为AbortPolicy(也是默认设置)。
- 一般情况下,代码只会去捕捉Exception,如果抛出Error(比如内存溢出)则会导致线程退出,而异常信息又没有拿到。最佳的解决办法是给线程池设置UncaughtExceptionHandler。
12.HashMap和TreeMap的区别?TreeMap排序规则?
- HashMap:适用于在Map中插入、删除和定位元素。
- Treemap:适用于按自然顺序或自定义顺序遍历键(key)。
- HashMap使用的数据结构是hash表(线性表)+链表+红黑树。TreeMap使用的数据结构是红黑树。
- HashMap通常比TreeMap快一点(hash表的数据结构使然,在链表数量超过8之后链表会转化为红黑树),建议多使用HashMap,在需要排序的Map时候才用TreeMap.
- HashMap的遍历结果是没有排序的,而TreeMap的遍历结果是排好序的。
TreeMap会自动进行排序。
根据key的Compare方法进行排序。
13.用java实现单例模式
线程安全(还简单易实现)的单例模式一般有2种,双重检锁(double check)和饿汉式。
双重检锁的实现相对较复杂,详看第2问的设计模式。
饿汉式通过静态变量提前加载对象,简单易实现,缺点是这个对象从类加载开始会一直存在,可能会比较消耗内存。
14.使用递归算法求n的阶乘:n! ,语言不限
// 求n的阶乘
public int fac(int n) {
if (n == 1) {
return 1;
}
// n*(n-1)*...*1
return n * fac(n - 1);
}
15.TreeSet 和 HashSet 区别
HashSet 是采用 hash 表来实现的。其中的元素没有按顺序排列,add()、remove()以及contains()等方法都是复杂度为 O(1)的方法。
TreeSet 是采用树结构实现(红黑树算法)。元素是按顺序进行排列,但是 add()、remove()以及 contains()等方法都是复杂度为 O(log (n))的方法。它还提供了一些方法来处理排序的 set,如 first(),last(),headSet(),tailSet()等等。
16.Stringbuffer 和 Stringbuild 区别
- 只是 StringBuffer 中的方法大都采用了 synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是线程不安全的。
- 在单线程程序下,StringBuilder 效率更快,因为它不需要加锁,不具备多线程安全而 StringBuffer 则每次都需要判断锁,效率相对更低.
ps:StringBuffer 与 StringBuilder 中的方法和功能完全是等价的。
17.Final、Finally、Finalize
final
修饰符(关键字)有三种用法:修饰类、变量和方法。
修饰类时,意味着它不能再派生出新的子类,即不能被继承,因此它和 abstract 是反义词。
修饰变量时,该变量使用中不被改变,必须在声明时给定初值,在引用中只能读取不可修改,即为常量。
修饰方法时,也同样只能使用,不能在子类中被重写。
finally
通常放在 try…catch 的后面构造最终执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要 JVM 不关闭都能执行,可以将释放外部资源的代码写在finally 块中。
finalize
Object 类中定义的方法,Java 中允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写 finalize() 方法可以整理系统资源或者执行其他清理工作。
18、==和 Equals 区别
==
如果比较的是基本数据类型,那么比较的是变量的值
如果比较的是引用数据类型,那么比较的是地址值(两个对象是否指向同一块内
存)
equals
如果没重写 equals 方法比较的是两个对象的地址值。
如果重写了 equals 方法后我们往往比较的是对象中的属性的内容
ps:equals 方法是从 Object 类中继承的,默认的实现就是使用==
20.比较ArrayList,LinkedList的存储特性和读写性能
ArrayList
以动态数组的形式保存对象,将对象放在连续的位置上。
随机读的时间复杂度O(1)
插入的时间复杂度O(n)。
LinkedList
以双向链表的形式保存对象,将每个对象串联起来。
随机读的时间复杂度O(n)
插入的时间复杂度O(1)。
20.Java 类加载过程
Java类加载需要经历以下几个过程:
1)加载
- 通过一个类的全限定 名(类的全名)获取定义该类的二进制字节流。
- 将该二进制流中的静态存储结构转化为方法区的运行时数据结构。
- 将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象,作为方法去这个类的各种数据的访问入口。
2)验证
验证的目的是为了确保Class文件的字节流中的信息不会危害到虚拟机。在该阶段主要完成以下四种验证:
- 文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父亲,是否集成了不被继承的类等。
- 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
- 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
3)准备
为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
4)解析
该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。
5)初始化
类加载的最后一步,前面的类加载过程,出了加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
21.java中垃圾收集的方法有哪些?
1)根搜索算法(GC Root Tracing)
通过一系列的名为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径成为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时(用图论来说就是GC Root到这个对象不可达时),证明该对象是可以被回收的。
Java可以成为GC Root的对象:
- 虚拟机栈(栈帧中的本地变量表)中的引用对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用对象
- 本地方法栈中JNI(即Native方法)的引用对象。
2)标记-清除算法(Mark-Sweep)
这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记那些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:
- 效率不高,标记和清除的效率都很低;
- 会产生大量不连续的内存碎片,导致以后程序在分配交大的对象时,由于没有充足的连续内存而提前触发一次GC动作。
3)复制算法(Copying)
为了解决效率问题,复制算法将可用内存按容量划分相等的两部分,然后每次只使用其中的一块,当第一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清除完第一块内存,在将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一块内存。
于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大的那份内存叫Eden区,其余两块较小的内存叫Survivor区。每次都会先使用Eden区,若Eden区满,就将对象赋值到第二块内存上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制赋值到老年代中。(java堆又分为新生代和老年代)。
4)标记-整理算法(Mark-Compact)
该算法是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。
标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。这样就不会产生内存碎片。
5)分代收集算法(Generational Collection)
根据对象的存活周期的不同将内存划分为几块,一般就分为新生代和老年代,根据各个年代的特点采用不同的收集算法。新生代(少量存活)用复制算法,老年代(对象存活率高)“标记-整理”算法。
6)引用计数算法(Reference Counting)
介绍:给对象添加一个引用计数器,每当一个地方引用它时,数据器加1;当引用失效时,计数器减1;计数器为0的即可被回收。
优点:实现简单,判断效率高。
缺点:很难解决对象之间的相互循环引用(objA.instance = objB; objB.instance = objA)的问题,所以java语言并没有选用引用计数法管理内存。
ps:分代划分内存介绍
整个JVM内存总共划分为三代:年轻代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)
- 年轻代:所有新生成的对象首先都放在年轻代内存中。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代内存分为一块较大的eden空间和两块较小的Survivor空间,每次使用eden和其中的一块Survivor。当回收时,将eden和Survivor中还存活的对象一次性拷贝到另一块Survivor空间上,最后清理eden和刚才用过的Survivor空间。
- 年老代:在年轻代经历了N次GC后,仍然存活的对象,就会被放在年老代中。因此可以认为年老代存放的都是一些生命周期较长的对象。
- 持久代:基本固定不变,用于存放静态文件,例如Java类和方法。持久代对GC没有显著影响。持久代可以通过-XX:MaxPermSize=进行设置。
22.如何判断一个对象是否存活?(或者GC对象的判定方法)
判断对象是否存活的算法包括:
1)引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1。
当引用失效时,计数器值减1。
任何时刻计数器为0的对象就是不能再被引用的。
ps:例如Object-C,Python语音使用引用计数算法进行内存管理。Java虚拟机没有选用引用计数器算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
2)可达性分析算法
可达性分析算法的基本思路是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则证明此对象是不可用的。
经过根搜索算法的可达性分析某个对象没有和GC Roots的直接或间接联系(不可达),并不意味着该对象将被回收,还需要经过两次标记和筛选工作,才能决定是否可以回收对象。
第一次:判断是否有finalize方法或者执行过finalize,如果没有finalize方法或者已经执行过finalize方法,不需要进行筛选则可以回收,否则需要进行筛选进行第二次标记和筛选。
第二次:执行对象的finalize方法,执行完成或者执行过程中判断对象是否和GC Roots是否有直接或者间接联系,如果依然没有联系则把对象放入回收列表等待回收,否则对象复活。
23.jvm、堆栈
1)堆
堆内存用来存放由new创建的对象和数组。
在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。
引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。
2)栈
在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。
当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
24.java基本数据类型
- byte:1字节8位,最大存储数据量是255,存放的数据范围是-128~127之间。封装类是 。Byte
- short:2字节16位,最大数据存储量是65536,数据范围是-32768~32767之间。 Short
- int:4字节32位,最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。Integer
- long:8字节64位,最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1。Long
- float:4字节32位,数据范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。Float
- double:8字节64位,数据范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。Double
- boolean:只有true和false两个取值。Boolean
- char:2字节16位,存储Unicode码,用单引号赋值。Character