java面试知识点总结

来源:互联网 发布:简历 协同过滤算法 编辑:程序博客网 时间:2024/06/14 13:10

1.借口和抽象类:

(1)、抽象类可以定义自己的数据成员,也可以有非abstract的方法,而在接口中数据成员默认都是static final的,而且所有方法也是abstract的。

(2)、一个类可以实现多个接口,但是只能继承自一个抽象类。

(3)、抽象类不能定义默认方法,接口可以。

(4)、接口可以多继承

2. HashMap和HashTable:

    (1)、两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全。

Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合(Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例,简单的说就是Collections.synchronizedMap()方法帮我们在操作HashMap时自动添加了synchronized来实现线程同步,类似的其它Collections.synchronizedXX方法也是类似原理)。类似代理?

(2)、HashMap可以使用null作为key,而Hashtable则不允许null作为key。

(3)、两者计算hash的方法不同:Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模。而HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸。

(4)、HashMap和Hashtable的底层实现都是数组+链表结构实现。

(5)、HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类

(6)、HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。HashMap扩容时是当前容量翻倍即:capacity*2,Hashtable扩容时是容量翻倍+1即:capacity*2+1。

3. HashSet和HashMap、Hashtable的区别:

    除开HashMap和Hashtable外,还有一个hash集合HashSet,有所区别的是HashSet不是key value结构,仅仅是存储不重复的元素,相当于简化版的HashMap,只是包含HashMap中的key而已

通过查看源码也证实了这一点,HashSet内部就是使用HashMap实现,只不过HashSet里面的HashMap所有的value都是同一个Object而已,因此HashSet也是非线程安全的,至于HashSet和Hashtable的区别,HashSet就是个简化的HashMap的,所以你懂的。

4. HashSet和HashMap、Hashtable的遍历:

    http://www.cnblogs.com/xd502djj/archive/2013/03/15/2961717.html

5. 引用的四种类型:

    (1)、强引用:强引用就是指在程序代码之中普遍存在的只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。

(2)、软引用:软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。软引用可以和一个引用队列联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。

(3)、弱引用:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

(4)、虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

 

6.Java泛型:

定义:所谓的泛型就是将类型参数化。在使用或者调用时传入参数类型。

作用:(1)、最重要的作用就是在编写容器类时,使容器类可以持有多种类型的对象。

     (2)、可以定义泛型类和泛型接口。

(3)、不需要强制类型转换。

类型擦除:Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。

7.Java运行时可以判断类型吗?

    可以。

    (1)、RTTI:运行时类型识别。RTTI维护类的相关信息,其主要功能是通过Class类来实现的,一个Class对象对应一个类。Java中的每一个对象都有相对应的Class类对象,无论对引用进行怎样的类型转换,对象本身对应的class对象还是同一个。

    (2)、反射:在运行状态,对任意一个类都能知道它的属性和方法,对于任意一个对象都能调用它的方法和属性。反射可以在运行时判断任意一个对象所属的类,构造任意一个类的对象,判断任意一个类的成员变量和方法,调用任意一个对象的方法和属性,生成动态代理。

8.动态代理:

    要使用动态代理首先要定义一个接口,然后定义一个接口的实现类,另外在定义一个代理类,通过将实现类的对象传递给代理类生成实现类的代理,通过该代理调用接口中的方法,不会直接调用实现类中的方法,而是用过代理利用反射机制调用。

    动态代理主要用来做方法的增强,让你可以在不修改源码的情况下,增强一些方法,在方法执行前后做任何你想做的事情。其实是代理模式的一种巧妙实现。

9.ConcurrentHashMap:

    它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想(JDK7与JDK8中HashMap的实现),但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。

(1)、重要属性:

    sizeCtl是一个控制标识符,负数代表正在进行初始化或扩容操作;-1代表正在初始化;-N 表示有N-1个线程正在进行扩容操作;正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小。

    (2)、重要的类:

    Node是最核心的内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。它与HashMap中的定义很相似,但是但是有一些差别它对value和next属性设置了volatile同步锁(与JDK7的Segment相同),它不允许调用setValue方法直接改变Node的value域,它增加了find方法辅助map.get()方法。

TreeNode,树节点类,另外一个核心的数据结构。当链表长度过长的时候,会转换为TreeNode。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。而且TreeNode在ConcurrentHashMap集成自Node类,而并非HashMap中的集成自LinkedHashMap.Entry<K,V>类,也就是说TreeNode带有next指针,这样做的目的是方便基于TreeBin的访问。

    TreeBin,这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。

ForwardingNode,一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1. 这里面定义的find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找。

(3)、思想:

在ConcurrentHashMap中,随处可以看到大量使用了U.compareAndSwapXXX的方法,这个方法是利用一个CAS算法实现无锁化的修改值的操作,他可以大大降低锁代理的性能消耗。这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。

(4)、三个核心方法:

tabAt(Node<K,V>[]tab, int i)获得在i位置上的Node节点

casTabAt(Node<K,V>[]tab, int i, Node<K,V> c, Node<K,V> v)利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少。在CAS算法中,会比较内存中的值与你指定的这个值是否相等,如果相等才接受你的修改,否则拒绝你的修改。因此当前线程中的值并不是最新的值,这种修改可能会覆盖掉其他线程的修改结果。

setTabAt(Node<K,V>[]tab, int i, Node<K,V> v)利用volatile方法设置节点位置的值。

(5)、put操作:

ConcurrentHashMap不允许key或value为null值。另外由于涉及到多线程,put方法就要复杂一点。在多线程中可能有以下两个情况:

a.如果一个或多个线程正在对ConcurrentHashMap进行扩容操作,当前线程也要进入扩容的操作中。这个扩容的操作之所以能被检测到,是因为transfer方法中在空结点上插入forward节点,如果检测到需要插入的位置被forward节点占有,就帮助进行扩容;

b.如果检测到要插入的节点是非空且不是forward节点,就对这个节点加锁,这样就保证了线程安全。尽管这个有一些影响效率,但是还是会比hashTable的synchronized要好得多。

整体流程就是首先定义不允许key或value为null,对于每一个放入的值,首先利用对key的hashcode进行一次hash计算,由此来确定这个值在table中的位置。

如果这个位置是空的,那么直接放入,而且不需要加锁操作。

如果这个位置存在结点,说明发生了hash碰撞,首先判断这个节点的类型。如果是链表节点(fh>0),则得到的结点就是hash值相同的节点组成的链表的头节点。需要依次向后遍历确定这个新加入的值所在位置。如果遇到hash值与key值都与新加入节点是一致的情况,则只需要更新value值即可。否则依次向后遍历,直到链表尾插入这个结点。如果加入这个节点以后链表长度大于8,就把这个链表转换成红黑树。如果这个节点的类型已经是树节点的话,直接调用树节点的插入方法进行插入新的值。

10.Java虚拟机的工作机制:

    Java源程序经过编译后生成字节码文件,字节码由解释器解释执行。虚拟将将每一条要解释的字节码送给解释器,解释器将他翻译成特定机器上的机器码,在特定机器上执行。

    Java字节码的执行方式由两种:

    即时编译(JIT):当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码(保存在方法区),并进行各层次的优化,完成这项任务的正是JIT编译器。提高效率。

    解释执行(javac编译):每次解释并执行一小段代码。节约内存。

11.类加载机制:

    (1)、类加载的步骤:加载,连接(验证、准备、解析),初始化,使用,卸载。

    加载:根据类的全限定名获取定义此类的二进制字节流,并将字节流代表的静态存储结构转化成方法区的运行时数据结构。

    验证:主要包括对字节码、元数据、文件格式,符号引用等的验证。

    准备:为类变量分配内存并设置初始值。该初始值是 “零值”,而不是Java代码中指定的初始值。

    解析:将常量池中的符号引用转换为直接引用。

    初始化:真正开始执行类中定义的JAVA程序代码。

12.类加载的时机:

    虚拟机规范规定的有且仅有的5种情况:

    (1)、new一个对象时,访问一个类的类变量或者调用一个类的静态方法时。

    (2)、对类进行反射调用时。

    (3)、要加载的类的父类还没有被加载时会加载其父类。

    (4)、启动虚拟机时,会加载包含main方法的类。

(5)、使用JDK1.7的动态语言支持时,若一个java.lang.invoke.MethodHandler实例最后的解析结果的方法句柄所对应的类没有加载,则会先触发起初始化。

特殊情况:

子类引用父类的静态变量,不会导致子类初始化。

通过数组定义引用类,不会触发此类的初始化。即ObjectA[]objA = new ObjectA[6];

引用常量时,不会触发该类的初始化。因为用final修饰某个类变量时,它的值在编译时就已经确定好放入常量池了,所以在访问该类变量时,等于直接从常量池中获取,并没有初始化该类。

13.类加载器:

    类加载器就是完成类加载过程的一段代码。Jvm中主要有四种主要的类加载器:

    (1)、启动类加载器:负责加载/jre/lib/rt.jar中的类。

    (2)、扩展类加载器:负责加载<JAVA_HOME>/lib/ext/下面的多个jar包中的类。

    (3)、应用程序类加载器:负责加载classpath下的jar包,即工程中导入的jar包。

    (4)、用户自定义的类加载器。

其中启动类加载器位于Java虚拟机的内部,是由c++代码实现的。其余的类加载器都位于Java虚拟机的外部,继承自ClassLoader类。

每一个类加载器都有自己独立的名称空间,任意一个类都需要加载它的加载器和这个类本身来确定他在虚拟机中的唯一性。

14.双亲委派模型:

    就是类加载器额一种层次关系,它要求除了顶层的启动类加载器外每一个类加载器都应该有自己的父加载器,该父子关系是通过组合实现的而非继承。

    若某一个类加载器接收到了一个加载请求,它首先将这个请求委托给自己的父加载器执行,只有当父加载器反馈自己无法完成这个请求时,该类加载器才会尝试自己去加载。

15.Java的内存区域:

   

方法区

虚拟机栈

本地方法栈

程序计数器

    程序计数器: 就是当前线程的行号指示器,用来指出下一条要执行的字节码指令的地址,没有规定OutOfMemory异常。

    虚拟机栈:线程私有的,用来执行Java方法。Java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。一个方法从调用到执行完毕的过程对应着一个栈帧从入栈到出栈的过程。

    本地方法栈:与虚拟机栈的作用类似,不过是用来执行本地方法的。

    Java堆:Java虚拟机管理的最大的一块内存,是所有线程共享的一块区域。也是收集器管理的主要区域,对象和数组的内存分配都在堆上进行。

    方法区:所有线程共享的一块区域,存放已经被虚拟机加载的类信息,常量,静态变量、以及即时编译翻译的机器码。内有运行时常量池。

16.对象的创建:

    (1)、首先检查对应的类是否已经加载,如果没有则执行类加载机制。

    (2)、类加载检查通过后就为新生对象分配内存。

    (3)、内存分配完成后,虚拟机将分配到的内存空间都初始化成零值。

    (4)、虚拟机对对象头进行必要的设置。(对象属于哪个类的实例,元数据,哈希码等)

    (5)、最后按照程序员的意愿进行初始化(Java代码)。

17.对象的内存布局:

    对象在内存中的布局大致分为3部分:

    (1)、对象头:主要分运行时数据(哈希码、GC分代年龄等信息)和类型指针(指明对象时哪个类的实例)两部分。

    (2)、实例数据:程序代码定义的相关信息。

    (3)、对齐补充:虚拟机规定对象大小时8字节的整数倍,对象头正好是8字节的整数倍,当实例数据不符合要求时,用对齐补充补齐。

18.创建对象时的内存分配策略:

    (1)、内存规整时可以采用指针碰撞,分配过的内存放在一边,没有分配过的内存放在另一边,中间有一个指针,每当分配一部分内存时,指针就移动相应的距离。

(2)、内存不规整时采用空闲列表,虚拟机维护一个列表,记录那些内存可用,需要分配内存是就在列表中查找。(CMS收集器采用的策略)

Ps:分配内存时会产生并发问题,解决的问题是及本地内存缓冲(TLAB),虚拟机为每个线程预先分配一小块内存,创建对象或者数组是在这一小块内存上进行分配,这个过程不需要考虑并发。当这一块内存不够用时,在向外扩展,在扩展时才需要考虑并发问题。

19.判断对象已死?

(1)、引用计数算法:

    很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1;任何时刻计数器都为0的对象就是不可能再被使用的。

引用计数算法的实现简单,判断效率也很高,在大部分情况下它都是一个不错的算法。但是Java语言中没有选用引用计数算法来管理内存,其中最主要的一个原因是它很难解决对象之间相互循环引用的问题

(2)、可达性分析:

这个算法的基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GCRoots是不可达的,所以它们将会判定为是可回收对象。在Java语言里,可作为GC Roots对象的包括如下几种:
   a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
    b.方法区中的类静态属性引用的对象
    c.方法区中的常量引用的对象
    d.本地方法栈中JNI的引用的对象 

20.垃圾收集算法:

    (1)、标记-清除算法:最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。它有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

    (2)、复制算法:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。实际应用中内存并非是按照1:1的比例划分的。

    (3)、标记-整理:该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

(4)、分代收集:分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(TenuredGeneration)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。(Eden:From Survivor:To Survivor = 8:1:1)

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法。

注意,在堆区之外还有一个代就是永久代(PermanetGeneration),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

21.CMS收集器:

    CMS收集器是一种以获取最短停顿时间为目标的收集器,基于“标记-清除”算法,算法实现的四个步骤:

    (1)、初始标记:需要停止用户程序的运行,标记与GC Root没用引用链的对象。

    (2)、并发标记:与用户程序并发执行,在初始标记的基础上继续标记。

    (3)、重新标记:标记(2)过程中发生变化的对象。

    (4)、并发清除:清除已标记的对象,与用户程序并发执行。

缺点:

    (1)、与用户程序并发执行,会占用CPU资源,导致用户程序变慢。

    (2)、无法清理浮动垃圾。

(3)、产生内存碎片。

22.Java中的代:


    (1)、新生代:主要用来存放新生对象。

    (2)、老年代:用来存放生命周期比较长的对象。

    (3)、永久代:主要用来存放类信息或者元数据。

一般来说:Young  :  Old = 1:2

23.Java中对象的内存分配:

(1)、新生对象优先在新生代的Eden区域中分配,若空间不够分配,则触发一次Minor GC。

(2)、大对象直接进入老年代,即需要大量连续内存的Java对象。

(3)、长期存活的对象进入老年代:

    a.虚拟机为每一个对象定义一个年龄计数器,若对象经历一次Minor GC后依然存活,则年龄计数器加1,当该值增大到一定程度(默认15),对象会晋升到老年代。

    b.若Survivor空间内相同年龄的对象的大小总和超过Survivor空间的一半,则年龄大于该值得对象进入老年代。

    (4)、空间分配担保:在执行复制算法时,若存活对象的大小超过了To Survivor空间的大小,则超出容量的部分对象进入老年代,如果老年代没有足够的空间,则触发一次Full GC来让老年代腾出足够的空间。



未完待续!!!!

1 0
原创粉丝点击