Java超高频面试题汇总!
今天小白给大家整理一下Java在面试中的高频问题,涵盖Java基础、Java集合、Java多线程与并发和Java虚拟机四个方面,共计70+问题,看完这些让你再也不怕面试官提问了!
目录
Java基础知识高频问题
1.Java语言的三大特性是什么?
2.重载与重写的区别
3.接口和抽象类的区别是什么?
4.Java中的内部类说一下
5.说一下final关键字的作用
6.说一下String,StringBuilder和StringBuffer的区别
7.说一下Java中的==与eaquels的区别
8.Java访问修饰符有哪些?都有什么区别?
追问:怎么获取private修饰的变量
9.Java中的异常体系说一下?
追问:异常的处理方式?
10.Java中的深拷贝和浅拷贝说一下?
追问:浅拷贝与深拷贝的特点是什么?
Java集合高频问题
1.Java中的集合框架有哪些?
2.ArrayList和LinkedList的底层实现和区别?
追问:说说ArrayList的扩容机制?
3.HashMap的底层实现?扩容?是否线程安全?
追问:HashMap扩容的时候为什么是2的n次幂?
追问:HashMap的put方法说一下。
追问:HashMap源码中在计算hash值的时候为什么要右移16位?
4.Java中线程安全的集合有哪些?
追问:说一下ConcurrentHashMap的底层实现,它为什么是线程安全的?
5.HashMap和Hashtable的区别
6.HashMap和TreeMap的区别?
Java多线程与并发编程高频问题
1.说说什么是线程安全?如何实现线程安全?
追问:synsynchronized和ReentLock的区别是什么?
追问:syn和volatile的区别
追问:synchronized锁升级的过程说一下?
追问:synchronize锁的作用范围
2.Java中线程的状态有哪些?线程间的通信方式有哪些?
追问:sleep后进入什么状态,wait后进入什么状态?
追问:sleep和wait的区别?
追问:wait为什么是数Object类下面的方法?
追问:start方法和run方法有什么区别?
3.AQS了解吗?
追问:Java中的并发关键字
追问:你使用过哪个AQS组件,有将其用于多线程编程吗?(给一个例题说一下思路或者直接写)
4.CAS说一下
追问:CAS带来的问题是什么?如何解决的?
追问:什么是乐观锁,什么是悲观锁?
5.Java中创建线程的方式有哪些?
追问:.线程池的好处?说几个Java中常见的线程池?说一下其中的参数和运行流程?
追问:拒绝策略有哪些?
追问:线程池的参数如何确定呢?
追问:Java中常见的阻塞队列有哪些?
6.ThreaLocal知道吗?
追问:用它可能会带来什么问题?
追问:什么是强软弱虚引用?
Java虚拟机高频问题
1.介绍一下Java运行时数据区域,并说一下每个部分都存哪些内容?
追问:程序计数器可以为空吗?
追问:堆中又怎么细分的?
追问:哪些区域会造成OOM
2.Java中对象的创建过程是什么样的?
追问:内存分配的策略有哪些?
追问:对象头包含哪些?
追问:对象的访问定位方法有几种,各有什么优缺点?
3.如何判断对象已死?
追问:GCroot可以是哪些?
追问:被标志为GC的对象一定会被GC掉吗?
4.垃圾回收算法有哪些?详细叙述一下。
追问:新生代和老年代一般使用什么算法?
追问:为什么新生代不使用标记清除算法?
5.垃圾回收器有哪些?
追问:CMS垃圾回收器的过程是什么样的?会带来什么问题?
追问:G1垃圾回收器的改进是什么?相比于CMS突出的地方是什么?
追问:现在jdk默认使用的是哪种垃圾回收器?
6.内存分配策略是什么样的?
追问:内存溢出与内存泄漏的区别?
7.Jvm调优了解过吗?常用的命令和工具有哪些?
追问:内存持续上升,如何排查?
追问:jstack和jsp的区别是什么?
8.虚拟机的加载机制是什么样的?
追问:类加载有哪些?
追问:什么叫双亲委派机制?
追问:如何打破双亲委派机制?
Java基础知识高频问题
1.Java语言的三大特性是什么?
回答:Java语言的三大特性分别是封装、继承和多态。
封装是指将对象的属性私有化,提供一些可以访问属性的方法,我们通过访问这些方法得到对象的属性。
继承是指某新类继承已经存在的类,该新类拥有被继承的类的所有属性和方法,并且新类可以根据自己的情况拓展属性或方法。其中新类称为子类,原存在的类被称为父类。
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
注意:Java不支持多继承
多态是同一个行为具有多个不同表现形式或形态的能力。多态就是同一个接口,使用不同的实例而执行不同操作
多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。【摘自JavaGuide】
在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
2.重载与重写的区别
回答:
重载是发生在同一个类中,具有相同的方法名,但是有不同的参数,参数的个数不一样、参数的位置不一样,这就叫重载,常见的就比如构造方法,有有参构造和无参构造。
重写是发生在当子类继承父类时,对父类中的一些方法根据自己的需求进行重写操作。
3.接口和抽象类的区别是什么?
回答:接口(interface)和抽象类(abstract class)是支持抽象类定义的两种机制。
接口是公开的,不能有私有的方法或变量,接口中的所有方法都没有方法体
,通过关键字interface
实现。
抽象类是可以有私有方法或私有变量的,通过把类或者类中的方法声明为abstract来表示一个类是抽象类,被声明为抽象的方法不能包含方法体。子类实现方法必须含有相同的或者更低的访问级别(public->protected->private)。抽象类的子类为父类中所有抽象方法的具体实现,否则也是抽象类。
相同点:
(1)都不能被实例化
(2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
不同点:
(1)接口只有定义,不能有方法的实现,但java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
(3)接口强调特定功能的实现,而抽象类强调所属关系。
(4)接口方法默认修饰符是 public
,抽象方法可以有 public
、protected
和 default
这些修饰符(抽象方法就是为了被重写所以不能使用 private
关键字修饰!)。
(5)接口被用于常用的功能,便于日后维护和添加删除,而抽象类更倾向于充当公共类的角色,不适用于日后重新对立面的代码修改。从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
4.Java中的内部类说一下
回答:内部类有四种,分别是静态内部类、局部内部类、匿名内部和成员内部类
静态内部类:常见的main函数就是静态内部类,调用静态内部类通过“外部类.静态内部类”
局部内部类:定义在方法中的类叫做局部内部类。
匿名内部类:是指继承一个父类或者实现一个接口的方式直接定义并使用的类,匿名内部类没有class关键字,因为匿名内部类直接使用new生成一个对象
5.说一下final关键字的作用
回答:final关键字可以修饰类、方法和属性。
当final修饰类的时候,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地指定为 final 方法。
当final修饰方法的时候,表明这个方法不能被重写。
当final修饰属性的时候,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
6.说一下String,StringBuilder和StringBuffer的区别
String
类中使用 final
关键字修饰字符数组来保存字符串,private final char value[]
,所以 String 对象是不可变的。
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组保存字符串char[]value
但是没有用 final
关键字修饰,所以这两种对象都是可变的。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
7.说一下Java中的==与eaquels的区别
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。
eaquels:
- 情况 1:类没有重写 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况 2:类重写了 equals() 方法。一般,我们都重写 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
8.Java访问修饰符有哪些?都有什么区别?
回答:Java中的修饰符有public private protected
1、public:public表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用
2、private:private表示私有,私有的意思就是除了class自己之外,任何人都不可以直接使用,私有财产神圣不可侵犯嘛,即便是子女,朋友,都不可以使用。
3、protected:protected对于子女、朋友来说,就是public的,可以自由使用,没有任何限制,而对于其他的外部class,protected就变成private。
追问:怎么获取private修饰的变量
回答:private通过反射获取,可以设置setAccessable为true实现
9.Java中的异常体系说一下?
回答:Java中的异常主要分为Error和Exception
Error 指Java程序运行错误,如果程序在启动时出现Error,则启动失败;如果程序运行过程中出现Error,则系统将退出程序。出现Error是系统的内部错误或资源耗尽,Error不能在程序运行过程中被动态处理,一旦出现Error,系统能做的只有记录错误的原因和安全终止。
Exception 指 Java程序运行异常,在运行中的程序发生了程序员不期望发生的事情,可以被Java异常处理机制处理。Exception也是程序开发中异常处理的核心,可分为RuntimeException(运行时异常)和CheckedException(检查异常),如下图所示
RuntimeException(运行时异常):指在Java虚拟机正常运行期间抛出的异常,RuntimeException可以被捕获并处理,如果出现此情况,我们需要抛出异常或者捕获并处理异常。常见的有NullPointerException、ClassCastException、ArrayIndexOutOfBoundsException等
CheckedException(检查异常):指在编译阶段Java编译器检查CheckedException异常,并强制程序捕获和处理此类异常,要求程序在可能出现异常的地方通过try catch语句块捕获异常并处理异常。常见的有由于I/O错误导致的IOException、SQLException、ClassNotFoundException等。该类异常通常由于打开错误的文件、SQL语法错误、类不存等引起。
追问:异常的处理方式?
回答:异常处理方式有抛出异常和使用try catch语句块捕获异常两种方式。
(1)抛出异常:遇到异常时不进行具体的处理,直接将异常抛给调用者,让调用者自己根据情况处理。抛出异常的三种形式:throws、throw和系统自动抛出异常。其中throws作用在方法上,用于定义方法可能抛出的异常;throw作用在方法内,表示明确抛出一个异常。
(2)使用try catch捕获并处理异常:使用费try catch 捕获异常能够有针对性的处理每种可能出现的异常,并在捕获到异常后根据不同的情况做不同的处理。其使用过程比较简单:用try catch语句块将可能出现异常的代码抱起来即可。
10.Java中的深拷贝和浅拷贝说一下?
回答:深拷贝和浅拷贝都是对象拷贝
浅拷贝:按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。(浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。)
上图: 两个引用student1
和student2
指向不同的两个对象,但是两个引用student1
和student2
中的两个teacher
引用指向的是同一个对象,所以说明是浅拷贝
。
深拷贝:在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。(深拷贝把要复制的对象所引用的对象都复制了一遍。)
上图:两个引用student1
和student2
指向不同的两个对象,两个引用student1
和student2
中的两个teacher
引用指向的是两个对象,但对teacher
对象的修改只能影响student1
对象,所以说是深拷贝
。
摘自:JavaGuide
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
追问:浅拷贝与深拷贝的特点是什么?
浅拷贝特点
(1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个。
(2) 对于引用类型,比如数组或者类对象,因为引用类型是引用传递,所以浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响。
深拷贝特点
(1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
(2) 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。
(3) 对于有多层对象的,每个对象都需要实现 Cloneable
并重写 clone()
方法,进而实现了对象的串行层层拷贝。
(4) 深拷贝相比于浅拷贝速度较慢并且花销较大。
Java集合面试高频考点
1.Java中的集合框架有哪些?
回答:Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。
Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、TreeMap、LinkedHashMap 等等。
2.ArrayList和LinkedList的底层实现和区别?
回答:ArrayList底层使用的是 Object数组;LinkedList底层使用的是 双向链表 数据结构。
ArrayList:增删慢、查询快,线程不安全,对元素必须连续存储。
LinkedList:增删快,查询慢,线程不安全。
追问:说说ArrayList的扩容机制?
回答:通过阅读ArrayList的源码我们可以发现当以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。当插入的元素个数大于当前容量时,就需要进行扩容了, ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右。
3.HashMap的底层实现?扩容?是否线程安全?
回答:在jdk1.7之前HashMap是基于数组和链表实现的,而且采用头插法。
而jdk1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。采用尾插法。
HashMap默认的初始化大小为 16。当HashMap中的元素个数之和大于负载因子*当前容量的时候就要进行扩充,容量变为原来的 2 倍。(这里注意不是数组中的个数,而且数组中和链/树中的所有元素个数之和!)
注意:我们还可以在预知存储数据量的情况下,提前设置初始容量(初始容量 = 预知数据量 / 加载因子)。这样做的好处是可以减少 resize() 操作,提高 HashMap 的效率
美团面试的时候问到这个问题,还给出具体的值,让我算出初始值设置为多少合适?
HashMap是线程不安全的,其主要体现:
1.在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
追问:HashMap扩容的时候为什么是2的n次幂?
回答:数组下标的计算方法是(n-1)& hash,取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。 并且采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
追问:HashMap的put方法说一下。
回答:通过阅读源码,可以从jdk1.7和1.8两个方面来回答
1.根据key通过哈希算法与与运算得出数组下标
2.如果数组下标元素为空,则将key和value封装为Entry对象(JDK1.7是Entry对象,JDK1.8是Node对象)并放入该位置。
3.如果数组下标位置元素不为空,则要分情况
(i)如果是在JDK1.7,则首先会判断是否需要扩容,如果要扩容就进行扩容,如果不需要扩容就生成Entry对象,并使用头插法添加到当前链表中。
(ii)如果是在JDK1.8中,则会先判断当前位置上的TreeNode类型,看是红黑树还是链表Node
(a)如果是红黑树TreeNode,则将key和value封装为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value。
(b)如果此位置上的Node对象是链表节点,则将key和value封装为一个Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历过程中会判断是否存在当前key,如果存在则更新其value,当遍历完链表后,将新的Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果大于8,则会将链表转为红黑树
(c)将key和value封装为Node插入到链表或红黑树后,在判断是否需要扩容,如果需要扩容,就结束put方法。
追问:HashMap源码中在计算hash值的时候为什么要右移16位?
回答:我的理解是让元素在HashMap中更加均匀的分布,具体的可以看下图,下图是《阿里调优手册》里说的。
4.Java中线程安全的集合有哪些?
Vector:就比Arraylist多了个同步化机制(线程安全)。
Hashtable:就比Hashmap多了个线程安全。
ConcurrentHashMap:是一种高效但是线程安全的集合。
Stack:栈,也是线程安全的,继承于Vector。
追问:说一下ConcurrentHashMap的底层实现,它为什么是线程安全的?
回答:在jdk1.7是 分段的数组+链表 ,jdk1.8的时候跟HashMap1.8的时候一样都是基于数组+链表/红黑树。
ConcurrentHashMap是线程安全的
(1)在jdk1.7的时候是使用分段所segment,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
(2)在jdk1.8的时候摒弃了 Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。synchronized只锁定当前链表或红黑二叉树的首节点。
5.HashMap和Hashtable的区别
回答:
(1)线程是否安全: HashMap
是非线程安全的,HashTable
是线程安全的,因为 HashTable
内部的方法基本都经过synchronized
修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap
吧!);
(2)对 Null key 和 Null value 的支持: HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException
。
(3)初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证,下面给出了源代码)。也就是说 HashMap
总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
(4)底层数据结构: JDK1.8 以后的 HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
(5)效率: 因为线程安全的问题,HashMap
要比 HashTable
效率高一点。另外,HashTable
基本被淘汰,不要在代码中使用它;
6.HashMap和TreeMap的区别?
回答:
1、HashMap是通过hash值进行快速查找的;HashMap中的元素是没有顺序的;TreeMap中所有的元素都是有某一固定顺序的,如果需要得到一个有序的结果,就应该使用TreeMap;
2、HashMap和TreeMap都是线程不安全的;
3、HashMap继承AbstractMap类;覆盖了hashcode() 和equals() 方法,以确保两个相等的映射返回相同的哈希值;
TreeMap继承SortedMap类;他保持键的有序顺序;
4、HashMap:基于hash表实现的;使用HashMap要求添加的键类明确定义了hashcode() 和equals() (可以重写该方法);为了优化HashMap的空间使用,可以调优初始容量和负载因子;
TreeMap:基于红黑树实现的;TreeMap就没有调优选项,因为红黑树总是处于平衡的状态;
5、HashMap:适用于Map插入,删除,定位元素;
TreeMap:适用于按自然顺序或自定义顺序遍历键(key)
Java多线程与并发编程高频问题
1.说说什么是线程安全?如何实现线程安全?
回答:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。【摘自深入理解Jvm虚拟机】
实现线程安全的方式有三大种方法,分别是互斥同步、非阻塞同步和无同步方案。
互斥同步:同步是指多个线程并发访问共享数据时,保证共享数据在同一各时刻只被一条(或一些,当使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界去、互斥量和信号量都是常见的互斥实现方式。Java中实现互斥同步的手段主要有synchronized关键字或ReentrantLock等。
非阻塞同步类似是一种乐观并发的策略,比如CAS。
无同步方案,比如使用ThreadLocal。
追问1:synsynchronized和ReentLock的区别是什么?
相同点:
(1)都是可重入锁
(2)都保证了可见性和互斥性
(3)都可以用于控制多线程对共享对象的访问
不同点:
(1)ReentrantLock等待可中断
(2)synchronized中的锁是非公平的,ReentrantLock默认也是非公平的,但是可以通过修改参数来实现公平锁。
(3)ReentrantLock绑定多个条件
(4)synchronized是Java中的关键字是JVM级别的锁,而ReentrantLock是一个Lock接口下的实现类,是API层面的锁。
(5)synchronized隐式获取锁和释放锁,ReentrantLock显示获取和释放锁,在使用时避免程序异常无法释放锁,需要在finally控制块中进行解锁操作。
追问2:syn和volatile的区别
synchronized
关键字和 volatile
关键字是两个互补的存在,而不是对立的存在!
- volatile 关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而 synchronized` 关键字可以修饰方法以及代码块。
- volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
【摘自网络】
追问3:synchronized锁升级的过程说一下?
回答:在jdk1.6后Java对synchronize锁进行了升级过程,主要包含偏向锁、轻量级锁和重量级锁,主要是针对对象头MarkWord的变化。
(1)偏向锁:
为什么要引入偏向锁?
因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
偏向锁的升级
当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
(2)轻量级锁
为什么要引入轻量级锁?
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
轻量级锁什么时候升级为重量级锁?
线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;
如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。
但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
追问4:synchronize锁的作用范围
回答:
(1)synchronize作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象。
(2)synchronize作用于静态方法时,锁住的是Class实例
(3)synchronize作用于一个代码块时,锁住的是所有代码块中配置的对象。
2.Java中线程的状态有哪些?线程间的通信方式有哪些?
回答:Java中线程生命周期分为新建(New)、运行(Runnable)、阻塞(Blocked)、无限期等待(Waiting)、限期等待(Time Waiting)和结束(Terminated)这6种状态。
Java中线程间通信方式有:
互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
信号量(Semphares) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量
事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操
追问1:sleep后进入什么状态,wait后进入什么状态?
回答:sleep后进入Time waiting超时等待状态,wait后进入等待waiting状态。
追问2:sleep和wait的区别?
回答:
(1)sleep方法属于Thread类,wait方法属于Object类
(2)sleep方法暂停执行指定的时间,让出CPU给其他线程,但其监控状态依然保持在指定的时间过后又会自动恢复运行状态。
(3)在调用sleep方法的过程中,线程不会释放对象锁,而wait会释放对象锁。
追问3:wait为什么是数Object类下面的方法?
这个问题我被问到过两次,第一次不会(美团),就去百度搜了搜,第二次遇到就会了(贝壳),下面是网上搜到的。
所谓的释放锁资源实际是通知对象内置的monitor对象进行释放,而只有所有对象都有内置的monitor对象才能实现任何对象的锁资源都可以释放。又因为所有类都继承自Object,所以wait()就成了Object方法,也就是通过wait()来通知对象内置的monitor对象释放,而且事实上因为这涉及对硬件底层的操作,所以wait()方法是native方法,底层是用C写的。【来自网络】
追问4:start方法和run方法有什么区别?
(1)start方法用于启动线程,真正实现了多线程运行。在调用了线程的start方法后,线程会在后台执行,无须等待run方法体的代码执行完毕。
(2)通过调用start方法启动一个线程时,此线程处于就绪状态,并没有运行。
(3)run方法也叫做线程体,包含了要执行的线程的逻辑代码,在调用run 方法后,线程就进入运行状态,开始运行run方法中的代码,在run方法运行结束后,该线程终止,CPU在调度其他的线程。
3.AQS了解吗?
回答:AQS是一个抽象队列同步器,通过维护一个状态标志位state和一个先进先出的(FIFO)的线程等待队列来实现一个多线程访问共享资源的同步框架。
AQS的原理大概是这样的,给每个共享资源都设置一个共享锁,线程在需要访问共享资源时首先需要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果没有获取到共享锁,该线程被放入到等待队列中,等待下一次资源调度。
AQS定义了两种资源共享方式:独占式和共享式
独占式:只有一个线程能执行,具体的Java实现有ReentrantLock。
共享式:多个线程可同时执行,具体的Java实现有Semaphore和CountDownLatch。
AQS只是一个框架(模板模式),只定义了一个接口,具体资源的获取、释放都交由自定义同步器去实现。不同的自定义同步器争取用共享资源的方式也不同,自定义同步器在实现时只需实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护,如获取资源失败入队、唤醒出队等,AQS已经在顶层实现好,不需要具体的同步器在做处理。
追问1:Java中的并发关键字
Java中常见的并发关键字有CountDownLatch、CylicBarrier、Semaphore和volatile。
追问2:你使用过哪个AQS组件,有将其用于多线程编程吗?(给一个例题说一下思路或者直接写)
回答:这个可以自己去网上找一些,或者自己总结一些。我一般举的例子是我在笔试中遇到的一个例子。就有四个子线程分别统计四个盘的容量,然后最终通过一个主线程将四个盘的进行求和,输出总的容量。
用到了CountDownLatch,他是基于线程计数器来实现并发控制,主要用于主线程等待其他子线程都执行完毕后执行相关操作。
下面是代码:
class DiskMemory { private int totalSize; public int getSize() { return (new Random().nextInt(3) + 1) * 100;//加一是为了防止获取磁盘大小为0,不符合常理 } public void setSize(int size) { totalSize += size; } public int getTotalSize() { return totalSize; } } public class t3 { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(4); DiskMemory diskMemory = new DiskMemory(); //设置四个子线程 main函数为主线程 CountDownLatch countDownLatch = new CountDownLatch(4); for (int i = 0; i < 4; i++) { executorService.execute(() -> { try { int size = diskMemory.getSize(); diskMemory.setSize(size); //Thread.sleep(1000); System.out.println("线程执行,磁盘大小:" + size); } catch (Exception e) {//InterruptedException e e.printStackTrace(); } countDownLatch.countDown();//计数器减一 System.out.println("--------"); }); } try { countDownLatch.await();//唤醒主线程 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("磁盘总大小:" + diskMemory.getTotalSize()); //线程池使用完需要手动关闭 executorService.shutdown(); } }
4.CAS说一下
CAS指Compare and swap比较和替换是设计并发算法时用到的一种技术,CAS指令有三个操作数,分别是内存位置(在Java中可以简单的理解为变量的内存地址,用V表示),旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令在执行的时候,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不会执行更新。
追问1:CAS带来的问题是什么?如何解决的?
回答:ABA问题、循环时间长开销很大、只能保证一个共享变量的原子操作
一般加版本号进行解决(具体操作:乐观锁每次在执行数据的修改操作时都会带上一个版本号,在预期的版本号和数据的版本号一致时就可以执行修改操作,并对版本号执行加1操作,否则执行失败。)
追问2:什么是乐观锁,什么是悲观锁?
回答:悲观锁和乐观锁并不是某个具体的“锁”而是一种并发编程的基本概念。乐观锁和悲观锁最早出现在数据库的设计当中,后来逐渐被 Java 的并发包所引入。
悲观锁:认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观地认为,不加锁的并发操作一定会出问题。
乐观锁:正好和悲观锁相反,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新。
5.Java中创建线程的方式有哪些?
回答:Java中创建线程的方式有4种,分别是
(1)写一个类继承子Thread类,重写run方法
(2)写一个类重写Runable接口,重写run方法
(3)写一个类重写Callable接口,重写call方法
(4)使用线程池
追问1:线程池的好处?说几个Java中常见的线程池?说一下其中的参数和运行流程?
回答:使用线程池可以降低资源消耗(反复创建线程是一件很消耗资源的事,利用已创建的线程降低线程创建和销毁造成的消耗)、提供处理速度(当任务到达时,可以直接使用已有线程,不比等到线程创建完成才去执行。)、线程资源可管理性和通过控制系统的最大并发数,以保证系统高效且安全的运行。
Executors 实现了以下四种类型的 ThreadPoolExecutor:
线程池有7大核心参数,分别是
corePoolSize:核心线程数
maximumPoolSize:线程池中最大线程数
keepAliveTime:多余空闲线程数的存活时间,当前线程数大于corePoolSize,并且等待时间大于keepAliveTime,多于线程或被销毁直到剩下corePoolSize为止。
TimeUnit unit: keepAliveTime的单位。
workQueue:阻塞队列,被提交但未必执行的任务。
threadFactory:用于创建线程池中工作线程的线程工厂,一般用默认的。
handler:拒绝策略,当堵塞队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize)。
线程池中的执行流程:
(1)当线程数小于核心线程数的时候,使用核心线程数。
(2)如果核心线程数小于线程数,就将多余的线程放入任务队列(阻塞队列)中
(3)当任务队列(阻塞队列)满的时候,就启动最大线程数.
(4)当最大线程数也达到后,就将启动拒绝策略。
追问2:拒绝策略有哪些?
回答:有四种拒绝策略
1.ThreadPoolExecutor.AbortPolicy
线程池的默认拒绝策略为AbortPolicy,即丢弃任务并抛出RejectedExecutionException异常(即后面提交的请求不会放入队列也不会直接消费并抛出异常);
2.ThreadPoolExecutor.DiscardPolicy
丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃(也不会抛出任何异常,任务直接就丢弃了)。
3.ThreadPoolExecutor.DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务(丢弃掉了队列最前的任务,并不抛出异常,直接丢弃了)。
4.ThreadPoolExecutor.CallerRunsPolicy
由调用线程处理该任务(不会丢弃任务,最后所有的任务都执行了,并不会抛出异常)
追问3:线程池的参数如何确定呢?
回答:
一般需要确定核心线程数、最大线程数、任务队列和拒绝策略,这些需要根据实际的业务场景去设置,可以大致分为CPU密集型和IO密集型。
CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务。
IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数。
详细可以看一下《阿里调优手册》,需要的请私信我回复:阿里调优手册
追问4:Java中常见的阻塞队列有哪些?
ArrayBlockingQueue:是一个我们常用的典型的有界队列,其内部的实现是基于数组来实现的。
LinkedBlockingQueue 从它的名字我们可以知道,它是一个由链表实现的队列,这个队列的长度Integer.MAX_VALUE ,这个值是非常大的,几乎无法达到,对此我们可以认为这个队列基本属于一个无界队列(也又认为是有界队列)。此队列按照先进先出的顺序进行排序。
SynchronousQueue 是一个不存储任何元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。同时它也支持公平锁和非公平锁。
PriorityBlockingQueue是一个支持优先级排序的无界阻塞队列,可以通过自定义实现 compareTo() 方法来指定元素的排序规则,或者通过构造器参数 Comparator 来指定排序规则。但是需要注意插入队列的对象必须是可比较大小的,也就是 Comparable 的,否则会抛出 ClassCastException 异常。
DelayQueue 是一个实现PriorityBlockingQueue的延迟获取的无界队列。具有“延迟”的功能。
6.ThreaLocal知道吗?
回答:Java中每一个线程都有自己的专属本地变量, JDK 中提供的ThreadLocal
类,ThreadLocal
类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
1.ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
2.ThreadLocal底层是通过ThreadLocalmap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值。
3.ThreadLocal经典的应用场景就是连接管理(一个线程持有一个链接,该连接对象可以在不同给的方法之间进行线程传递,线程之间不共享同一个连接)
追问1:用它可能会带来什么问题?
回答:如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalmap,ThreadLocalmap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是:在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象。
ThreadLocalMap
中使用的 key 为ThreadLocal
的弱引用,而 value 是强引用。所以,如果ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。【摘自JavaGuide】
追问2:什么是强软弱虚引用?
回答:
(1)强引用是使用最普遍的引用。只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象
(2)软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。只有在内存不足的时候JVM才会回收该对象。
(3)只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
(4)虚引用也称为幻影引用,一个对象是都有虚引用的存在都不会对生存时间都构成影响,也无法通过虚引用来获取对一个对象的真实引用。唯一的用处:能在对象被GC时收到系统通知,JAVA中用PhantomReference来实现虚引用
虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
Java虚拟机高频问题
1.介绍一下Java运行时数据区域,并说一下每个部分都存哪些内容?
回答:Java的运行时区主要包含堆、方法区、虚拟机栈、程序计数器和本地方法栈,其中堆和方法区是所有线程所共有的。而且虚拟机栈、程序计数器和本地方法栈是线程所私有的。
堆:存放对象实例
方法区:用来存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
虚拟机栈:(生命周期与线程相同)Java中每个方法执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
程序计数器:保存下一条需要执行的字节码指令,是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都是依赖程序计数器。
本地方法栈:与虚拟机栈类似
追问1:程序计数器可以为空吗?
回答:可以为空,当执行的是本地方法时。
追问2:堆中又怎么细分的?
回答:堆中可以细分为新生代和老年代,其中新生代又分为Eden区,From Survivor和To Survivor区,比例是8:1:1。
追问3:哪些区域会造成OOM
回答:除了程序计数器不会产生OOM,其余的均可以产生OOM。
2.Java中对象的创建过程是什么样的?
回答:Java中对象的创建过程为5步
(1)当遇到new关键字的时候,首先坚持这个指令的参数是否可以在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化。
(2)在类加载检查后,接下来需要为新对象分配内存。
(3)需要将分配到的内存空间都初始化为零。
(4)需要对对象进行相关的设置,比如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的GC分代年龄等信息。
(5)执行<init>()方法。</init>
追问1:内存分配的策略有哪些?
回答:Java中的内存分配策略主要有两种,分别是指针碰撞和空闲列表。
指针碰撞:假设Java堆中的内存都是规整的,所有被使用过的放在一边,未使用过的放在一边,中间有一个指针作为分界,分配内存仅仅需要把这个指针向空闲空间方向移动一段即可。
空闲列表:如果Java堆中的内存不是规整的,已使用过的和空闲的交错,虚拟机就需要维护一个列表,记录哪些内存是可用的,在分配的时候找到足够大的一块内存进行分配。
追问2:对象头包含哪些?
回答:虚拟机中对象头包含两类信息,第一类是用于存储对象自身运动时数据、如哈希码、GC分代年龄、线程持有的锁、偏向线程ID、偏向时间戳。对象的另外一部分是类型指针,即对象指向它的类型元数据的指针。
追问3:对象的访问定位方法有几种,各有什么优缺点?
回答:Java虚拟机中对象的访问方式有①使用句柄和②直接指针两种。
- 句柄: 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
- 直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。
总结:使用句柄最大的好处就是reference中存储的是稳定句柄地址,在对象移动时只会改变句柄中的实例数据指针,而reference本身不需要被修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
3.如何判断对象已死?
回答:Java中判断对象死亡的方法有引用计数法和可达性分析。
引用计数法:对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
可达性分析:通过一系列的GC Roots的根对象作为 起始节点,从这些节点开始,根据引用关系向下搜索,如果某个对象到GC Roots间没有任何引用链相连。
追问1:GCroot可以是哪些?
回答:在Java中可以作为GC Roots的比较多,分别有
(1)在虚拟机栈中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
(2)在方法区中类静态属性引用的对象,比如Java类的引用类型静态变量。
(3)在方法区中常量引用的对象,比如字符串常量池里的引用。
(4)在本地方法栈中JNI引用的对象。
(5)Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象。
(6)所有被同步锁持有的对象。
追问2:被标志为GC的对象一定会被GC掉吗?
回答:不一定,还有逃脱的可能。真正宣告一个对象死亡至少经历两次标记的过程。
如果对象进行可达性分析后没有与GC Roots相连,那么这是第一次标记,之后会在进行一次筛选,筛选的条件是是否有必要执行finalize()方法。【详细可以看课本《深入理解Java虚拟机》】
4.垃圾回收算法有哪些?详细叙述一下。
回答:垃圾回收算法主要有三种,分别标记清除、标记整理和标记复制。
标记清除:算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记复制:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。
标记整理:首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
追问1:新生代和老年代一般使用什么算法?
回答:新生代一般使用标记复制和标记整理算法,老年代一般使用标记清除算法。
追问2:为什么新生代不使用标记清除算法?
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
5.垃圾回收器有哪些?
回答:垃圾回收器可以在新生代和老年代都有,在新生代有Serial、ParNew、Parallel Scavenge;老年代有CMS、Serial Old、Parallel Old;还有不区分年的G1算法。
追问1:CMS垃圾回收器的过程是什么样的?会带来什么问题?
回答:CMS回收过程可以分为4个步骤。
(1)初试标记:初试标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,但需要暂停所有其他的工作线程。
(2)并发标记: GC 和用户线程一起工作,执行GC Roots跟踪标记过程,不需要暂停工作线程。
(3)重新标记:在并发标记过程中用户线程继续运作,导致在垃圾回收过程中部分对象的状态发生了变化,未来确保这部分对象的状态的正确性,需要对其重新标记并暂停工作线程。
(4)并发清除:清理删除掉标记阶段判断的已经死亡的对象,这个过程用户线程和垃圾回收线程同时发生。
带来的问题:
(1)CMS收集器对处理器资源非常敏感。
(2)CMS无法处理“浮动垃圾”。
(3)CMS是基于标记-清除算***产生大量的空间碎片。
追问2:G1垃圾回收器的改进是什么?相比于CMS突出的地方是什么?
回答:G1垃圾回收器抛弃了分代的概念,将堆内存划分为大小固定的几个独立区域,并维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾回收时间,优先回收垃圾最多的区域。(G1算法是可控STW的一种算法,GC收集器和我们GC调优的目标就是尽可能的减少STW的时间和次数。)
G1突出的地方:
基于标记整理算法,不产生垃圾碎片。
可以精确的控制停顿时间,在不牺牲吞吐量的前提下实现短停顿垃圾回收。
追问3:现在jdk默认使用的是哪种垃圾回收器?
回答:(被问到过好几次)
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
6.内存分配策略是什么样的?
对象优先在Eden分配,如果说Eden内存空间不足,就会发生Minor GC/Young GC
大对象直接进入老年代,大对象:需要大量连续内存空间的Java对象,比如很长的字符串和大型数组,1、导致内存有空间,还是需要提前进行垃圾回收获取连续空间来放他们,2、会进行大量的内存复制。
-XX:PretenureSizeThreshold 参数 ,大于这个数量直接在老年代分配,缺省为0 ,表示绝不会直接分配在老年代。
长期存活的对象将进入老年代,默认15岁,-XX:MaxTenuringThreshold调整
动态对象年龄判定,为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
空间分配担保:新生代中有大量的对象存活,survivor空间不够,当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代.只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行Minor GC,否则FullGC。
追问1:内存溢出与内存泄漏的区别?
内存溢出:实实在在的内存空间不足导致;
内存泄漏:该释放的对象没有释放,多见于自己使用容器保存元素的情况下。
7.jvm调优了解过吗?常用的命令和工具有哪些?
回答:Linux中有top、vmstat、pidstat,jdk中的jstat、jstack、jps、jmap等。(建议详细去看看这些命令的区别和作用,都可能会被问到)
追问1:内存持续上升,如何排查?
回答: CPU100%那么一定有线程在占用系统资源, 找出哪个进程cpu高(top),该进程中的哪个线程cpu高(top -Hp) , 导出该线程的堆栈 (jstack) , 查找哪个方法(栈帧)消耗时间 (jstack) 工作线程占比高 | 垃圾回收线程占比高 。【详细可以到网络搜索,最好是自己清楚这个排查思路!】
(1)通过top找到占用率高的进程
(2)通过top -Hp pid找到占用CPU高的线程ID
(3)把线程ID转化为16进制,得到线程IDxx
(4)通过命令jstack 找到有问题的代码
追问2:jstack和jsp的区别是什么?
jstack:(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照。
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。
在代码中可以用java.lang.Thread类的getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码就完成jstack的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈。
jps : 列出当前机器上正在运行的虚拟机进程
-p :仅仅显示VM 标示,不显示jar,class, main参数等信息.
-m:输出主函数传入的参数. 下的hello 就是在执行程序时从命令行输入的参数
-l: 输出应用程序主类完整package名称或jar完整名称.
-v: 列出jvm参数, -Xms20m -Xmx50m是启动程序指定的jvm参数
8.虚拟机的加载机制是什么样的?
回答:JVM的类加载分为7个阶段:分别是加载、验证、准备、解析、初始化、使用和卸载。
加载:读取Class文件,并根据Class文件描述创建对象的过程。
验证:确保Class文件符合当前虚拟机的要求。
准备:在方法区中为类变量分配内存空间并设置类中变量的初始值。
解析:JVM会将常连池中的符合引用替换为直接引用。
初始化:执行类构造器<client>方法为类进行初始化。</client>
追问1:类加载有哪些?
回答:JVM提供了三种类加载器,分别启动类加载器(Bootstrap Classloader)、扩展类加载器(Extention Classloader)和应用类加载器(Application Classloader)
追问2:什么叫双亲委派机制?
回答:双亲委派机制是指一个类在收到类加载请求后不会尝试自己加载这个类,而且把这该类加载请求委派给其父类去完成,父类在接收到该加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。若父类加载器在接收到类加载请求后发现自己也无法加载该类,则父类会将该请求反馈给子类向下委派子类加载器加载该类,直到被加载成功,若找不到会曝出异常。
追问3:如何打破双亲委派机制?
回答:重写一个类继承ClassLoader,并重写loadClass方法。(Tomcat是不支持双亲委派机制的)
#Java开发##春招##实习##面经##求面经#