二、hashcode与equals hashmap的扩容过程
在进行map或者set的时候必须重新hashcode与equals方法,为了防止equals相等但是hashcode不相等,如果不重写hascode就会调用object类的hashcode,而object的hashcode方法是在使用地址生成的。
重新equals一定要重写hashcode的,不重写会造成equals方法相等,但是hashcode不一样。
如果两个对象 equals,Java 运行时环境会认为他们的 hashCode 一定相等。
如果两个对象不 equals,他们的 hashCode 有可能相等。
如果两个对象 hashCode 相等,他们不一定 equals。
如果两个对象 hashCode 不相等,他们一定不 equals
扩容过程:
Jdk1.7就是简单的扩容之后再进行hashcode再取余得到数组中的下标,有可能和原来的位置一样,也有可能不一样(但是不一样的相当于原先的位置+16,以初始大小16为例)
Jdk1.8由于涉及到红黑树便会更复杂一些,hashcode与原始长度判断位置是否发生变化,对于红黑树会分成两个链表,一个在原来位置,一个在新位置,之后判断是否在新map中转换为树。
三、ConcurrentHashMap是怎么保证安全的
Jdk1.8 CAS + synchronized + volatile cas比较and交换
Jdk1.7Segment + HashEntry + ReentrantLock
四、hashMap的容量为什么是2的次方?
1、加快hash运算速度
当使用可以计算出hashcode之后需要进行取余求当前元素在hash表中的位置,%效率比较低,因此使用 & ,但是 a % b = a &(b - 1),但是前提就是b是2的次幂。
2、使得元素分布更均匀也就是减少hash冲突。
如果b为2的次幂的时候,b - 1就是奇数,在二进制中最后一位为1,与a & 之后最后一位可以是0也可以是1,有利于使得元素在hash表中分布更均匀。假如数组长度就是默认的16,减一就是15,通过和15进行与运算计算出下标并且加入数组,因为要保证0-15比较均匀的存数,所以15保证了二进制位都为1,与哈希值相与就可以保证的出来的数在0-15的范围内且均匀分布,如果是16的话,0001 0000,相与后全填到一个位置上了。
hashmap的底层以及扩容jdk1.8
1.hashMap底层实现是一个哈希表,数组+单链表+红黑树(同时也是双向链表)
2.hashMap初始容量大小为16,扩容因子为0.75,扩容倍数为2;
五、jvm垃圾回收机制GC
Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法;年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和年老代分别提供了多种不同的垃圾收集器。
java中有四种垃圾回收算法,分别是标记清除法、标记整理法、复制算法、分代收集算法
1、标记清除法
标记清除法: 第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记; 第二步:在遍历一遍,将所有标记的对象回收掉; 特点:效率不行,标记和清除的效率都不高;标记和清除后会产生大量的不连续的空间分片,可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC;
2、标记整理法
标记整理法: 第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记; 第二步:将所有的存活的对象向一段移动,将端边界以外的对象都回收掉; 特点:适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生;
3、复制算法
复制算法: 将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除; 特点:不会产生空间碎片;内存使用率极低;
4、分代收集算法
分代收集算法: 根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;
JVM性能调优监控工具jmap、jps、jconsoler等可以查看jvm的内存模型
六. JVM中一次完整的GC是什么样子的?
先描述一下Java堆内存划分。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ),新生代默认占总空间的 1/3,老年代默认占 2/3。 新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1。
新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。
老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。
再描述它们之间转化流程:
- 对象优先在Eden分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
- 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;
- Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
- 移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代。GC年龄的阀值可以通过参数 -XX:MaxTenuringThreshold 设置,默认为 15;
- 动态对象年龄判定:Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%;
- Survivor 区内存不足会发生担保分配,超过指定大小的对象可以直接进入老年代。
- 大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组),为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
- 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和老年代。
常见的垃圾回收器
区别一: 使用范围不一样
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
区别二: STW的时间
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
区别三: 垃圾碎片
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
区别四: 垃圾回收的过程不一样
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现,在标记清理过程中不会导致用户线程无法定位引用对象。仅作用于老年代收集。它的步骤如下:
初始标记(CMS initial mark):独占CPU,stop-the-world, 仅标记GCroots能直接关联的对象,速度比较快;
并发标记(CMS concurrent mark):可以和用户线程并发执行,通过GCRoots Tracing 标记所有可达对象;
重新标记(CMS remark):独占CPU,stop-the-world, 对并发标记阶段用户线程运行产生的垃圾对象进行标记修正,以及更新逃逸对象;
并发清理(CMS concurrent sweep):可以和用户线程并发执行,清理在重复标记中被标记为可回收的对象。
G1垃圾回收器不在严格区分新生代与老年代,它将整个内存空间分为大小相同的区域,会预测每个区域的垃圾以及回收时间,G1通过对应用之前的行为和停顿时间进行分析构建出可预测停顿时间模型,并且利用这个信息来预测停顿时间内的垃圾收集情况。比如:G1会首先回收那些收集效率高的内存区域(这些区别大部分空间是可回收垃圾,这也是为啥叫G1的原因)。
七、可达性分析中可作为根的有:
可以作为GC roots的对象包含以下的几种
- 虚拟机栈中的引用对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中的native方法的引用对象
八、介绍一下空间分配担保原则
如果YougGC时新生代有大量对象存活下来,而 survivor 区放不下了,这时必须转移到老年代中,但这时发现老年代也放不下这些对象了,那怎么处理呢?其实JVM有一个老年代空间分配担保机制来保证对象能够进入老年代。
在执行每次 YoungGC 之前,JVM会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。因为在极端情况下,可能新生代 YoungGC 后,所有对象都存活下来了,而 survivor 区又放不下,那可能所有对象都要进入老年代了。这个时候如果老年代的可用连续空间是大于新生代所有对象的总大小的,那就可以放心进行 YoungGC。但如果老年代的内存大小是小于新生代对象总大小的,那就有可能老年代空间不够放入新生代所有存活对象,这个时候JVM就会先检查 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许,就会判断老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次YoungGC,尽快这次YoungGC是有风险的。如果小于,或者 -XX:HandlePromotionFailure 参数不允许担保失败,这时就会进行一次 Full GC。
在允许担保失败并尝试进行YoungGC后,可能会出现三种情况:
- ① YoungGC后,存活对象小于survivor大小,此时存活对象进入survivor区中
- ② YoungGC后,存活对象大于survivor大小,但是小于老年大可用空间大小,此时直接进入老年代。
- ③ YoungGC后,存活对象大于survivor大小,也大于老年大可用空间大小,老年代也放不下这些对象了,此时就会发生“Handle Promotion Failure”,就触发了 Full GC。如果 Full GC后,老年代还是没有足够的空间,此时就会发生OOM内存溢出了。
九、线程安全的集合有Vector、HashTable、Stack、ArrayBlockingQueue、ConcurrentHashMap、ConcurrentLinkedQueueVector相当于 ArrayList 的翻版,是长度可变的数组,Vector的每个方法都加了 synchronized 修饰符,是线程安全的。
Hashtable是一个线程安全的集合,是单线程集合,它给几乎所有public方法都加上了synchronized关键字。
Stack继承于Vector, 栈是后进先出的。
ArrayBlockingQueue是一个阻塞队列,底层使用数组结构实现,按照先进先出(FIFO)的原则对元素进行排序。
ConcurrentHashMap 采用了分段锁(Segment),HashTable的加锁方法是给每个方法加上synchronized关键字,线程安全。
ConcurrentLinkedQueue是一种FIFO的无界队列,是线程安全的,它适用于“高并发”的场景。
十、接口和抽象类
一、抽象类与接口的相同之处
1、抽象类和接口都不能被实例化,都用于被其他类实现或继承
2、他们都可以包含抽象方法,并且在其他类继承或实现的时候都必须实现这些抽象方法
二、抽象类与接口的区别
1、抽象类是对事物属性的抽象,而接口是对行为的抽象
2、接口只能做方法的声明,而抽象类中既可以包含方法的声明,也可以包含方法的实现。
3、接口里只能定义静态常量,而不能定义成员变量,抽象类中既可以定义静态常量,也可以定义成员变量。
4、接口没有构造函数,而抽象类有构造函数。
5、java语法当中只支持类的单继承,而可以存在接口的多实现。
6、接口方法的访问权限必须是公共的,被public修饰
十一、深浅拷贝
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。clone是浅拷贝
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
十二、反射的理解
反射是Java语言的一个特性,它允许程序在运行时(注意不是编译的时候)来进行自我检查并且对内部的成员进行操作。例如它允许一个Java类获取它所有的成员变量和方法并且显示出来。
通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象,这种编程方式可以让对象在生成时才被决定到底是哪一种对象。只是在Spring中要生产的对象都在配置文件中给出定义,
十三、单例模式
//懒汉式单例类.在第一次调用的时候实例化自己
public class Singleton {
private Singleton() {}
private static Singleton single=null;
//静态工厂方法
public static Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}
十四、内部类
成员内部类:成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员),在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问
局部内部类:局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内
匿名内部类:匿名内部类也是不能有访问修饰符和static修饰符的。匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter
静态内部类:类似于静态方法
内部类的好处:每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整
十五、自动拆箱装箱
装箱执行Integer.valueof(),拆箱执行a.intValue()
十六、LinkedList和ArrayList
1. ArrayList是实现了基于动态数组的数据结构,默认大小是10,会扩容1.5倍。而LinkedList是基于链表的数据结构;
2. 对于随机访问get和set,ArrayList要优于LinkedList,因为LinkedList要移动指针;
3. 对于添加和删除操作add和remove,一般大家都会说LinkedList要比ArrayList快,因为ArrayList要移动数据。但是实际情况并非这样,对于添加或删除,LinkedList和ArrayList并不能明确说明谁快谁慢,这主要取决于删除添加的位置,如果都是尾插法的话,ArrayList的效率其实更高,随机插入删除的话LinkedList的效率会比较高。
十七、Java面向对象的特性以及封装、继承、多态的优缺点
封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。
封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口控制。封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。
适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。
封装的优点
1. 良好的封装能够减少耦合。
2. 类内部的结构可以自由修改。
3. 可以对成员变量进行更精确的控制。
4. 隐藏信息,实现细节。
继承:使子类拥有父类非private的属性和功能,子类还具有自己的属性和功能,同时子类还可以改写父类中的功能(方法重写、与多态有关)
继承的好处:
(1)继承使得所有的子类的公共部分都放在了父类中,使得代码得到了共享。提高了代码的重用性,避免了重复。
(2)继承可使得修改或扩展继承而来的实现都较为容易
(3)使类与类之间产生联系,为多态提供了前提
继承的缺点:
(1)继承具有入侵性(即继承必须拥有父类的所有非私有属性和方法)
(2)父类变,子类变
(2)继承破坏封装,父类实现细节暴露给子类,增大了两个类之间的耦合性
多态(面向对象的核心):指相同的操作或者函数、过程可作用于多种类型的对象上并获得不同的结果;不同的对象,收到同一消息可以产生不同的结果。多态性通过派生类重载基类中的虚函数型方法来实现。
多态的前提:
(1)有继承关系或者实现接口关系。
(2)有方法的重写。
(3)有父类引用指向之类对象。
多态的优点:
(1)提高了代码的维护性(继承保证)
(2)提高了代码的可扩展性(有多态保证)
多态需要注意的地方:
(1)要实现多态,对象的声明必须是父类,而不是子类,实例化的对象是子类
(2)虚方法是按照其运行时的类型而非编译时类型进行动态绑定调用的
静态多态性和动态多态性:
(1)静态多态性——编译时的多态性
实现方式:重载
(2)动态多态性——运行时的多态性(只直到系统运行时,才根据实际情况决定实现何种操作)
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则error;如果有,调用子类的同名方法。
父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器
十八、对原子性的理解
JAVA内存模型定义的8中操作;read、load、use、assign、store、write、lock、unlock等八种指令都是原子的