面试Java开发常用题的答案
一、数据结构与算法基础
· 说一下几种常见的排序算法和分别的复杂度。
· 用Java写一个冒泡排序算法
/**
现在有一个包含1000个数的数组,仅前面100个无序,后面900个都已排好序且都大于前面100个数字,那么在第一趟遍历后,最后发生交换的位置必定小于100,且这个位置之后的数据必定已经有序了,也就是这个位置以后的数据不需要再排序了,于是记录下这位置,第二次只要从数组头部遍历到这个位置就可以了。如果是对于上面的冒泡排序算法2来说,虽然也只排序100次,但是前面的100次排序每次都要对后面的900个数据进行比较,而对于现在的排序算法3,只需要有一次比较后面的900个数据,之后就会设置尾边界,保证后面的900个数据不再被排序。
*/
public static void bubbleSort(int [] a, int n){
int j , k;
int flag = n ;//flag来记录最后交换的位置,也就是排序的尾边界
while (flag > 0){//排序未结束标志
k = flag; //k 来记录遍历的尾边界
flag = 0;
for(j=1; j<k; j++){
if(a[j-1] > a[j]){//前面的数字大于后面的数字就交换
//交换a[j-1]和a[j]
int temp;
temp = a[j-1];
a[j-1] = a[j];
a[j]=temp;
//表示交换过数据;
flag = j;//记录最新的尾边界.
}
}
}
}
· 描述一下链式存储结构。
它不要求逻辑上相邻的元素在物理位置上也相邻。因此它没有顺序存储结构所具有的弱点,同时也失去了顺序表可随机存取的优点。
其特点主要表现为:
1、比顺序存储结构的存储密度小;
2、插入、删除灵活,结点可以被插入到链表的任何位置,首、中、末都可以,而且不必要移动结点中的指针;
3、链表的大小可以按需伸缩,是一种动态存储结构,其实现的集合在增、删方面性能更高;
4、查找结点时的效率就相对数组较低,只能从第一个结点开始顺着链表逐个查找(这是他的缺点)。
· 如何遍历一棵二叉树?
二叉树的遍历分为三种:
前序遍历:按照“根左右”,先遍历根节点,再遍历左子树 ,再遍历右子树
中序遍历:按照“左根右“,先遍历左子树,再遍历根节点,最后遍历右子树
后续遍历:按照“左右根”,先遍历左子树,再遍历右子树,最后遍历根节点
其中前,后,中指的是每次遍历时候的根节点被遍历的顺序
package com.tree;
import java.util.ArrayList;
import java.util.List;
public class Tree {
private Node root;
private List<Node> list=new ArrayList<Node>();
public Tree(){
init();
}
//树的初始化:先从叶节点开始,由叶到根
public void init(){
Node x=new Node("X",null,null);
Node y=new Node("Y",null,null);
Node d=new Node("d",x,y);
Node e=new Node("e",null,null);
Node f=new Node("f",null,null);
Node c=new Node("c",e,f);
Node b=new Node("b",d,null);
Node a=new Node("a",b,c);
root =a;
}
//定义节点类:
private class Node{
private String data;
private Node lchid;//定义指向左子树的指针
private Node rchild;//定义指向右子树的指针
public Node(String data,Node lchild,Node rchild){
this.data=data;
this.lchid=lchild;
this.rchild=rchild;
}
}
/**
* 对该二叉树进行前序遍历 结果存储到list中 前序遍历:ABDXYCEF
*/
public void preOrder(Node node)
{
list.add(node); //先将根节点存入list
//如果左子树不为空继续往左找,在递归调用方法的时候一直会将子树的根存入list,这就做到了先遍历根节点
if(node.lchid != null)
{
preOrder(node.lchid);
}
//无论走到哪一层,只要当前节点左子树为空,那么就可以在右子树上遍历,保证了根左右的遍历顺序
if(node.rchild != null)
{
preOrder(node.rchild);
}
}
/**
* 对该二叉树进行中序遍历 结果存储到list中
*/
public void inOrder(Node node)
{
if(node.lchid!=null){
inOrder(node.lchid);
}
list.add(node);
if(node.rchild!=null){
inOrder(node.rchild);
}
}
/**
* 对该二叉树进行后序遍历 结果存储到list中
*/
public void postOrder(Node node)
{
if(node.lchid!=null){
postOrder(node.lchid);
}
if(node.rchild!=null){
postOrder(node.rchild);
}
list.add(node);
}
/**
* 返回当前数的深度
* 说明:
* 1、如果一棵树只有一个结点,它的深度为1。
* 2、如果根结点只有左子树而没有右子树,那么树的深度是其左子树的深度加1;
* 3、如果根结点只有右子树而没有左子树,那么树的深度应该是其右子树的深度加1;
* 4、如果既有右子树又有左子树,那该树的深度就是其左、右子树深度的较大值再加1。
*
* @return
*/
public int getTreeDepth(Node node) {
if(node.lchid == null && node.rchild == null)
{
return 1;
}
int left=0,right = 0;
if(node.lchid!=null)
{
left = getTreeDepth(node.lchid);
}
if(node.rchild!=null)
{
right = getTreeDepth(node.rchild);
}
return left>right?left+1:right+1;
}
//得到遍历结果
public List<Node> getResult()
{
return list;
}
public static void main(String[] args) {
Tree tree=new Tree();
System.out.println("根节点是:"+tree.root);
//tree.preOrder(tree.root);
tree.postOrder(tree.root);
for(Node node:tree.getResult()){
System.out.println(node.data);
}
System.out.println("树的深度是"+tree.getTreeDepth(tree.root));
}
}
二叉树与一般树的区别
- 一般树的子树不分次序,而二叉树的子树有左右之分.
- 由于二叉树也是树的一种,所以大部分的树的概念,对二叉树也适用.
- 二叉树的存贮:每个节点只需要两个指针域(左节点,右节点),有的为了操作方便也会 增加指向父级节点的指针,除了指针域以外,还会有一个数据域用来保存当前节点的信息
二叉树的特点:
- 性质1:在二叉树的第i层上至多有2^(i-1)个节点(i >= 1)
- 性质2:深度为k的二叉树至多有2^k-1个节点(k >=1)
- 性质3:对于任意一棵二叉树T而言,其叶子节点数目为N0,度为2的节点数目为N2,则有N0 = N2 + 1。
- 性质4:具有n个节点的完全二叉树的深度。
· 倒排一个LinkedList。
根据LinkedList的实现,LinkedList的底层是双向链表,它在get任何一个位置的数据的时候,都会把前面的数据走一遍。用迭代器或者foreach循环(foreach循环的原理就是迭代器)去遍历LinkedList即可,这种方式是直接按照地址去找数据的,将会大大提升遍历LinkedList的效率。
public static <T> LinkedList<T> reverse(LinkedList<T> linkedList)
{
if (linkedList == null) return linkedList;
LinkedList<T> temp_linkedlist = new LinkedList<T>();
for (T item: linkedList) {
temp_linkedlist.addLast(item);
}
return temp_linkedlist;
}
· 用Java写一个遍历目录下面的所有文件。
public static void foreachFileList(String filePath) throws IOException {
LinkedList<File> linkedList = new LinkedList<File>();
File file = new File(filePath);
if (file.exists()) {
linkedList.add(file);
while (true) {
file = linkedList.poll();
if (file == null) break;
File[] fileList = file.listFiles();
for (File fileItem : fileList) {
if (fileItem.isDirectory()) {
linkedList.add(fileItem);
continue;//for
}
if (fileItem.isFile())
System.out.println(fileItem.getCanonicalPath());
}
}
}
}
二、Java基础
· 接口与抽象类的区别?
- 一个类可以实现多个接口,但只能继承最多一个抽象类
- 抽象类可以包含具体的方法;接口所有的方法都是抽象的(不管是否对接口声明都是抽象的)(jdk1.7以前,jdk1.8开始新增功能接口中有default 方法,有兴趣自己研究)
- 抽象类可以声明和使用字段;接口则不能,但是可以创建静态的final常量
- 抽象类中的方法可以是public、protected、private或者默认的package;接口的方法都是public(不管是否声明,接口都是公开的)
- 抽象类可以定义构造函数,接口不能。
- 接口被声明为public,省略后,包外的类不能访问接口
· Java中的异常有哪几类?分别怎么使用?
- Throwable包含了错误(Error)和异常(Excetion两类)
- Exception又包含了 运行时异常(RuntimeException, 又叫非检查异常) 和 非运行时异常(又叫检查异常)
- (1) Error是程序无法处理了, 如果OutOfMemoryError、OutOfMemoryError等等, 这些异常发生时, java虚拟机一般会终止线程 .
-(2) 运行时异常都是RuntimeException类及其子类,如 NullPointerException、IndexOutOfBoundsException等, 这些异常是不检查的异常, 是在程序运行的时候可能会发生的, 所以程序可以捕捉, 也可以不捕捉. 这些错误一般是由程序的逻辑错误引起的, 程序应该从逻辑角度去尽量避免. - (3) 检查异常是运行时异常以外的异常, 也是Exception及其子类, 这些异常从程序的角度来说是必须经过捕捉检查处理的, 否则不能通过编译. 如IOException、SQLException等
- (1) Error是程序无法处理了, 如果OutOfMemoryError、OutOfMemoryError等等, 这些异常发生时, java虚拟机一般会终止线程 .
· 常用的集合类有哪些?比如List如何排序?
-
常用的集合分为List(有序排放)、Map(以名和值一一对应的存放)、Set(既无序也没名).在这三者之中其中List和Set是Collection接口的子接口,而Map不是Collection接口的子接口.
- List常用有:ArrayList和LinkedList,Vecotr(线程安全)
- Set常用有: TreeSet, HashSet 元素不可重复,内部结构用HashMap,Key为Set的item值,value为一个固定的常量。java.util.Collections.newHashSetFromMap(),内部其实质还是通过ConcurrentHashMap实现线程安全的。
- Map: TreeMap和LinkedHashMap,HashMap,HashTable(线程安全)
-
sort()方法排序的本质其实也是借助Comparable接口和Comparator接口的实现,一般有2种用法:
- 直接将需要排序的list作为参数传入,此时list中的对象必须实现了Comparable接口,然后sort会按升序的形式对元素进行排序;
- 传入list作为第一个参数,同时追加一个Comparator的实现类作为第二个参数,然后sort方法会根据Comparator接口的实现类的逻辑,按升序进行排序;
· ArrayList和LinkedList内部的实现大致是怎样的?他们之间的区别和优缺点?
- Linkedlist集合的优势:添加元素时可以指定位置,比ArrayList集合添加元素要快很多。
- Linkedlist在get很慢,LinkedList在get任何一个位置的数据的时候,都会把前面的数据走一遍。尽量不使用,而使用foreach LinkedList的方式来直接取得数据。
- 这两种方式各有优缺,为更好的使用可以将这两者进行联合使用,使用Linkedlist集合进行存储和添加元素,使用Arraylist集合进行get获取元素。
· 内存溢出是怎么回事?请举一个例子?
- 内存溢出(out of memory)通俗理解就是内存不够,在计算机程序中通俗的理解就是开辟的内存空间得不到释放。
- OOM有堆溢出,栈溢出,方法区溢出(主要是动态生成class的处理过多)
· ==和equals的区别?
- ==号在比较基本数据类型时比较的是值,而用==号比较两个对象时比较的是两个对象的地址值
- Object类中equals()方法底层依赖的是==号,那么,在所有没有重写equals()方法的类中,调用equals()方法其实和使用==号的效果一样,然而,Java提供的所有类中,绝大多数类都重写了equals()方法,重写后的equals()方法一般都是比较两个对象的值
· hashCode方法的作用?
- hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;
- 如果两个对象相同,就是适用于equals(Java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;
- 如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;
- 两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。
· NIO是什么?适用于何种场景?
- JDK引入了一种基于通道和缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆的 DirectByteBuffer 对象作为这块内存的引用进行操作,避免了在 Java 堆和 Native 堆中来回复制数据。
- NIO 是一种同步非阻塞的 IO 模型。同步是指线程不断轮询 IO 事件是否就绪,非阻塞是指线程在等待 IO 的时候,可以同时做其他任务。同步的核心就是 Selector,Selector 代替了线程本身轮询 IO 事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当 IO 事件就绪时,可以通过写道缓冲区,保证 IO 的成功,而无需线程阻塞式地等待。
· HashMap实现原理,如何保证HashMap的线程安全?
HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。HashMap底层就是一个数组结构,数组中的每一项又是一个链表。
- =使用 java.util.Hashtable 类,此类是线程安全的。Hashtable是通过每个方法用synchronized来处理,性能不及ConcurrentHashMap
- 使用 java.util.concurrent.ConcurrentHashMap,此类是线程安全的。采用了分段锁实现同步。
- 使用 java.util.Collections.synchronizedMap() 方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作。
· JVM内存结构,为什么需要GC?
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存,简化代码开发。
· NIO模型,select/epoll的区别,多路复用的原理
(过长,没有回答)
· Java中一个字符占多少个字节,扩展再问int, long, double占多少字节
Java char: utf-16:2个字节, int-4, long-8,double-9
· 创建一个类的实例都有哪些办法?
- 关键字 new。工厂模式是对这种方式的包装;
- 类实现克隆接口,克隆一个实例。原型模式是一个应用实例;
- 用该类的加载器,newinstance。java的反射,反射使用实例:Spring的依赖注入、切面编程中动态代理等取得
- 通过IO流反序列化读取一个类,获得实例。
· final/finally/finalize的区别?
- final:如果一个类被final修饰,意味着该类不能派生出新的子类,不能作为父类被继承。因此一个类不能被声明为abstract,又被声明为final。将变量或方法声明为final。可以保证他们在使用的时候不被改变。其初始化可以在两个地方:一是其定义的地方,也就是在final变量在定义的时候就对其赋值;二是在构造函数中。这两个地方只能选其中的一个。被声明为final的方法也只能使用,不能重写。
- finally:在异常处理的时候,提供finally块在成功或失败都可以执行任何的清除操作。
- finalize:finalize是方法名,java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的,可以从Object.finalize()继承下来。
· Session/Cookie的区别?
- cookie是客户端的会话状态的一种储存机制。一般限制4k以内。session是一种服务器端的信息管理机制。
- session产生的session_id放在cookie里面,如果用户把cookie禁止掉,可以通过在url中保留session_id
· String/StringBuffer/StringBuilder的区别,扩展再问他们的实现?
StringBuilder:线程非安全的,StringBuffer:线程安全的,三者在执行速度方面的比较:StringBuilder > StringBuffer > String
· Servlet的生命周期?
- Servlet 通过调用 init () 方法进行初始化。
- Servlet 调用 service() 方法来处理客户端的请求。
- Servlet 通过调用 destroy() 方法终止(结束)。
- 最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。
· 如何用Java分配一段连续的1G的内存空间?需要注意些什么?
使用ArrayList来分配,注意堆内存不足造面OOM
· Java有自己的内存回收机制,但为什么还存在内存泄漏的问题呢?
主要是没有释放对象引用造成的内存泄漏,比如下例:
class MyList{
/*
* 此处只为掩饰效果,并没有进行封装之类的操作
* 将List集合用关键字 static 声明,这时这个集合将不属于任MyList 对象,而是一个类成员变量
*/
public static List<String> list = new ArrayList<String>();
}
class Demo{
public static void main(String[] args) {
MyList list = new MyList();
list.list.add("123456");
// 此时即便我们将 list指向null,仍然存在内存泄漏,因为MyList中的list是静态的,它属于类所有而不属于任何特定的实例
list = null;
}
}
· 什么是java序列化,如何实现java序列化?(写一个实例)?
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。Java的序列化需要实现Serializable接口。
//序列化后生成指定文件路径
File file = new File("D:" + File.separator + "person.ser") ;
ObjectOutputStream oos = null ;
//装饰流(流)
oos = new ObjectOutputStream(new FileOutputStream(file)) ;
//实例化类
Person per = new Person("张三",30) ;
oos.writeObject(per) ;
//把类对象序列化
oos.close() ;
· String s = new String("abc");创建了几个 String Object?
两个对象,一个是“abc”对象,在常量池中创建;一个是new关键字创建的s对象指向“abc”。
三、JVM
· JVM堆的基本结构。
java_heap_struct.jpg参考阅读:JVM内存堆布局图解分析
· JVM的垃圾算法有哪几种?CMS垃圾回收的基本流程?
基本的算法有:
-
标记-清除算法
等待被回收对象在被标记后直接对对象进行清除,会带来另一个新的问题——内存碎片化。如果下次有比较大的对象实例需要在堆上分配较大的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。 -
复制算法(Java堆中新生代的垃圾回收算法)
此GC算法实际上解决了标记-清除算法带来的“内存碎片化”问题。首先还是先标记处待回收内存和不用回收的内存,下一步将不用回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域则是连续的。它的缺点就是会损失掉部分系统内存,因为你总要腾出一部分内存用于复制。 -
标记-压缩算法(或称为标记-整理算法,Java堆中老年代的垃圾回收算法)
jvm_gc.png
对于新生代,大部分对象都不会存活,所以在新生代中使用复制算法较为高效,而对于老年代来讲,大部分对象可能会继续存活下去,如果此时还是利用复制算法,效率则会降低。标记-压缩算法首先还是“标记”,标记过后,将不用回收的内存对象压缩到内存一端,此时即可直接清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。老年代的垃圾回收称为“Major GC”。
CMS的基本流程:
- 初始标记 :在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
- 并发标记 :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
- 并发预清理 :并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。
- 重新标记 :这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"跟对象"开始向下追溯,并处理对象关联。
- 并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
- 并发重置 :这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收
延伸说明:
- CMS(Concurrent Mark-Sweep) 在启动JVM参数加上-XX:+UseConcMarkSweepGC,这个参数表示对于老年代的回收采用CMS。CMS采用的基础算法是:标记—清除(Mark-Sweep)。CMS不会整理、压缩堆空间。这样就会有一个问题:经过CMS收集的堆会产生空间碎片。CMS的另一个缺点是它需要更大的堆空间。
- 如果你的应用程序对停顿比较敏感,并且在应用程序运行的时候可以提供更大的内存和更多的CPU(也就是硬件牛逼),那么使用CMS来收集会给你带来好处。还有,如果在JVM中,有相对较多存活时间较长的对象(老年代比较大)会更适合使用CMS。
· JVM有哪些常用启动参数可以调整,描述几个?
各主要JVM启动参数的作用如下:
-Xms:设置jvm内存的初始大小
-Xmx:设置jvm内存的最大值
-Xmn:设置新域的大小(这个似乎只对jdk1.4来说是有效的,后来就废弃了)
-Xss:设置每个线程的堆栈大小(也就是说,在相同物理内存下,减小这个值能生成更多的线程)
-XX:NewRatio:设置新域与旧域之比,如-XX:NewRatio=4就表示新域与旧域之比为1:4
-XX:NewSize:设置新域的初始值
-XX:MaxNewSize:设置新域的最大值
-XX:MaxPermSize:设置永久域的最大值
-XX:SurvivorRatio=n:设置新域中Eden区与两个Survivor区的比值。(Eden区主要是用来存放新生的对象,而两个Survivor区则用来存放每次垃圾回收后存活下来的对象)
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimestamps -XX:+PrintGCApplicationStopedTime
JVM启动参数使用中常见的错误:
java.lang.OutOfMemoryError相信很多开发人员都用到过,这个主要就是JVM参数没有配好引起的,但是这种错误又分两种:java.lang.OutOfMemoryError:Javaheapspace和java.lang.OutOfMemoryError:PermGenspace,其中前者是有关堆内存的内存溢出,可以同过配置-Xms和-Xmx参数来设置,而后者是有关永久域的内存溢出,可以通过配置-XX:MaxPermSize来设置。
· 如何查看JVM的内存使用情况?
jstat,jmap,jstack,j viusalvm
· Java程序是否会内存溢出,内存泄露情况发生?举几个例子。
- 静态集合类引起内存泄漏:
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
//o的对象还在Vector中,因此需要从中移除,或者直接把vector=null
- 各种资源性连接没有正确关闭.比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。
- 不正确使用单例模式是引起内存泄漏的一个常见问题.
- 当集合里面的对象属性被修改后,造成该对象的Hashcode改变了,再调用remove()方法时不起作用。
· 你常用的JVM配置和调优参数都有哪些?分别什么作用?
- Trace跟踪参数(跟踪GC、类、变量的内存变化情况)
- 打开GC跟踪日志(每次执行GC的信息都能打印,获得执行时间,空间大小):
- -verbose:gc 或 -XX:+printGC 或 -XX:+printGCDetails
- 类加载监控:(监控类加载的顺序)-XX:+TraceClassLoading
- 堆的分频参数
- -Xmx1024M 指定最大堆,JVM最多能够使用的堆空间 (超过该空间引发OOM)
- -Xms128M 指定最小堆,JVM至少会有的堆空间(尽可能维持在最小堆)
- -Xmn 128M(new) 设置新生代大小
-
栈的分配参数
-Xss 每个线程都有独立的栈空间(几百k,比较小)需要大量线程时,需要尽可能减小栈空间
栈空间太小-----StackOverFlow栈溢出(一般递归时产生大量局部变量导致)总结:
1.根据实际情况调整新生代和幸存代的大小
2.官方推荐:新生代占堆空间3/8
3.幸存代占新生代1/10
4.OOM时,dump出堆到文件,方便排查
· 常用的GC策略,什么时候会触发YGC,什么时候触发FGC?
常见内存回收策略:
- 串行&并行
串行:单线程执行内存回收工作。十分简单,无需考虑同步等问题,但耗时较长,不适合多cpu。
并行:多线程并发进行回收工作。适合多CPU,效率高。 - 并发& stop the world
stop the world:jvm里的应用线程会挂起,只有垃圾回收线程在工作进行垃圾清理工作。简单,无需考虑回收不干净等问题。
并发:在垃圾回收的同时,应用也在跑。保证应用的响应时间。会存在回收不干净需要二次回收的情况。 - 压缩&非压缩©
压缩:在进行垃圾回收后,会通过滑动,把存活对象滑动到连续的空间里,清理碎片,保证剩余的空间是连续的。
非压缩:保留碎片,不进行压缩。
copy:将存活对象移到新空间,老空间全部释放。(需要较大的内存。)
- YGC :对新生代堆进行GC: edn空间不足
- FGC的时机:old空间不足; 2.perm空间不足;3.显示调用System.gc() ,包括RMI等的定时触发; 4.YGC时的悲观策略;dump live的内存信息时(jmap –dump:live)。
四、多线程/并发
· 如何创建线程?如何保证线程安全?
保证线程安全: 竞争与原子操作、同步与锁、可重入、使用valatile防止CPU过度优化。
- 继承Thread类,必须重写run方法,在run方法中定义需要执行的任务。
- 实现Runnable接口。
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
class MyRunnable implements Runnable{... }
Note: Java如何创建进程:
第一种方式是通过Runtime.exec()方法来创建一个进程,第二种方法是通过ProcessBuilder的start方法来创建进程。
· 什么是死锁?如何避免死锁
- 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
- 按同样的顺序访问(锁定)资源
- 尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。 3. 使用Atom原子操作(无锁模式)
· Volatile关键字的作用?
volatile特性一:内存可见性,即线程A对volatile变量的修改,其他线程获取的volatile变量都是最新的。volatile特性二:可以禁止指令重排序,避免重排序指令后访问数据错乱.
Volatile可以看做一种轻量级的锁,但又和锁有些不同。
a) 它对于多线程,不是一种互斥(mutex)关系。
b) 用volatile修饰的变量,不能保证该变量状态的改变对于其他线程来说是一种“原子化操作”。
· HashMap在多线程环境下使用需要注意什么?为什么?
- 并发操作HashMap,是有可能带来死循环以及数据丢失的问题的。当我们调用get()这个链表中不存在的元素的时候,就会出现死循环。
- HashMap在并发执行put操作时会引起死循环,导致CPU利用率接近100%。因为多线程会导致HashMap的Node链表形成环形数据结构,一旦形成环形数据结构,Node的next节点永远不为空,就会在获取Node时产生死循环。
- 一句话总结就是,并发环境下的rehash过程可能会带来循环链表,导致死循环致使线程挂掉。
- HashTable,它并未使用分段锁,而是锁住整个数组,高并发环境下效率非常的低,会导致大量线程等待。同样的,Synchronized关键字、Lock性能都不如分段锁实现的ConcurrentHashMap。
· Java程序中启动一个线程是用run还是start?
start方法:通过该方法启动线程的同时也创建了一个线程,真正实现了多线程。而run方法只是当初普通的方法调用.
· 什么是守护线程?有什么用?
守护线程(即daemon thread)是服务线程,处理后台事务,如垃圾回收等,JVM内部的实现是如果运行的程序只剩下守护线程的话,程序将终止运行,直接结束。所以守护线程是作为辅助线程存在的。应用程序里的线程,一般都是用户自定义线程,用户也可以在应用程序代码自定义守护线程,只需要调用Thread类的t.setDaemon(true);设置一下即可。
· 线程和进程的差别是什么?
- 进程是系统进行资源分配的基本单位,有独立的内存地址空间; 线程是CPU调度的基本单位,没有单独地址空间,有独立的栈,局部变量,寄存器, 程序计数器等。
- 创建进程的开销大,包括创建虚拟地址空间等需要大量系统资源; 创建线程开销小,基本上只有一个内核对象和一个堆栈。
- 一个进程无法直接访问另一个进程的资源;同一进程内的多个线程共享进程的资源。
- 进程切换开销大,线程切换开销小;进程间通信开销大,线程间通信开销小。
- 线程属于进程,不能独立执行。每个进程至少要有一个线程,成为主线程
· Java里面的Threadlocal是怎样实现的?
- 作用:在并发环境下避免竞争、简化编程,高效.在并发环境下提供了一个逻辑上全局的访问点访问线程本地对象
- 原理: 每个线程内部都有一个hastable作为存储存储器保存线程本地对象集,ThreadLocal实例对象作为key可以被所有线程共享,这个实例对象就是我们所说得全局的访问点,通过它可以访问线程本地对象。所以我们可以这样说:通过ThreadLocal提供了逻辑上全局的线程本地对象。
· ConcurrentHashMap的实现原理是?
在ConcurrentHashMap中,它使用了多个锁代替HashTable中的单个锁,也就是锁分离技术(Lock Stripping)。 每个hash区间使用的ReentrantLock锁。
ConcurrentHashMap源码剖析
· sleep和wait区别
- 这两个方法来自不同的类分别是Thread和Object
- 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用.
synchronized(x){ x.notify() //或者wait() }
- sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
· notify和notifyAll区别
- wait此方法导致当前线程(称之为 T)将其自身放置在对象的等待集中,然后放弃此对象上的所有同步要求。
- 被wait的线程,想要继续运行的话,它必须满足2个条件:
- 由其他线程notify或notifyAll了,并且当前线程被通知到了
- 经过和其他线程进行锁竞争,成功获取到锁了
- 2个条件,缺一不可。其实在实现层面,notify和notifyAll都达到相同的效果,都只会有一个线程继续运行。但notifyAll免去了,线程运行完了通知其他线程的必要,因为已经通知过了。什么时候用notify,什么时候使用notifyAll,这就得看实际的情况了。
· 什么是条件锁、读写锁、自旋锁、可重入锁?
-
条件锁
在lock中提供了与之关联的条件,一个锁可能关联一个或多个条件,这些条件通过condition接口声明。目的是运行线程获取锁并且查看等待某一个条件是否满足,如果不满足则挂起直到某个线程唤醒它们。condition接口提供了挂起线程和唤起线程的机制; -
读写锁
Java中读写锁有个接口java.util.concurrent.locks.ReadWriteLock,也有具体的实现ReentrantReadWriteLock,它们不是继承关系,但都是基于 AbstractQueuedSynchronizer来实现。ReentrantReadWriteLock的锁策略有两种,分为公平策略和非公平策略注意: 在同一线程中,持有读锁后,不能直接调用写锁的lock方法 ,否则会造成死锁。。
-
可重入(Reentrant)锁
如果锁具备可重入性,则称作为可重入锁。像synchronized和 ReentrantLock都是可重入锁。假如某一时刻,线程A执行到了method1,此时线程 A获取了这个对象的锁,而由于method2也是synchronized方法,因为可重入,线程A不需要申请加锁即可执行。(可以理解锁的维度是线程,所以已拥有锁不需要再次申请加锁)。不可重入锁(自旋锁):不可以再次进入方法A,也就是说获得锁进入方法A是此线程在释放锁钱唯一的一次进入方法A。 -
可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
· 线程池ThreadPoolExecutor的几个参数说明?
- corePoolSize 核心线程池大小
- maximumPoolSize 线程池最大容量大小
- keepAliveTime 线程池空闲时,线程存活的时间,
- TimeUnit 时间单位
- ThreadFactory 创建线程的工厂
- BlockingQueue任务队列,用于保存等待执行的任务的阻塞队列。比如基于数组的有界ArrayBlockingQueue、,基于链表的无界LinkedBlockingQueue,最多只有一个元素的同步队列SynchronousQueue,优先级队列PriorityBlockingQueue
- RejectedExecutionHandler 线程拒绝策略
- 线程池在执行excute方法时,主要有以下四种情况:
- 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(需要获得全局锁)
- 如果运行的线程等于或多于corePoolSize ,则将任务加入BlockingQueue
- 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(需要获得全局锁)
- 如果创建新线程将使当前运行的线程超出maxiumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
线程池采取上述的流程进行设计是为了减少获取全局锁的次数。在线程池完成预热(当前运行的线程数大于或等于corePoolSize)之后,几乎所有的excute方法调用都执行步骤2。