什么时候会触发full gc

  1. System.gc()方法的调用

  2. 老年代空间不足

  3. 永生区空间不足(JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据)

  4. GC时出现promotion failed和concurrent mode failure

  5. 统计得到的Minor GC晋升到旧生代平均大小大于老年代剩余空间

  6. 堆中分配很大的对象

可以作为root的对象:

  1. 类中的静态变量,当它持有一个指向一个对象的引用时,它就作为root

  2. 活动着的线程,可以作为root

  3. 一个Java方法的参数或者该方法中的局部变量,这两种对象可以作为root

  4. JNI方法中的局部变量或者参数,这两种对象可以作为root

例子:下述的Something和Apple都可以作为root对象。

public AClass{

  public static Something;
  public static final Apple;
   ''''''
}

 

Java方法的参数和方法中的局部变量,可以作为root.

public Aclass{

public void doSomething(Object A){
    ObjectB b = new ObjectB; 
    }
 }

 

新生代转移到老年代的触发条件

  1. 长期存活的对象

  2. 大对象直接进入老年代

  3. minor gc后,survivor仍然放不下

  4. 动态年龄判断 ,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代

G1和CMS的区别

  1. G1同时回收老年代和年轻代,而CMS只能回收老年代,需要配合一个年轻代收集器。另外G1的分代更多是逻辑上的概念,G1将内存分成多个等大小的region,Eden/ Survivor/Old分别是一部分region的逻辑集合,物理上内存地址并不连续。

    java面试题及答案_java
  2. CMS在old gc的时候会回收整个Old区,对G1来说没有old gc的概念,而是区分Fully young gc和Mixed gc,前者对应年轻代的垃圾回收,后者混合了年轻代和部分老年代的收集,因此每次收集肯定会回收年轻代,老年代根据内存情况可以不回收或者回收部分或者全部(这种情况应该是可能出现)。

 

双亲委派模型中有哪些方法。用户如何自定义类加载器 。怎么打破双亲委托机制

  1. 双亲委派模型中用到的方法:

  • findLoadedClass(),

  • loadClass()

  • findBootstrapClassOrNull()

  • findClass()

  • defineClass():把二进制数据转换成字节码。

  • resolveClass()

自定义类加载器的方法:继承 ClassLoader 类,重写 findClass()方法 。

  1. 继承ClassLoader覆盖loadClass方法 原顺序

  2. findLoadedClass

  3. 委托parent加载器加载(这里注意bootstrap加载器的parent为null)

  4. 自行加载 打破委派机制要做的就是打乱2和3的顺序,通过类名筛选自己要加载的类,其他的委托给parent加载器。

即时编译器的优化方法

字节码可以通过以下两种方式转换成合适的语言:

  1. 解释器

  2. 即时编译器 即时编译器把整段字节码编译成本地代码,执行本地代码比一条一条进行解释执行的速度快很多,因为本地代码是保存在缓存里的

编译过程的五个阶段

  1. 第一阶段:词法分析

  2. 第二阶段:语法分析

  3. 第三阶段:词义分析与中间代码产生

  4. 第四阶段:优化

  5. 第五阶段:目标代码生成

java应用系统运行速度慢的解决方法

问题解决思路:

  1. 查看部署应用系统的系统资源使用情况,CPU,内存,IO这几个方面去看。找到对就的进程。

  2. 使用jstack,jmap等命令查看是JVM是在在什么类型的内存空间中做GC(内存回收),和查看GC日志查看是那段代码在占用内存。首先,调节内存的参数设置,如果还是一样的问题,就要定位到相应的代码。

  3. 定位代码,修改代码(一般是代码的逻辑问题,或者代码获取的数据量过大。)

 

内存溢出是什么,什么原因导致的

内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。为了解决Java中内存溢出问题,我们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用GC函数来释放内存,因为不同的JVM实现者可能使用不同的算法管理GC,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是中断式执行GC。但GC只能回收无用并且不再被其它对象引用的那些对象所占用的空间。Java的内存垃圾回收机制是从程序的主要运行对象开始检查引用链,当遍历一遍后发现没有被引用的孤立对象就作为垃圾回收。

 

引起内存溢出的原因有很多种,常见的有以下几种:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

  3. 代码中存在死循环或循环产生过多重复的对象实体;

  4. 使用的第三方软件中的BUG;

  5. 启动参数内存值设定的过小;

 

内存溢出的解决

内存溢出虽然很棘手,但也有相应的解决办法,可以按照从易到难,一步步的解决。

 

第一步,就是修改JVM启动参数,直接增加内存。这一点看上去似乎很简单,但很容易被忽略。JVM默认可以使用的内存为64M,Tomcat默认可以使用的内存为128MB,对于稍复杂一点的系统就会不够用。在某项目中,就因为启动参数使用的默认值,经常报“OutOfMemory”错误。因此,-Xms,-Xmx参数一定不要忘记加。

 

第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。在一个项目中,使用两个数据库连接,其中专用于发送短信的数据库连接使用DBCP连接池管理,用户为不将短信发出,有意将数据库连接用户名改错,使得日志中有许多数据库连接异常的日志,一段时间后,就出现“OutOfMemory”错误。经分析,这是由于DBCP连接池BUG引起的,数据库连接不上后,没有将连接释放,最终使得DBCP报“OutOfMemory”错误。经过修改正确数据库连接参数后,就没有再出现内存溢出的错误。

 

查看日志对于分析内存溢出是非常重要的,通过仔细查看日志,分析内存溢出前做过哪些操作,可以大致定位有问题的模块。

 

第三步,找出可能发生内存溢出的位置。重点排查以下几点:

  1. 检查代码中是否有死循环或递归调用。

  2. 检查是否有大循环重复产生新对象实体。

  3. 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。

  4. 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。

 

第四步,使用内存查看工具动态查看内存使用情况。某个项目上线后,每次系统启动两天后,就会出现内存溢出的错误。这种情况一般是代码中出现了缓慢的内存泄漏,用上面三个步骤解决不了,这就需要使用内存查看工具了。

 

内存查看工具有许多,比较有名的有:Optimizeit Profiler、JProbe Profiler、JinSight和Java1.5的Jconsole等。它们的基本工作原理大同小异,都是监测Java程序运行时所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员可以根据这些信息判断程序是否有内存泄漏问题。一般来说,一个正常的系统在其启动完成后其内存的占用量是基本稳定的,而不应该是无限制的增长的。持续地观察系统运行时使用的内存的大小,可以看到在内存使用监控窗口中是基本规则的锯齿形的图线,如果内存的大小持续地增长,则说明系统存在内存泄漏问题。通过间隔一段时间取一次内存快照,然后对内存快照中对象的使用与引用等信息进行比对与分析,可以找出是哪个类的对象在泄漏。

 

通过以上四个步骤的分析与处理,基本能处理内存溢出的问题。当然,在这些过程中也需要相当的经验与敏感度,需要在实际的开发与调试过程中不断积累。

 

总体上来说,产生内存溢出是由于代码写的不好造成的,因此提高代码的质量是最根本的解决办法。有的人认为先把功能实现,有BUG时再在测试阶段进行修正,这种想法是错误的。正如一件产品的质量是在生产制造的过程中决定的,而不是质量检测时决定的,软件的质量在设计与编码阶段就已经决定了,测试只是对软件质量的一个验证,因为测试不可能找出软件中所有的BUG。

 

JAVA 线程状态转换图示


java面试题及答案_java_02

synchronized 的底层怎么实现

  1. 同步代码块(Synchronization)基于进入和退出管程(Monitor)对象实现。每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

  • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

  • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

  1. 被 synchronized 修饰的同步方法并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成

讲一下CAS

CAS,compare and swap的缩写,中文翻译成比较并交换。乐观锁用到的机制就是CAS,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试。

 

原理:

  1. CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

JDK文档说cas同时具有volatile读和volatile写的内存语义。

 

缺点:

  1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化

  2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

  3. 只能保证一个共享变量的原子操作。对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

线程池

Executor线程池框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

 

ThreadPoolExecutor执行的策略

  1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务

  2. 线程数量达到了corePools,则将任务移入队列等待

  3. 队列已满,新建线程(非核心线程)执行任务

  4. 队列已满,总线程数又达到了maximumPoolSize,就会由(RejectedExecutionHandler)抛出异常

新建线程 -> 达到核心数 -> 加入队列 -> 新建线程(非核心) -> 达到最大数 -> 触发拒绝策略

 

常见四种线程池

  1. CachedThreadPool():可缓存线程池。

  • 线程数无限制

  • 有空闲线程则复用空闲线程,若无空闲线程则新建线程

  • 一定程序减少频繁创建/销毁线程,减少系统开销

  1. FixedThreadPool():定长线程池。

  • 可控制线程最大并发数(同时执行的线程数)

  • 超出的线程会在队列中等待

  1. ScheduledThreadPool():定时线程池。

  • 支持定时及周期性任务执行。

  1. SingleThreadExecutor():单线程化的线程池。

  • 有且仅有一个工作线程执行任务

  • 所有任务按照指定顺序执行,即遵循队列的入队出队规则

 

 

 

 

四种拒绝策略

  1. AbortPolicy:拒绝任务,且还抛出RejectedExecutionException异常,线程池默认策略

  2. CallerRunPolicy:拒绝新任务进入,如果该线程池还没有被关闭,那么这个新的任务在执行线程中被调用

  3. DiscardOldestPolicy: 如果执行程序尚未关闭,则位于头部的任务将会被移除,然后重试执行任务(再次失败,则重复该过程),这样将会导致新的任务将会被执行,而先前的任务将会被移除。

  4. DiscardPolicy:没有添加进去的任务将会被抛弃,也不抛出异常。基本上为静默模式。

 

为什么要用线程池

  1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

  2. 运用线程池能有效的控制线程最大并发数,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

  3. 对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现

对象锁和静态锁之间的区别
  1. 对象锁用于对象实例方法,

  2. 类锁用于类的静态方法或一个类的class对象。

  3. 类的对象实例可以有很多,不同对象实例的对象锁互不干扰,而每个类只有一个类锁

 

简述volatile字

两个特性

  1. 保证了不同线程对这个变量进行 读取 时的可见性,即一个线程修改 了某个变量的值 , 这新值对其他线程来说是立即可见的 。(volatile 解决了 线程间 共享变量

  2. 禁止进行指令重排序 ,阻止编译器对代码的优化

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性,锁保证了原子性,而volatile保证可见性和有序性

happens-before 原则(先行发生原则):

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在 后面的操作

  2. 锁定规则:一个 unLock 操作先行发生于后面对同一个锁的 lock 操作

  3. volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

  4. 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以 得出操作 A 先行发生于操作 C

  5. 线程启动规则:Thread 对象的 start()方法先行发生于此线程的每个一个动作

  6. 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测 到中断事件的发生

  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 T hread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

  8. 对象终结规则:一个对象的初始化完成先行发生于他的 finalize()方法的开始

Lock 和synchronized 的区别

  1. Lock 是一个 接口,而 synchronized 是 Java 中的 关键字, synchronized 是 内置的语言实现;

  2. synchronized 在 发生异常时,会 自动释放线程占有的锁,因此 不会导 致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放 锁,则很 可能造成死锁现象,因此用 使用 Lock 时需要在 finally 块中释放锁;

  3. Lock 可以让 等待锁的线程响应中断 (可中断锁),而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去, 不能够响应中 断 (不可中断锁);

  4. 通过 Lock 可以知道 有没有成功获取锁 (tryLock ( ) 方法 :如果获取 了锁 ,回 则返回 true ;回 否则返回 false e, , 也就说这个方法无论如何都会立即返回 。在拿不到锁时不会一直在那等待。),而 synchronized 却无法办到。

  5. Lock 可以提高 多个线程进行读操作的效率( 读写锁)。

  6. Lock 可以实现 公平锁,synchronized 不保证公平性。在性能上来说,如果线程竞争资源不激烈时,两者的性能是差不多的,而 当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优 于 synchronized。所以说,在具体使用时要根据适当情况选择。

 

ThreadLocal(线程变量副本)

Synchronized实现内存共享,ThreadLocal为每个线程维护一个本地变量。采用空间换时间,它用于线程间的数据隔离,为每一个使用该变量的线程提供一个副本,每个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。ThreadLocal类中维护一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本。ThreadLocal在Spring中发挥着巨大的作用,在管理Request作用域中的Bean、事务管理、任务调度、AOP等模块都出现了它的身影。Spring中绝大部分Bean都可以声明成Singleton作用域,采用ThreadLocal进行封装,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。

通过Callable和Future创建线程

Java 5在concurrency包中引入了java.util.concurrent.Callable 接口,它和Runnable接口很相似,但它可以返回一个对象或者抛出一个异常。

 

Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。由于Callable任务是并行的,我们必须等待它返回的结果。java.util.concurrent.Future对象为我们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它我们可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。

 

  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

  2. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。

  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

 

什么叫守护线程,用什么方法实现守护线程(Thread.setDeamon()的含义)

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。JVM内部的实现是如果运行的程序只剩下守护线程的话,程序将终止运行,直接结束。所以守护线程是作为辅助线程存在的,主要的作用是提供计数等等辅助的功能。

如何停止一个线程?

终止线程的三种方法:

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。在定义退出标志exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值,

  thread.exit = true;  // 终止线程thread 
 
  1. 使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。使用stop方法可以强行终止正在运行或挂起的线程。我们可以使用如下的代码来终止线程:thread.stop(); 虽然使用上面的代码可以终止线程,但使用stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,因此,并不推荐使用stop方法来终止线程。

  2. 使用interrupt方法中断线程,使用interrupt方法来终端线程可分为两种情况:

  • 线程处于阻塞状态,如使用了sleep方法。

  • 使用while(!isInterrupted()){……}来判断线程是否被中断。在第一种情况下使用interrupt方法,sleep方法将抛出一个InterruptedException例外,而在第二种情况下线程将直接退出。

注意:在Thread类中有两个方法可以判断线程是否通过interrupt方法被终止。一个是静态的方法interrupted(),一个是非静态的方法isInterrupted(),这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted可以用来判断其他线程是否被中断。因此,while (!isInterrupted())也可以换成while (!Thread.interrupted())。

什么是线程安全?什么是线程不安全?

  1. 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。

  2. 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据 在多线程的情况下,由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。

 

 

HashSet和TreeSet区别

HashSet

  1. 不能保证元素的排列顺序,顺序有可能发生变化

  2. 不是同步的

  3. 集合元素可以是null,但只能放入一个null 当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。

TreeSet

  1. TreeSet是SortedSet接口的唯一实现类

  2. TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象

讲一下LinkedHashMap

LinkedHashMap的实现就是HashMap+LinkedList的实现方式,以HashMap维护数据结构,以LinkList的方式维护数据插入顺序

 

LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。在遍历的时候会比HashMap慢TreeMap能够把它保存的记录根据键排序,默认是按升序排序,也可以指定排序的比较器

 

利用LinkedHashMap实现LRU算法缓存(

  1. LinkedList首先它是一个Map,Map是基于K-V的,和缓存一致

  2. LinkedList提供了一个boolean值可以让用户指定是否实现LRU)

 

Java8 中HashMap的优化(引入红黑树的数据结构和扩容的优化)

  1. if (binCount >= TREEIFY_THRESHOLD - 1) 当符合这个条件的时候,把链表变成treemap红黑树,这样查找效率从o(n)变成了o(log n) ,在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:

  2. 我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置

 

这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。

元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:hashMap 1.8 哈希算法例图2

 


java面试题及答案_java_03

因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”

Map遍历的keySet()和entrySet()性能差异原因

Set<Entry<StringString>> entrySet = map.entrySet();
Set<Stringset = map.keySet();`
  1. keySet()循环中通过key获取对应的value的时候又会调用getEntry()进行循环。循环两次

  2. entrySet()直接使用getEntry()方法获取结果,循环一次

  3. 所以 keySet()的性能会比entrySet()差点。所以遍历map的话还是用entrySet()来遍历

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }  

 

 

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
}

 

抽象类和接口的对比

 

 

参数 抽象类 接口
默认的方法实现 它可以有默认的方法实现 接口完全是抽象的。它根本不存在方法的实现
实现 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现
构造器 抽象类可以有构造器 接口不能有构造器
与正常Java类的区别 除了你不能实例化抽象类之外,它和普通Java类没有任何区别 接口是完全不同的类型
访问修饰符 抽象方法可以有public、protected和default这些修饰符 接口方法默认修饰符是public。你不可以使用其它修饰符。
main方法 抽象方法可以有main方法并且我们可以运行它 接口没有main方法,因此我们不能运行它。
多继承 抽象方法可以继承一个类和实现多个接口 接口只可以继承一个或多个其它接口
速度 它比接口速度要快 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。
添加新方法 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 如果你往接口中添加方法,那么你必须改变实现该接口的类。

 

创建一个类的几种方法?

  1. 使用new关键字 → 调用了构造函数

  2. 使用Class类的newInstance方法  → 调用了构造函数

 

Employee emp2 = (Employee)Class.forName("org.programming.mitra.exercises.Employee").newInstance();

 

  1. 使用Constructor类的newInstance方法  → 调用了构造函数

Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();
 
  1. 使用clone方法   → 没有调用构造函数

  2. 使用反序列化 }→ 没有调用构造函数

ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));
Employee emp5 = (Employee) in.readObject();

Redirect和forward

  1. 上图所示的间接转发请求的过程如下:浏览器向Servlet1发出访问请求;Servlet1调用sendRedirect()方法,将浏览器重定向到Servlet2;浏览器向servlet2发出请求;最终由Servlet2做出响应。

  2. 上图所示的直接转发请求的过程如下:浏览器向Servlet1发出访问请求;Servlet1调用forward()方法,在服务器端将请求转发给Servlet2;最终由Servlet2做出响应。

 

什么是泛型,为什么要使用以及类型擦除

  1. 泛型的本质就是“参数化类型”,也就是说所操作的数据类型被指定为一个参数。创建集合时就指定集合元素的数据类型,该集合只能保存其指定类型的元素, 避免使用强制类型转换。

  2. Java 编译器生成的字节码是不包含泛型信息的,泛型类型信息将在 编译处理 时 被擦除,这个过程即 类型擦除。类型擦除可以简单的理解为将泛型 java 代码转 换为普通 java 代码,只不过编译器更直接点,将泛型 java 代码直接转换成普通 java 字节码。

类型擦除的主要过程如下:

  1. 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。

  2. 移除所有的类型参数。

Object跟这些标记符代表的java类型有啥区别呢?

Object是所有类的根类,任何类的对象都可以设置给该Object引用变量,使用的时候可能需要类型强制转换,但是用使用了泛型T、E等这些标识符后,在实际用之前类型就已经确定了,不需要再进行类型强制转换。

Error类和Exception类区别

  1. Error类和Exception类的父类都是throwable类,他们的区别是:Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。Exception类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行, 而不应该随意终止异常。

  2. Exception类又分为运行时异常(Runtime Exception)和受检查的异常(Checked Exception ),运行时异常;ArithmaticException,IllegalArgumentException,编译能通过,但是一运行就终止了,程序不会处理运行时异常,出现这类异常,程序会终止。而受检查的异常,要么用try。。。catch捕获,要么用throws字句声明抛出,交给它的父类处理,否则编译不会通过。

 

throw和throws区别

throw:(针对对象的做法)抛出一个异常,可以是系统定义的,也可以是自己定义的

public void yichang(){
    NumberFormatException e = new NumberFormatException();
    throw e;
}

 

throws:(针对一个方法抛出的异常)抛出一个异常,可以是系统定义的,也可以是自己定义的。

public void yichang() throws NumberFormatException{
    int a = Integer.parseInt("10L");
}

 

  1. throws出现在方法函数头;而throw出现在函数体。

  2. throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常。

  3. 两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

 

.class 文件是什么类型文件

class文件是一种8位字节的二进制流文件

java中序列化之子类继承父类序列化

父类实现了Serializable,子类不需要实现Serializable

 

相关注意事项 1. 序列化时,只对对象的状态进行保存,而不管对象的方法;2. 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;c)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;3. 并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如:1.安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。2. 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现。

 

2,反过来父类未实现Serializable,子类实现了,序列化子类实例的时候,父类的属性是直接被跳过不保存,还是能保存但不能还原?(答案:值不保存)

 

解:父类实现接口后,所有派生类的属性都会被序列化。子类实现接口的话,父类的属性值丢失。

 

java中序列化之子类继承父类序列化

标识符

标识符可以包括这4种字符:字母、下划线、$、数字;开头不能是数字;不能是关键字

Integer i=new Integer(127);和Integer i=127;的区别

Integer i = 127的时候,使用Java常量池技术,是为了方便快捷地创建某些对象,当你需要一个对象时候,就去这个池子里面找,找不到就在池子里面创建一个。但是必须注意 如果对象是用new 创建的。那么不管是什么对像,它是不会放到池子里的,而是向堆申请新的空间存储。Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值在-128到127之间的数时才可使用对象池。超过了就要申请空间创建对象了

    int i1=128;
    Integer i2=128;
    Integer i3=new Integer(128);//自动拆箱

    System.out.println(i1==i2);//true
    System.out.println(i1==i3);//true

    Integer i5=127;
    Integer i6=127;
    System.out.println(i5==i6);//true


    Integer i5=127;
    Integer ii5=new Integer(127);
    System.out.println(i5==ii5);//false

    Integer i7=new Integer(127);
    Integer i8=new Integer(127);
    System.out.println(i7==i8);//false

 

手写单例模式

最好的单例模式是静态内部类,不要写双重检验

private static class LazySomethingHolder {
  public static Something something = new Something();
}

public static Something getInstance() {
  return LazySomethingHolder.something;
}

 

为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?

Java的每个对象中都有一个锁(monitor,也可以成为监视器) 并且wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在Java的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是Object类的一部分,这样Java的每一个类都有用于线程间通信的基本方法

Java中wait 和sleep 方法比较

  1. 这两个方法来自不同的类分别是Thread和Object

  2. 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

  3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)

  4. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

  5. sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。

  • 注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep 而不是t线程

  1. wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。wait()方法也同样会在wait的过程中有可能被其他对象调用interrupt()方法而产生

 

hashCode和equals方法的关系

在有些情况下,程序设计者在设计一个类的时候为需要重写equals方法,比如String类,但是千万要注意,在重写equals方法的同时,必须重写hashCode方法。也就是说对于两个对象,如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等;如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同;如果两个对象的hashcode值不等,则equals方法得到的结果必定为false;如果两个对象的hashcode值相等,则equals方法得到的结果未知。

Object类中有哪些方法,列举3个以上(可以引导)

Object方法:equals()、toString()、finalize()、hashCode()、getClass()、clone()、wait()、notify()、notifyAll()

String s=new String("xyz")究竟创建String Object分为两种情况:

  1. 如果String常理池中,已经创建"xyz",则不会继续创建,此时只创建了一个对象new String("xyz");

  2. 如果String常理池中,没有创建"xyz",则会创建两个对象,一个对象的值是"xyz",一个对象new String("xyz")。

什么是值传递和引用传递

值传递

public class TempTest {

  private void test1(int a) {
    a = 5;
    System.out.println("test1方法中的a=" + a);
  }

  public static void main(String[] args) {
    TempTest t = new TempTest();
    int a = 3;
    t.test1(11);
    System.out.println("main方法中a=" + a);
  }

}

 

test1方法中的a=5 main方法中a=3 值传递:传递的是值的拷贝,传递后就互不相关了 引用传递:传递的是变量所对应的内存空间的地址

public class TempTest {
  private void test1(A a) {
    a.age = 20;
    System.out.println("test1方法中a=" + a.age);
  }

  public static void main(String[] args) {
    TempTest t = new TempTest();
    A a = new A();
    a.age = 10;
    t.test1(a);
    System.out.println("main方法中a=" + a.age);
  }
}

class A {
  public int age = 0;
}

 

test1方法中a=20 main方法中a=20 传递前和传递后都指向同一个引用(同一个内存空间) 如果不互相影响,方法是在test1方法里面新new一个实例就可以了

 

讲一下netty

netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件

Nio的原理(同步非阻塞)

服务端和客户端各自维护一个管理通道的对象,我们称之为 selector,该对 象能检测一个或多个通道(channel)上的事件。我们以服务端为例,如果服务 端的 selector 上注册了读事件,某时刻客户端给服务端送了一些数据,阻塞 I/O 这时会调用 read()方法阻塞地读取数据,而 NIO 的服务端会在 selector 中添加 一个读事件。服务端的处理线程会轮询地访问 selector,如果访问 selector 时发 现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处 理线程会一直阻塞直到感兴趣的事件到达为止。

 


java面试题及答案_java_04

 

缓冲区Buffer、通道Channel、选择器Selector

缓冲区Buffer

  • 缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。

 

通道Channel

  • 通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。通道与流的不同之处在于 通道是双向 的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类,比如 InputStream 只能进行读取操作,OutputStream 只能进行写操作),而通道是双向的,可以用于读、写或者同时用于读写。

 

选择器(Selector )

  • NIO 有一个主要的类 Selector,这个类似一个观察者,只要我们把需要探知 的 socketchannel 告诉 Selector,我们接着做别的事情, 当有事件发生时,他会 通知我们,传回一组 SelectionKey, 我们读取这些 Key, 就会获得我们刚刚注册 过的 socketchannel, 然后,我们从这个 Channel 中读取数据,放心,包准能 够读到,接着我们可以处理这些数据。

  • Selector 内部原理实际是在做一个 对所注册的 channel 的轮询访问,不断 地轮询,一旦轮询到一个 channel 有所注册的事情发生,比如数据来了,他就 会站起来报告, 交出一把钥匙,让我们 通过这把钥匙来读取这个 channel 的内 容。

 

BIO和NIO的区别

  1. BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

  2. NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

NIO的selector作用

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。

 

为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(这样Selector才知道需要记录什么数据),一共有4种事件:

 

  1. connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT(8)

  2. accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT(16)

  3. read:读事件,对应值为SelectionKey.OP_READ(1)

  4. write:写事件,对应值为SelectionKey.OP_WRITE(4)

 

每次请求到达服务器,都是从connect开始,connect成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,最后写回数据返回。

所以,当SocketChannel有对应的事件发生时,Selector都可以观察到,并进行相应的处理。