Java(1)

什么是多态?

Java引用变量有两种类型:编译时类型,运行时类型。Person p = new Student(). Person是编译时类型,Student是运行时类型。
如果编译时类型和运行时类型不一致,就会出现多态。

Java实现多态有三个必要条件:继承、重写、向上转型

继承:在多态中必须存在有继承关系的子类和父类。

重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。

向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

Java中有两种形式可以实现多态,继承和接口

基于继承的实现机制主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。

基于接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。

● 请问Java中接口与抽象类是否相同?

参考回答:
不同,
抽象类 接口
abstract interface
extends implements
public/protected/private public
有main方法 无main方法
比接口速度快

  • 对于接口当中常见的成员:而且这些成员都有固定的修饰符
    1、全局常量:public static final
    2、抽象方法: public abstract
  • 实现一个接口,都得覆盖;继承是父类有的,可以拿来用,也可覆盖某些方法。
  • 一个类在继承另一个类的同时,还可以实现多个接口
  • 抽象类需要被继承,而且只能单继承。接口需要被实现,而且可以多实现。
  • 抽象类中可以定义抽象方法和非抽象方法,子类继承后,可以直接使用非抽象方法。
  • 接口中只能定义抽象方法,必须由子类去实现。

什么时候使用抽象类和接口:

如果拥有一些方法并且想让它们中的一些有默认实现,使用抽象类。

如果想实现多重继承,必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。

如果基本功能在不断改变,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么就需要改变所有实现了该接口的类。

● 请你说一下垃圾回收机制

参考回答:
垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。
主要负责回收堆内存中的对象。
当对象永久性的失去引用时系统会在合适的时候回收它所占的内存。
回收之前,先调用它的finalize()方法。

对象在内存中的状态:

  • 可达状态:有一个以上的引用变量引用它。
  • 可恢复状态:没有引用变量引用它,垃圾回收机制准备回收它,回收之前,先调用fianlize()方法,重新让一个引用变量应用该对象,则该对象进入可达状态。否则,进入不可达状态。
  • 不可达状态:没有引用变量引用它,即使调用了finalize()方法也不行,系统回收它。

强制垃圾回收
System.gc()
Runtime.getRuntime().gc()

finalize()方法的四个特点:
(1)不要主动调用某个对象的finalize()方法,应交给垃圾回收机制调用
(2)它何时被调用,具有不确定性
(3)JVM执行可恢复对象的finalize()方法时,可能使该对象或系统中其他对象重现变成可达状态。
(4)JVM执行finalize()方法时出现异常时,垃圾回收机制不会报告异常,程序继续执行。

引用:如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

(1)强引用(Strong Reference):如“Object obj = new Object()”,这类引用是Java程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。

(2)软引用(Soft Reference):它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2之后提供了SoftReference类来实现软引用。

(3)弱引用(Weak Reference):它也是用来描述非须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。

(4)虚引用(Phantom Reference):最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用。

垃圾:无任何对象引用的对象。

判断对象是否是垃圾的算法:

引用计数算法(Reference Counting Collector)、根搜索算法(Tracing Collector):

回收:清理“垃圾”占用的内存空间而非对象本身。

Tracing算法(Tracing Collector) 标记—清除算法:分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。

Compacting算法(Compacting Collector)标记—整理算法:标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

Copying算法(Copying Collector):将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。

Adaptive算法(Adaptive Collector):监控当前堆的使用情况,并将选择适当算法的垃圾收集器。

发生地点:一般发生在堆内存中,因为大部分的对象都储存在堆内存中。

(堆内存为了配合垃圾回收有什么不同区域划分,各区域有什么不同?)

Java的堆内存基于Generation算法(Generational Collector)划分为新生代、年老代和持久代。新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。分代收集基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收(GC),以便提高回收效率。

按执行机制划分Java有四种类型的垃圾回收器:

(1)串行垃圾回收器(Serial Garbage Collector)

(2)并行垃圾回收器(Parallel Garbage Collector)

(3)并发标记扫描垃圾回收器(CMS Garbage Collector)

(4)G1垃圾回收器(G1 Garbage Collector)

发生时间:程序空闲时间不定时回收。

● 请你说一下Java中的异常处理机制
五个关键字:try, catch, finally, throw, throws

try, catch

try
{
// 业务实现代码
}
catch(Exception e)
{
输入不合法
}

  • 如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给java运行时环境,这个过程被称为抛出(throw)异常。
  • 当Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,这个过程称为捕获(catch)异常,若不找不到catch,则运行时环境终止,Java程序退出。
  • try块里声明的变量是代码快内局部变量,只在try快内有效,在cathc块中不能访问。
  • try后面可以跟好几个catch,但是一次只能执行一个。
  • IndexOutOfBoundsException:数组越界异常 NumberFormatException:数字格式异常
    ArithmeticException:算术异常
    NullPointException:空指针异常
  • 异常捕获时,先捕获小异常,再捕获大异常。
  • catch捕获多种异常:
  • catch(IndexOutOfBoundsException | NumberFormatException)
    {
    final ie = new NumberFormatException(“test”);
    }
  • 捕获多种异常时,异常变量使用隐式的final修饰。
  • 捕获一种异常时,异常变量没有final修饰。
    try
    {
    FileInputStream fis = new FileInputStream(“a.txt”);
    }
    catch(IOException ioe)
    {
    System.out.println(ioe.getMessage());
    ioe.printStackTrace();
    }
    getMessage():得到异常对象的详细信息。
    printStackTrace():打印异常的追踪信息。

finally 回收资源

  • finally回收try块中打开的物理资源(数据库连接、网络连接、磁盘文件)。不管怎样,finally块总会被执行。
  • 有try块,catch和finally至少有一个,catch块可以有多个,finally块必须在catch后面。
  • 除非在try块、catch块中调用了退出虚拟机的方法(System.exit(1)),否则不管在try,catch中执行了怎样的代码、出现了怎样的情况,异常处理的finally块都会被执行。
  • 在finally块中,不要使用return 和throw,这将会导致try、throw块中的return、throw失效。
  • Java 7增加了try语句的功能,它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,try语句在该语句结束时自动关闭这些资源。这些资源实现类必须实现AutoCloseable或Closeable接口,实现它们的close()方法。

使用throws声明抛出异常
思路:

  • 当前方法不知道如何处理这种类型的异常,该异常应该由上一级调度者处理;
  • 如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常交给JVM处理。
  • JVM对异常的处理:打印异常的跟踪栈信息,并中止程序运行。

使用throw声明抛出异常
程序自行抛出异常,用throw完成。throw抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。
使用throw语句抛出的异常时Checked异常(提示程序员修改),throw语句可以放在try块里,也可以放在一个带throws声明抛出的方法里。
如果抛出的runtime异常,无须显式声明抛出。

catch和throw一起使用
在实际应用中,当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可以完全处理。
try
{
d = Double.parseDouble(bidPrice);
}
catch(Exception e)
{
e.printStackTrace();
throw new AuctionException(“竞拍必须是数值”);
}
当catch捕获到异常后,系统打印该异常的跟踪栈信息,接着抛出一个AuctionException异常,通知该方法的调用者再次处理该AuctionException异常。
Java(1)
异常链
程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为:异常转译。

异常跟踪栈
函数调用过程:main方法调用firstMethod方法,firstMethod调用secondMethod方法,secondMethod调用thirdMethod方法。
异常处理过程:逆序: 异常从thirdMethod方法开始触发,传到secondMethod方法,再传到firstMethod方法,最后传到main方法,在main方法中止,这个过程就是Java的异常跟踪栈。

● 请问多线程是什么?

参考回答:
多线程是为了同步完成多项任务,为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。

一个采用了多线程技术的应用程序可以更好地利用系统资源。其主要优势在于充分利用了CPU的空闲时间片,可以用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。更为重要的是,由于同一进程的所有线程是共享同一内存,使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。

● 请你来聊一聊集合类和内存

参考回答:
一、集合类。
Java中的集合包含多种数据结构,如链表、队列、哈希表等。从类的继承结构来说,可以分为两大类,一类是继承自Collection接口,这类集合包含List、Set和Queue等集合类。另一类是继承自Map接口,这主要包含了哈希表相关的集合类。

1、List、Set和Queue类的继承结构图:绿色的虚线代表实现,绿色实线代表接口之间的继承,蓝色实线代表类之间的继承。

Collection接口除了实现映射的集合类之外的所有集合类定义了一些方法

List集合类型:描述了一种按位置存储数据的对象,是有序的。用的比较多List包括ArrayList和LinkedList,这两者的区别:ArrayList的底层的通过数组实现,所以其随机访问的速度比较快,但是对于需要频繁的增删的情况,效率就比较低了。而对于LinkedList,底层通过链表来实现,所以增删操作比较容易完成,但是对于随机访问的效率比较低。

Queue:一般可以直接使用LinkedList完成,LinkedList继承自Deque,所以LinkedList具有双端队列的功能。PriorityQueue是为每个元素提供一个优先级,优先级高的元素会优先出队列。

Set:Set与List的主要区别是Set是不允许元素是重复的,而List则可以允许元素是重复的。HashSetLinkedHashSet的区别在于后者可以保证元素插入集合的元素顺序与输出顺序保持一致。而TresSet的区别在于其排序是按照Comparator来进行排序的,默认情况下按照字符的自然顺序进行升序排列。

Iterable:Collection类继承自Iterable,该接口的作用是提供元素遍历的功能,也就是说所有的集合类(除Map相关的类)都提供元素遍历的功能。Iterable里面包含了Iterator的迭代器。

2、Map类型的集合:最大的优点在于其查找效率比较高,理想情况下可以实现O(1)的时间复杂度。Map中最常用的是HashMapLinkedHashMap:能够保证插入集合的元素顺序与输出顺序一致。这两者与TreeMap的区别在于TreeMap是根据键值进行排序的,其底层的实现也有本质的区别,HashMap底层是一个哈希表,而TreeMap的底层数据结构是一棵树。

二、Java内存区域划分
请你说一下java jvm的内存机制

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分成若干个不同的数据区域。
Java(1)
1.程序计数器:
运行java方法:虚拟机字节码指令的地址
可以看做是当前线程所执行的字节码行号指示器。在JVM的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

每条线程都有一个独立的程序计数器,所以程序计数器是线程私有的内存区域。

如果线程执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果线程执行的是一个Native方法,计数器的值为空。(Native方法不是用Java代码实现的,它们来源于本地库的实现)

Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。

2.Java虚拟机栈:
执行Java方法服务(也就是字节码)
与程序计数器一样,Java虚拟机栈是线程私有的,它的生命周期与线程相同。
描述Java方法执行的内存模型,每个方法执行的同时会创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

  • 局部变量表存放了编译时期可知的各种基本数据类型和对象引用。局部变量表所需的内存空间在编译时期完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

Java虚拟机规范对这个区域规定了两种异常情况:
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;
如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;

3.本地方法栈:
为虚拟机使用到的Native方法服务
本地方法栈与虚拟机栈的区别:虚拟机栈为虚拟机执行Java方法服务(也就是字节码)而本地方法栈为虚拟机使用到的Native方法服务。
Java虚拟机规范对这个区域规定了两种异常情况:StackOverflowError 和 OutOfMemoryError异常。

4.Java堆:
存放对象实例+垃圾回收机制
Java堆是被所有的线程共享的一块内存区域,在虚拟机启动时创建。Java堆的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾回收器管理的主要区域,从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以Java堆可以细分为:新生代、老生代;从内存分配的角度看,线程共享的Java堆可能划分出多个线程私有的分配缓冲区(TLAB)。

Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

Java虚拟机规范规定,如果在堆上没有内存完成实例分配,并且堆上也无法再扩展时,将会抛出OutOfMemoryError异常。

Java堆内存的OOM异常:

内存泄露:指程序中一些对象不会被GC所回收,它始终占用内存,即被分配的对象引用链可达但已无用。

内存溢出:程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

5.方法区:

被所有的线程共享的一块内存区域。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

不需要连续的内存和可以选择固定大小或者可扩展之外,还可以选择不实现垃圾回收。

Java虚拟机规范规定,当方法区无法满足内存分配的需求时,将抛出OutOfMemoryError异常。

● 请你说一说有哪几种垃圾回收算法

参考回答:
Tracing算法(Tracing Collector) 标记—清除算法:分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。
Compacting算法(Compacting Collector)标记—整理算法:标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

Copying算法(Copying Collector):将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。

Adaptive算法(Adaptive Collector):监控当前堆的使用情况,并将选择适当算法的垃圾收集器。

● 请你回答一下GC Root可以是哪些

参考回答:
1 、 虚拟机栈(栈帧中的本地变量表)中引用的对象。
2、 本地方法栈中JNI(即一般说的native方法)引用的对象。

3、 方法区中的静态变量和常量引用的对象。

● 请你说一下OOM可能发生在哪,怎么查看,怎么调优
参考回答:
除了程序计数器不会抛出OOM外,其他各个内存区域都可能会抛出OOM。
最常见的OOM情况有以下三种:

• java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。

• java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。

• java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。

OOM分析–heapdump

要dump堆的内存镜像,可以采用如下两种方式:

• 设置JVM参数-XX:+HeapDumpOnOutOfMemoryError,设定当发生OOM时自动dump出堆信息。不过该方法需要JDK5以上版本。

• 使用JDK自带的jmap命令。"jmap -dump:format=b,file=heap.bin " 其中pid可以通过jps获取。

dump堆内存信息后,需要对dump出的文件进行分析,从而找到OOM的原因。常用的工具有:

• mat: eclipse memory analyzer, 基于eclipse RCP的内存分析工具。

• jhat:JDK自带的java heap analyze tool,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等,并支持对象查询语言OQL,分析相关的应用后,可以通过http://localhost:7000来访问分析结果。不推荐使用,因为在实际的排查过程中,一般是先在生产环境 dump出文件来,然后拉到自己的开发机器上分析,所以,不如采用高级的分析工具比如前面的mat来的高效。

● 请你说一下类加载

参考回答:
1、什么是类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

加载.class文件的方式

– 从本地系统中直接加载

– 通过网络下载.class文件

– 从zip,jar等归档文件中加载.class文件

– 从专有数据库中提取.class文件

– 将Java源文件动态编译为.class文件

2、类的生命周期

类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或**另一个阶段。

A.加载:查找并加载类的二进制数据

加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

1、通过一个类的全限定名来获取其定义的二进制字节流。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

B.连接

– 验证:确保被加载的类的正确性

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

• 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

• 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

• 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

• 符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

– 准备:为类的静态变量分配内存,并将其初始化为默认值

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

– 解析:把类中的符号引用转换为直接引用

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

C.初始化

初始化,为类的静态变量的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

①声明类变量是指定初始值

②使用静态代码块为类变量指定初始值

JVM初始化步骤

1、假如这个类还没有被加载和连接,则程序先加载并连接该类

2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

3、假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

– 创建类的实例,也就是new的方式

– 访问某个类或接口的静态变量,或者对该静态变量赋值

– 调用类的静态方法

反射(如Class.forName(“com.shengsiyuan.Test”))

– 初始化某个类的子类,则其父类也会被初始化

– Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

D.结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

– 执行了System.exit()方法

– 程序正常执行结束

– 程序在执行过程中遇到了异常或错误而异常终止

– 由于操作系统出现错误而导致Java虚拟机进程终止

3、类的加载

类加载有三种方式:

• 命令行启动应用时候由JVM初始化加载

• 通过Class.forName()方法动态加载

• 通过ClassLoader.loadClass()方法动态加载

4、双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派机制:

• 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

• 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

• 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

• 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

● 请你说一下AQS

参考回答:
Java并发包(JUC)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrangLock、Semaphore,它们的实现都用到了一个共同的基类–AbstractQueuedSynchronizer,简称AQS。AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。状态信息通过procted类型的getState,setState,compareAndSetState进行操作

AQS支持两种同步方式:1.独占式2.共享式。这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。总之,AQS为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。同步器的设计是基于模板方法模式的,一般的使用方式是这样:

1.使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)

2.将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

● 请你说一下volatile
轻量级的同步机制
参考回答:
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序
《深入理解Java虚拟机》中对volatile的描述:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

● 请你说一下死锁的原因,以及如何打破,如何查看死锁进程状态

参考回答:
1、死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。死锁的四个必要条件
• 互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。

• 请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源。

• 非剥夺条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。

• 循环等待条件(Circular wait):系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。

java中产生死锁可能性的最根本原因是:1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环;2)默认的锁申请操作是阻塞的。

如,线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。

2、避免死锁:

• 方案一:破坏死锁的循环等待条件

• 方法二:破坏死锁的请求与保持条件,使用lock的特性,为获取锁操作设置超时时间。这样不会死锁(至少不会无尽的死锁)

• 方法三:设置一个条件遍历与一个锁关联。该方法只用一把锁,没有chopstick类,将竞争从对筷子的争夺转换成了对状态的判断。仅当左右邻座都没有进餐时才可以进餐。提升了并发度。

死锁检测:

  • 让死锁发生,当发生死锁时,检测到死锁的发生和尝试解除死锁、恢复系统正常状态。

3、linux中查看死锁进程状态

使用pstack 和 gdb 工具对死锁程序进行分析

pstack 进程号 查看各个线程的堆栈信息

当进程吊死的时候,多次使用,死锁的线程将一直处于等锁的状态,确定某些线程一直没有变化,一直处于等锁的状态。那么这些线程很可能是死锁了。如果怀疑哪些线程发生死锁了,可以采用gdb 进一步attach线程并进行分析。

执行命令gdb attach 进程号,进入gdb调试终端

运行:(gdb) info thread

● 请你说一下内存泄漏

参考回答:
内存泄漏是堆中存在不再使用的对象但垃圾收集器无法从内存中删除它们的情况,因此它们会被不必要地一直存在.

1、单例造成的内存泄漏

由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。

2、非静态内部类创建静态实例造成的内存泄漏

非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,从而导致Activity的内存资源不能被正常回收。

3、Handler造成的内存泄漏

4、线程造成的内存泄漏

如果任务在Activity销毁之前还未完成,那么将导致Activity的内存资源无法被回收,从而造成内存泄漏。

5、资源未关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。

6、使用ListView时造成的内存泄漏

7、集合容器中的内存泄露

8、WebView造成的泄露

避免内存泄漏:

1、在涉及使用Context时,对于生命周期比Activity长的对象应该使用Application的Context。

2、对于需要在静态内部类中使用非静态外部成员变量(如:Context、View ),可以在静态内部类中使用弱引用来引用外部类的变量来避免内存泄漏。

3、对于不再需要使用的对象,显示的将其赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null。

4、保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。

5、对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:

1)将内部类改为静态内部类

2)静态内部类中使用弱引用来引用外部类的成员变量

● 请你说一说class和interface的区别

参考回答:
1、接口类似于类,但接口的成员都没有执行方式,它只是方法、属性、事件和索引的组合而已,并且也只能包含这四种成员;类除了这四种成员之外还可以有别的成员(如字段)。
2、不能实例化一个接口,接口只包括成员的签名;而类可以实例化(abstract类除外)。

3、接口没有构造函数,类有构造函数。

4、接口不能进行运算符的重载,类可以进行运算符重载。

5、接口的成员没有任何修饰符,其成员总是公共的,而类的成员则可以有修饰符(如:虚拟或者静态)。

6、派生于接口的类必须实现接口中所有成员的执行方式,而从类派生则不然。

● 请你说一说强引用和弱引用

参考回答:
强引用:
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:

Object o=new Object(); // 强引用

当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如果不使用时,要通过如下方式来弱化引用,如下:

o=null; // 帮助垃圾收集器回收此对象

显式地设置o为null,或超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于gc的算法

弱引用:

弱引用也是用来描述非必需对象的,只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。提供WeakReference类实现弱引用。

例子:

String str=new String(“abc”);

WeakReference abcWeakRef = new WeakReference(str);

str=null;

当垃圾回收器进行扫描回收时等价于:

str = null;

System.gc();

如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用Weak Reference 来记住此对象。

String abc = abcWeakRef.get(); //该代码会让str再次变为一个强引用:

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就是用弱引用。这个引用不会在对象的垃圾回收判断中产生任何附加的影响。