android程序员在面试时都会被问到Java方面的知识,本文整理了部分Java方面的面试题,如下:
0、Java垃圾回收和System.gc的关系
Java根据垃圾收集算法,周期性的进行垃圾回收,回收哪些无用的对象。以下情况会触发GC:
应用程序空闲时,即没有程序在运行,会GC。因为GC线程的优先级较低,CPU较忙时一般不会执行,以下场景除外:堆内存不足。
Java堆内存不足时会GC,如果一次GC后内存还不够,会触发第二次GC,第二次还不够就会触发OOM。
System.gc只是呼叫JVM进行垃圾回收,但这只是建议而已,不一定立刻执行。
1、Java元注解有哪些
元注解指的是对注解进行注解的注解,有以下四种:
@Target :表示该注解用在什么地方,可能值在枚举类ElementType中,包括 ElemrntType.CONSTRUCTOR----------构造器
ElemrntType.FIELD----------域声明
ElemrntType.METHOD--------------方法
@Retention:在什么地方保存该注解信息,包括:
RetentionPolicy.SOURCE----------------注解在编译时就被丢弃
RetentionPolicy.CLASS----------------注解在编译时保留但在VM中丢弃
RetentionPolicy.RUNTIME----------------注解在运行时一直存在,即可以通过反射进行调研
@Documented:将此注解包含在javadoc 中,即该注解可以被javadoc生成文档
@Inherited: 允许子类继承父类中的注解
2、Synchronized详解
Java用Synchronized来实现线程同步,该关键字可以加在方法上,也可以加在对象上,如果他的作用的对象是非静态的,则它取得的锁是对象;如果作用的对象是静态方法或者类,则它取得的锁是类对象(Class对象)。每个对象有一把锁,谁取得这个锁,谁就可以访问对象的方法。
可重入性:假设类A有两个同步方法a()、b(),
线程T在访问A的方法a()时会先获取A的锁,如果这时候a()方法调用了b()方法,由于b()方法也是同步的,所以也需要获取A的锁,由于在调用a()方法时已经获取到A的对象锁了,调用b()时就可以直接进入方法不会导致死锁,这就是可重入性。
不可继承性:如果类A的b()方法是同步的,如果子类B的b()方法不是同步的,那么子类B的b()方法就不具备同步特性。
对象锁和类锁:
Java的所有对象都含有一个锁,这个锁由JVM自动获取和释放,线程进入Synchronized代码块会去获取该对象的锁,如果已经有线程获取了对象锁,那个当前线程就会等待。对象锁是控制实例方法的锁,类锁是用来控制静态代码的同步的,相当于Class对象的锁。类锁和对象锁不同,一个是类的Class对象的锁,一个是类的实例对象的锁,也就是说一个线程访问静态的Synchronized时,另外的线程可以访问对象实例的Synchronized方法。
3、Java引用类型
强引用----------- 垃圾收集器不会收集它,宁可抛出OOM
软引用(Soft Reference)-----------当垃圾收集器工作时,如果内存足够,就不回收只被软引用关联的对象,如果内存不够,则会进行回收。一旦被回收,这个软引用就会被加入到关联的ReferenceQueue中。
弱引用(Weak Reference) -------------- 对象只能生存到下一次GC,当垃圾收集器工作时无论内存是否足够都会收集只被弱引用关联的对象。一旦一个弱引用被回收,便会加入一个注册引用队列ReferenceQueue中。
虚引用(Phantom Reference) ------------ 最弱的引用类型,get方法返回null。主要用来跟踪对象被垃圾回收的状态。虚引用被回收时会放入ReferenceQueue,从而达到跟踪对象垃圾回收的作用。
ReferenceQueue-----------是这样的一个对象,当一个obj被gc掉之后,其相应的包装类,即ref对象会被放入queue中。我们可以从queue中获取到相应的对象信息,同时进行额外的处理。比如反向操作,数据清理等。
WeakReference weakReference=new WeakReference(obj,ReferenceQueue)将引用队列传入即可进行观察。
4、垃圾收集算法
可达性分析算法,通过一系列作为GCRoot的对象,虚拟机从GCRoot沿着对象传递的引用链来寻找,如果找不到,就认为对象不可达,即该对象可以被回收。
在垃圾收集器对对象回收之前首先对GCroot不可达的对象进行第一次标记,然后进行筛选(判断该对象是否有必要执行finalize()方法,finalize()方法只能被调用一次。如果有必要执行时,就会放在一个处理队列里等待虚拟机处理,虚拟机会触发该对象的finalize(),但是不承诺会等待它结束,防止他进入死循环而阻塞后边的进程,然后在finalize()执行完之后,在对那些仍旧没有被引用的对象进行第二次标记,然后开始进行回收,换而言之,该对象可以在自己的finalize()方法里让别人引用自己,这样自己就不会被回收了,从而拯救自己。
判断一个类是否可以被回收有以下三个条件:该类的所有实例已经被回收,该类的ClassLoader已经被回收,该类的Class对象没有引用。
垃圾收集算法一般分为:
标记-清除算法-----会产生不联系的内存碎片,导致以后想分配大内存时,明明剩余的总内存足够,确不能分配。
复制算法----将空间分为两个部分AB,当A快用完时将A中存活的对象复制到B,然后回收A。
标记-整理算法----类似于标记清除算法,只是在其基础上将回收完还存活的对象往一侧移动,这样剩余的空间就是连续的了。
对应Java堆来说,使用分代收集,新生代使用复制算法,老年代使用标记清除或者标记整理。
可以作为gcroot的对象如下:虚拟机栈中本地变量引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈jni引用的对象。
5、Java线程池
Java线程池使用ThreadPoolExecutor类实现,构造函数如下:
corePoolSize:线程池中一直存活的线程最小数量,也叫核心线程。
maximumPoolSize:线程池能够容纳的最大线程数,当提交一个任务到线程池,如果处于RUNNING的线程数量少于corePoolSize,那么即使一些非核心线程处于空闲等待状态,系统也会创建一个新的线程来处理这个任务。如果处于运行状态的线程数大于corePoolSize,但又小于maximumPoolSize,系统就会判断线程池内部阻塞队列workQueue是否有空位置,如果有就先存入阻塞队列,如果没有就创建一个新线程来执行这个任务。如果将corePoolSize和maximumPoolSize设置为相同,那个该线程池就是一个容量固定的线程池。
keepAliveTime:空闲线程处于等待的超时时间,当总线程数大于corePoolSize且allowCoreThreadTimeOut为false时,多出来的空闲等待的线程就会开始计算各自的等待时间,等待时间超过keepAliveTime该线程就会停止工作。如果keepAliveTime为true,那么不论核心线程还是非核心线程都会受到keepAliveTime的制约。
workQueue:是一个阻塞队列,用于保存等待任务。当提交一个任务到线程池后,线程池会根据当前运行的线程数量做出相应处理,方式如下:
1、如果运行的线程数少于corePoolSize,则直接创建一个新的core线程来执行任务
2、如果运行的线程数大于等于corePoolSize,那么线程池会优先将该任务提交到workQueue队列中
3、基于第2点如果任务加入到workQueue(队列满了)失败了,则查看当前运行的线程数和maximumPoolSize的大小关系,如果当前运行线程数小于maximumPoolSize,则创建一个新线程来执行任务;如果大于或等于maximumPoolSize,说明线程池满了,就直接拒绝该任务。队列提交新任务的三种常见切换策略:
a、直接切换:常用SynchronousQueue同步队列,队列内部不会存储元素,每次插入都会进入阻塞状态,知道别的线程执行了删除操作;反之一样。 “直接切换”的意思就是, 处理方式由“将任务暂时存入队列”直接切换为“新建一个线程来处理该任务”。这种策略适合用来处理多个有相互依赖关系的任务,因为该策略可以避免这些任务因一个没有及时处理而导致依赖于该任务的其他任务也不能及时处理而造成的锁定效果。因为这种策略的目的是要让几乎每一个新提交的任务都能得到立即处理, 所以这种策略通常要求最大线程数 maximumPoolSizes是无界的(即:Integer.MAX_VALUE)。Executors.newCachedThreadPool() 使用了这个队列。
b、无界队列:使用 Integer.MAX_VALUE作为默认容量,例如LinkedBlockingQueue,这种情况下maximumPoolSize的设置其实没有效果,线程池最大的线程数就是corePoolSize,超过之后新任务总能存入队列中。Executors.newFixedThreadPool() 使用了这个队列
c、有界队列:限制队列的长度。例如ArrayBlockingQueue
threadFactory:线程工厂,用于创建线程,可以给线程设置特定的属性,例如优先级、名字等。
handler:提交线程到线程池失败后的处理策略。当线程池处于下面任意状态时就会拒绝服务:
1、线程池处于SHUNDOWN状态,不论线程池和阻塞队列是否满了,都会拒绝服务。
2、当线程池的所有线程都处于运行状态(corePoolSize和maximumPoolSize都满了)并且线程池中的阻塞队列已经满了
线程池默认给我们提供了以下处理策略:
1、 AbortPolicy:直接抛出RejectedExecutionException 异常,默认就是这种实现方式。
2、CallerRunsPolicy
3、DiscardPolicy:直接不执行新提交的任务。
4、DiscardOldestPolicy:当线程池已经关闭,就不执行这个任务了;当线程池没有关闭,会将阻塞中的队首元素一次,然后新提交这个任务到队尾。
6、HashMap的结构
采用Key-Value形式存储,如果Node数量小于8,Node的结构就是单链表,如果Node的数量大于8,Node的结构就是TreeNode,红黑树。
7、String、StringBuffer、StringBuilder的区别
String是字符串常量。Sting的+操作,其实在编译时转化成了StringBuilder操作。
StringBuffer是字符串变量,是线程安全的,内部通过Synchronized实现线程同步。
StringBuilder是字符串变量,是非线程安全的。
8、数组和链表的区别
数组将数据在内存中连续存放,每个元素占用的内存相同,可以通过下标迅速访问里面的元素。数组的插入和删除需要移动大量的元素。如果应用需要快速访问元素且很少做插入和删除操作,就应该使用数组。
链表恰巧相反,链表中的元素存储不是有序的,而是通过指针联系在一起,例如上个元素有个指针指向下个元素,以此类推,直到最后一个元素。如果要访问链表,需要从头开始根据指针进行查找,但是要想删除元素,不需要移动大量元素,只需要修改指针即可。如果应用经常做插入和删除操作,不常访问元素,就使用链表。
9、Volatile关键字
http://www.importnew.com/24082.html
Java内存模型规定了所以变量必须存在主内存中。每条线程有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主存拷贝而来的)。线程对变量的读写都在工作内存中,不同线程之间无法之间访问对方的工作内存中的变量,线程将变量的传递通过主内存来完成。基于这种内存模型,便产生了多线程编程中的数据“脏读”问题。
例如在执行i=10;i=i+1的操作时,执行线程首先在自己的工作内存对变量进行赋值操作,然后再写入主存,而不是直接写入到主存。当两个线程同时执行这段代码的时候,我们希望执行完成后i的值为12,但实际情况确不一定是这样,存在了以下场景:
初始时,两个线程分别将i=10的值写入到自己的工作内存,然后线程1对i执行加1操作,然后线程1将i=11写入到主存;如果当线程1将i=11写入之前,线程2执行i+1时,由于主存中的i=10,所以线程2取到i=10,然后执行i+1,执行完成后i的值是11,最后主存中的i=11。
原子性:原子性指的是一个操作要么全部执行,要么全不执行。Java中看似很简单的x++操作,其实并不是原子操作,编译后产生了多条指令。Java内存模型只保证基本的读取和赋值的原子性,其他的操作需要程序员自己保证,可以通过synchronized、lock等手段。
可见性:可见性指的是多个线程同时访问某个变量时,一个线程修改这个变量的值,其余线程应该能够马上看到修改后的值。Java通过volatile关键字来保证可见性,它会保证修改的值会被立即更新到主存。
有序性:Java在编译的时候,为了提高程序的运行效率,会对指令进行重排,但执行结果不会影响。
理解Volatile的工作:
1、保证了可见性:一旦变量声明为volatile,对变量的修改都在主内存,那么一个线程修改变量后,该修改后的变量对其他线程立即可见。
线程2通过设置stop想要达到中断线程1的目的并不一定能够实现,线程1将stop变量保存到自己的工作内存,线程2设置stop后线程1还没来得及写入主存就去干其余事了。这种场景下就需要使用volatile变量来修饰stop变量,保证线程2修改stop后立即将stop变量强制写入主存;并且导致线程1的工作线程中的stop变量的缓存值立即无效;由于线程1工作内存中的stop变量已经无效,所以线程1再次读取stop变量的时候会去主存取数据。
2、保证有序性:Volatile关键帧禁止指令重排,所以在一定程度上保证有序。
3、不保证原子性:volatile不能保证原子性。
10、JVM内存结构
https://www.cnblogs.com/SaraMoring/p/5713732.html
线程栈(虚拟机栈):每个线程都有自己的线程栈【线程栈的大小可以通过JVM的-Xss参数进行设置,32位系统在JDK5之后,每个线程栈的大小为1M】,线程栈中存放的数据都是线程私有的。栈里面存放着栈帧,代表一个函数调用,栈帧里面存放着函数的形参、局部变量、返回地址等。
堆:所有的线程都共享同一个堆空间,堆里面存放的是对象数据(排除基本类型数据和引用以外的数据)。
方法区:线程共享的数据区。
本地方法栈:为Java调用本地方法(JNI)提供的。
程序计数器:在CPU中有个程序计数器,存放了下一条CPU指令的地址。JAVA虚拟机没有使用CPU的程序计数器,而是在虚拟机内存中开辟了一块区域来模拟CPU的程序计数器功能。JVM中,每个线程都有自己的程序计数器,JVM的程序计数器里面存放的是当前正在执行的字节码地址,而不是下一条。
Java虚拟机栈:虚拟机栈也是线程私有的,描述的是Java方法执行的内存模型:每个方法执行的时候都会创建一个栈帧,用于保存局部变量表、操作数栈等。方法的执行对应着一个栈帧从入栈到出栈的过程。局部变量表中存放着编译期间可以知道的基本类型(int char…)、对象引用。局部变量表所需要的内存在编译的时候就已经分配好了,当进入一个方法时,这个方法在栈帧中需要多少局部变量空间是确定好的,方法运行期间不会改变局部变量表的大小。如果方法递归严重,虚拟机栈可能会保存StackOverFollowError的异常。
本地方法栈:本地方法栈存放了JNI调用的栈帧。
Java堆:堆是所有线程共享的区域,是用来存放对象的,几乎所有的实例对象都在里面分配,堆是垃圾收集器主要工作的地方。
方法区:方法区也叫永久代,是线程共享的区域,存储了虚拟机加载的类信息、常量池等。常量池是方法区的一部分,存放着编译期间已经确定好的常量(字面量,static final类型)。
JVM共享的数据区域如下:
分为堆(新生代、老年代)、永久代(方法区),新生代可以分为Eden区(存放新生对象)、Survivor幸存区(存放垃圾回收后幸存的对象);永久代(方法区)管理类信息、常量、静态对象等。
Jvm的堆的垃圾收集采用分代收集策略,新生代采用复制算法,老年代采用标记整理算法。
复制算法:将内存分为A/B部分:
1. 新生对象被分配到A块中未使用的内存当中。当A块的内存用完了, 把A块的存活对象对象复制到B块。
2.清理A块所有对象。
3.新生对象被分配的B块中未使用的内存当中。当B块的内存用完了, 把B块的存活对象对象复制到A块。
4.清理B块所有对象。
5. goto 1
11、深拷贝和浅拷贝
浅拷贝:创建一个新的实例,将旧实例的成员逐个复制给新的实例,类似于复写clone方法,引用变量没有重新创建。
深拷贝:实现类的拷贝构造函数时,不仅复制所有的非引用变量,还要为引用变量创建新的实例。
12、泛型的extend和super的区别
假设有这样一个盘子,实现如下。
现在定义一个水果盘子,理论上可以如下实现:
逻辑上装水果的盘子肯定可以装苹果,但实际上java编译器却不允许,编译器认为苹果是一种水果,但装苹果的盘子并不一定是装水果的盘子。于是就有了extend和super的概念。
extends:上界通配符,如下所示,可以存放Fruit的所有子类:
Plate p = new Plate();//存放苹果
Plate p = new Plate();//存放橘子
Note:使用extends通配符后,Plate的set方法不能使用,试想一下,Plate <? extends Fruit> p可能是Apple类型的,也可能是Origin类型的,如果p指向Apple类型,而set进去一个Origin类型,在运行时就会报错,所以不能set。Get方法是可以使用的,因为get出来的肯定是一种Fruit。
super:下界通配符,存放Fruit的父类。
Plate p = new Plate();
Plate p = new Plate();
Note:使用super通配符后,Plate的get方法不能使用,试想一下,Plate <? super Fruit> p可能是Food类型的,也可能是Object类型的,如果p指向Food类型,而get的时候使用Fruit f = p.get(),Food并不一定是Fruit在运行时就会报错,所以不能get。set方法是可以使用的,因为set进去Fruit,肯定是Fruit的父类。
类型擦除:Java的泛型是在编译层实现的,编译完成后class里面会使用原始类型。例如List、List在编译完成后,都变成了List,只能存放Object类型。如果类型变量有限定的话,那么原始类型就用第一个边界类型变量来替换,例如Plate<?extends Fruit>就被擦除成Plate<Fruit>。
桥接模式
13、HashMap和HashTable的区别
HashTable是基于陈旧的Dictionary类,完成Map接口;HashMap继承与AbstractMap。
HashTable的方法是同步的,通过Synchronized实现线程的安全;HashMap是非同步的。
HashTable不允许null类型的key和value进行存储;HashMap允许null类型的key或者value。
HashTable使用的是Enumeration遍历,HashMap使用的是Iterator。
14、HashMap和HashSet的区别
HashSet实现了Set接口,不允许有重复的值,HashSet内部也是使用HashMap来实现的,只是使用插入进去的值作为Key,使用固定的PRESENT作为value。
HashMap的put方法如下:
如果HashSet插入进去的值的hashCode已经在map中存在,所以会直接更新Node的值。
HashMap判断重复的过程:根据Key计算hashCode,根据hashCode查询Node列表,如果找到Node列表就说明可能存在重复。接着比较Node的Key和put传入的Key是否完全一致(引用一致、equals返回true、hashCode一致),HashSet在插入元素的时候,如果插入相同的对象,这边就完全一致。如果完全一致,说明插入的元素存在重复,直接更新旧value即可。如果Key不完全一致,就查询Node列表,在列表里面查询Key完全一致的元素,如果找到就更新Value,找不到就插入新的Node。
也就是说存在重复的条件是Key的引用一致、equals方法一致、hashCode一致。
HashSet使用add的对象作为Key,如果是同一个对象,就说明存在重复。
15、Java异常体系
16、修改equals方法的签名
正确的equals方法如下:
错误的写法如下:
错误的写法虽然在调用foodA.equals(foodB)的时候能够正常运行,但当将foodA存到集合的时候就有问题。例如:List.add(foodA) List.contains(foodA)却返回了false。
17、ThreadLocal如何保证local
ThreadLocal的set和get方法如上图所示,它保证了每隔线程有自己的变量副本,假设有如下ThreadLocal:
ThreadLocal tl =new ThreadLocal();
当线程A和线程B同时访问tl时,每个线程获取到的都是属于自己Thread的本地变量,互相不影响。原理就在于ThreadLocal的数据结构。
每个Thread都有一个ThreadLocalMap类型的threadLocals变量,当调用ThreadLocal.set的时候,map里面存储了当前ThreadLoacal的引用和value值对象,假设线程A访问了tl,线程A的threadLocals的map中就有Key=tl,value=v的键值对,当调用get的时候,会先获取线程A的threadLocals对象,入得threadLoacal对应的value,每个线程都是取的自己ThreadLocalMap中的ThreadLocal对应的Value,互相不影响。
18、线程的状态
新建:新创建一个线程对象
就绪:线程对象创建后,调用了Thread.start方法,等待被调度
运行:可运行状态的线程获得cpu的时间片,执行程序代码
阻塞:由于某种原因而放弃cpu的使用权,暂时停止运行。直到进入可运行状态才有机会再次获得cpu的时间片。
结束:run方法执行结束
19、类的初始化时机
遇到new、getstatic、putstatic、invokestatic,如果类还没有初始化,则先触发类的初始化。常见场景:new一个对象、调用或者设置类的静态变量、调用类的静态方法。
使用java.lang.reflect反射类时,如果类还没创建,则触发初始化。
当初始化一个类时,如果父类还没初始化,则先触发其父类初始化。
当虚拟机启动时,用户需要指定一个虚拟机执行的类(main函数),虚拟机会先触发这个类的初始化。
总结:一个类对另一个类进行主动引用,将会触发类的初始化,否则不会。
20、类加载机制
加载:将类文件的二进制流加载到内存;将二进制流的静态存储结构转化为运行时数据结构;在内存中生成代表这个类的java.lang.class对象、作为方法区这个类各种对象的访问入口。
验证:验证Class文件的字节流是否符合虚拟机的要求,包括文件格式验证、元数据验证、字节码验证、符号引用验证。
准备:为类变量(static变量)分配内存并设置初始值。
解析:将常量池的符号引用转化为直接引用。
初始化:开始执行类中定义的java程序,执行类构造器<cinit>,cinit是由编译器自动收集所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在代码中出现的顺序决定,静态语句块中只能访问定义在它之前的静态变量,定义在之后变量,前面只能赋值,不能访问。<cinit>方法与类的构造器(<init>)不同,它不需要显式地调用父类构造器,虚拟机会保证子类的<cinit>执行之前,父类的<cinit>已经执行完毕。因此虚拟机中第一个执行的<cinit>方法是java.lang.Object的<cinit>方法。由于父类的<cinit>先执行,所以父类的静态语句块优先于子类执行。
<cinit>对于类来说不是必须的,如果类没有静态语句也没有赋值操作,可以不生成<cinit>。
接口中不能有静态语句块,但同样有赋值操作,所以接口也会生成cinit方法。