面试资料总结 :
java部分:
- Java数据类型的分类:基本数据类型(8种)和引用数据类型
- 基本数据类型: byte, short, int, long, float, double,char,Boolean
- 引用对象类型:对象、数组,默认的初始值为null
-
按作用域分:成员变量(类变量、实例变量)、局部变量
-
static修饰符表示静态的,在类加载时Jvm会把它放到方法区,被本类以及本类的所有实例所共用。在编译后所分配的内存会一直存在,直到程序退出内存才会释放这个空间。如果一个被所有实例共用的方法被申明为static,就可以节省空间,不用每个实例初始化的时候都分配内存。
-
java类被加载过程:
类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:
①加载(以二进制形式来生成Class对象)
②链接(又分为验证、准备和解析)
校验:检查导入类或接口的二进制数据的正确性;
准备:给类的静态变量分配并初始化存储空间;
解析:将符号引用转成直接引用;
③初始化(激活类的静态变量和静态代码块、初始化Java代码)
静态代码块就是在类加载器加载对象时,要执行的一组语句。静态块只会在类加载到内存中的时候执行一次,位置可以随便放,如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
静态类:
只能在内部类中定义静态类,静态内部类与外层类绑定,即使没有创建外层类的对象,它一样存在。静态类的方法可以是静态的方法也可以是非静态的方法,静态的方法可以在外层通过静态类调用,而非静态的方法必须要创建类的对象之后才能调用。只能引用外部类的static成员变量(也就是类变量),当然前提是满足修饰关键字(public等)的可见性要求。
如果一个内部类不是被定义成静态内部类,那么在定义成员变量或者成员方法的时候,是不能够被定义成静态的。
- 重写的要求:
- 方法名相同,参数类型相同
- 子类返回类型小于等于父类方法返回类型
- 子类抛出异常小于等于父类方法抛出异常
- 子类访问权限大于等于父类方法访问权限
- Collection是集合继承结构中的顶层接口(interface),其是Iterable的子类。
ArrayList
:线程不同步。默认初始容量为10,当数组大小不足时增长率为当前长度的50%
。
Vector
:线程同步。默认初始容量为10,当数组大小不足时增长率为当前长度的100%
。它的同步是通过Iterator
方法加synchronized
实现的。
LinkedList
:线程不同步。双端队列形式。
Stack
:线程同步。继承自Vector
,添加了几个方法来完成栈的功能。
Set
:Set是一种不包含重复元素的Collection,Set最多只有一个null元素。
HashSet
:线程不同步,内部使用HashMap
进行数据存储,提供的方法基本都是调用HashMap
的方法,所以两者本质是一样的。集合元素可以为NULL
。
- hashmap:每次扩大一倍。当单个桶中元素个数大于等于8时,链表实现改为红黑树实现;当元素个数小于6时,变回链表实现。由此来防止hashCode攻击。
-
HashTable
:线程安全,HashMap的迭代器(Iterator)是fail-fast
迭代器。HashTable不能存储NULL的key和value。
-
静态分派主要针对重载,方法调用时如何选择。静态类型是在编译时可知的,而动态类型是在运行时可知的,编译器不能知道一个变量的实际类型是什么。编译器在重载时候通过参数的静态类型而不是实际类型作为判断依据。并且静态类型在编译时是可知的,所以编译器根据重载的参数的静态类型进行方法选择。
-
动态分派主要针对重写,使用
invokevirtual
指令调用。invokevirtual
指令多态查找过程:
找到操作数栈顶的第一个元素所指向的对象的实际类型,记为C。
如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果权限校验不通过,返回java.lang.IllegalAccessError异常。
否则,按照继承关系从下往上一次对C的各个父类进行第2步的搜索和验证过程。
如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError异常。
虚拟机动态分派的实现:
由于动态分派是非常繁琐的动作,而且动态分派的方法版本选择需要考虑运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实现中基于性能的考虑,在方法区中建立一个虚方法表(
invokeinterface
有接口方法表),来提高性能。
虚方法表中存放各个方法的实际入口地址。如果某个方法在子类没有重写,那么子类的虚方法表里的入口和父类入口一致,如果子类重写了这个方法,那么子类方法表中的地址会被替换为子类实现版本的入口地址。
- 类型擦除:
1、Java中的泛型基本上都是在编译器这个层次来实现的,在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉,这个过程就称为类型擦除。如在代码中定义的
List<Object>
和List<String>
等类型,在编译之后都会变成List
。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。
-
通配符所代表的其实是一组类型,但具体的类型是未知的。因为对于
List<?>
中的元素只能用Object
来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。 如 List<? extends Number>说明List中可能包含的元素类型是Number及其子类。而List<? super Number>则说明List中包含的是Number及其父类。 -
根据
Liskov替换原则
,子类是可以替换父类的。但是反过来的话,即用父类的引用替换子类引用的时候,就需要进行强制类型转换。 -
String a = "123";
String b = "123";
创建这两个常量时,a、b是对数值“123”的引用,a、b指向的内存是一样的,因为a、b是常量,而不是对象;如果是两个对象的话,即使内部数据一样,但是表示的还是不同的两片内存 -
阻塞状态的分类:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
- Java 提供了三种创建线程的方法(要知道具体怎么操作):
- 通过实现 Runnable 接口;
- 通过继承 Thread 类本身;
- 通过 Callable 和 Future 创建线程。
- classLoader:
程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。
- classLoader原理:
ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。
- Java虚拟机为什么要使用双亲委托模型?
因为这样可以避免重复加载。
安全因素,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。
- JVM在搜索类的时候,又是如何判定两个class是相同的呢?
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。
- 垃圾收集算法
- 标记-清除算法
分标记和清除两个阶段:首先标记处所需要回收的对象,在标记完成后统一回收所有被标记的对象。
它有两点不足:一个效率问题,标记和清除过程都效率不高;一个是空间问题,标记清除之后会产生大量不连续的内存碎片(类似于我们电脑的磁盘碎片),空间碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。- 复制算法
为了解决效率问题,出现了“复制”算法,他将可用内存按容量划分为大小相等的两块,每次只需要使用其中一块。当一块内存用完了,将还存活的对象复制到另一块上面,然后再把刚刚用完的内存空间一次清理掉。这样就解决了内存碎片问题,但是代价就是可以用内容就缩小为原来的一半。- 标记-整理算法
复制算法在对象存活率较高时就会进行频繁的复制操作,效率将降低。因此又有了标记-整理算法,标记过程同标记-清除算法,但是在后续步骤不是直接对对象进行清理,而是让所有存活的对象都向一侧移动,然后直接清理掉端边界以外的内存。- 分代收集法
当前商业虚拟机的GC都是采用分代收集算法
为了增大垃圾收集的效率,所以JVM将堆进行分代,分为不同的部分,一般有三部分,新生代,老年代和永久代(在新的版本中已经将永久代废弃,引入了元空间的概念,永久代使用的是JVM内存而元空间直接使用物理内存):- 新生代(对应minor GC)
所有新new出来的对象都会最先出现在新生代中,当新生代这部分内存满了之后,就会发起一次垃圾收集事件,这种发生在新生代的垃圾收集称为Minor collections。这种收集通常比较快,因为新生代的大部分对象都是需要回收的,那些暂时无法回收的就会被移动到老年代。- 老年代(对应Full GC)
老年代用来存储那些存活时间较长的对象。一般来说,我们会给新生代的对象限定一个存活的时间,当达到这个时间还没有被收集的时候就会被移动到老年代中。- 永久代
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。