1、请你说一说JAVA中的五种引用?JAVA中都有哪些引用类型?
强引用
- 只要沿着GC ROOT的引用链能够找到该对象,就不会被垃圾回收;只有当GC ROOT都不引用该对象的时候,才会回收强引用对象。
- 换句话说就是,只要强引用存在,JVM垃圾回收器就永远不会回收被引用的对象,内存不足的时候,JVM会抛出
OutOfMemoryError
。
比如:new
一个对象Student,将对象Student通过=
(赋值运算符),赋值给变量stu,即变量stu就强引用了对象Student。
// 只要 stu 指向 Student 对象,那它就是强引用,永远都不会被 JVM 回收
Student stu = new Student();
// 如果将 stu 置为 null,可以切断 GC Root 引用链,这样 stu 就会被 JVM 回收
stu = null;
软引用
- 如果仅有软引用引用某个对象的时候,在垃圾回收后,内存仍不足的时候会再次触发垃圾回收,回收软引用对象。即,在内存足够的时候,JVM不会回收软引用对象,但当内存不足的时候,软引用对象就会被回收,所以软引用对象通常用来描述一些非必要但仍有必要的对象。
// 不直接通过 list 引用 byte[]
// list -----> SoftReference -----> byte[] 添加了一层软引用:
List<SoftReference<byte[]>> list = new ArrayList<>();
弱引用
- 弱引用是较软引用更低一级的引用,如果仅有弱引用引用某个对象,在垃圾回收的时候,无论内存是否充足,都会回收弱引用所引用的对象。
List<WeakReference<byte[]>> list = new ArrayList<>();
虚引用
- 虚引用必须配合引用队列使用,主要配合ByteBuffer使用,引用对象被回收的时候,会将虚引用入队,然后调用虚引用相关方法(
Unsafe.freeMemory()
)释放直接内存。
终结器引用
- 所有的类都继承自Object类,Object类有一个
finalize()
方法。当某个对象不再被其他的对象所引用的时候,会先将终结器引用对象放入引用队列中(被引用对象暂时没有被回收),然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize()
方法。调用以后,该对象就可以被垃圾回收了。
引用队列
在回收软引用、弱引用所指向的对象的时候,软引用本身不会被清理。如果想要清理引用,需要使借助引用队列;
ReferenceQueue
,当一个引用(软引用、弱引用)关联到了一个引用队列后,当这个引用所引用的对象要被垃圾回收的时候,就会将它加入到所关联的引用队列中,所以判断一个引用对象是否已经被回收的一个现象就是,这个对象的引用是否被加入到了它所关联的引用队列。
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
说到底,引用队列就是一个对引用的回收机制,当软引用或者弱引用所包装的对象为null
或者被回收的时候,这个引用也就不再具有价值,引用队列就是清除掉这部分引用的一个回收机制。
- 软引用和弱引用可以配合引用队列(也可以不配合):在弱引用和虚引用所引用的对象被回收以后,会将这些引用放入引用队列中,方便一起回收这些软/弱引用对象。
- 虚引用和终结器引用必须配合引用队列:虚引用和终结器引用在使用的时候会关联一个引用队列。
2、JVM是如何判断对象可以被回收的?判断对象是否可以被回收的两种算法?
2.1 引用计数数法(JAVA不采用这种方法)
- 如果一个对象被其他变量所引用,则让该对象的引用次数
+1
.,如果该对象被引用两次则其引用计数为2,依次类推。 - 某个变量不再引用该对象,则让该对象的引用计数
-1
,当该对象的引用奇数变为0的时候,则表示该对象没有被其他变量所引用,这时候该对象就可以被作为垃圾进行回收。
引用计数法弊端:循环引用的时候,两个对象的引用计数都为1,导致两个对象都无法被释放回收。最终就会造成内存泄漏。
2.2、可达性分析算法
可达性分析算法就是JVM中判断对象是否是垃圾的算法:该算法首先要确定GC Root(根对象,就是肯定不会被当成垃圾回收的对象)。
在垃圾回收之前,JVM会先对堆中的所有对象进行扫描,判断每一个对象是否能被GC ROOT直接或者间接的引用,如果能被根对象直接或者间接引用则表示该对象不能被垃圾回收,反之则表示该对象可以被回收。
- JVM中的垃圾回收器通过可达性分析来探索所有存活的对象。
- 扫描堆中的对象,看能否沿着GC ROOT为起点的引用链找到该对象,如果找不到,则表示可以回收。否则就不可以回收。
- 可以作为GC ROOT的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中Native方法引用的对象。
3、JVM垃圾回收算法有哪些?
3.1、标记-清除算法
- 标记清除算法:顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识,清除相应的内容,给堆内存腾出相应空间。
- 这里的腾出内存空间并不是将内存空间的字节清0,而是记录下这段内容的起始结束地址,下次分配地址的时候,会直接覆盖这段内容。
- 它的主要不足有两个:
- 一个是效率问题,标记和清除两个过程的效率都不高。
- 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片,太多可能可能会导致以后再程序运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
3.2、标记-整理算法
- 标记-整理算法:会将不被GC Root引用的对象回收,清除其占用的内存空间。然后整理剩余的对象内存空间,可以有效避免因内存碎片而导致的问题,但是牵扯到对象的整体移动,需要消耗一定的时间,所以效率较低。
3.3、复制算法
- 复制算法:将内存分为等大小的两个区域,FROM和TO(TO中为空)。先将GC ROOT引用的对象从FROM放入TO中,再回收不被GC ROOT引用的对象。然后交换FROM和TO。
这样也可以避免内存碎片问题,但是会占用双倍的内存空间。流程如下:
然后清除FROM中的需要回收的对象:
最后交换FROM和TO的位置:(FROM换成TO,TO换成FROM):
4、分代收集算法
- 分代收集算法:这种算法是把JAVA堆分为新生代和老年代,新生代默认的空间占比总空间的
1/3
,老年代的默认占比是2/3
,根据不同年代的特点采用最适当的收集算法:- 在新生代中,每次垃圾收集的时候都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
- 新生代有三个分区:伊甸园、TO幸存区、FROM幸存区,它们默认的占比是:
8:1:1
。
- 新生代有三个分区:伊甸园、TO幸存区、FROM幸存区,它们默认的占比是:
- 在老年代中,因为对象存活率高,没有额外空间对它进行分配担保,就必须使用标记-清除或者标记-整理算法来进行回收。
- 在新生代中,每次垃圾收集的时候都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
下面来逐步介绍一下分代收集算法的流程:
- 长时间使用的对象放在老年代中(长时间回收一次,回收花费时间久),用完就可丢弃的对象放在新生代中(频繁需要回收,回收速度相对较快):
新创建的对象都被放在了新生代的伊甸园中:
- 当伊甸园中的内存不足的时候,就会进行一次垃圾回收,这时的回收叫做Minor GC(Young GC):
Minor GC会将伊甸园与幸存区FROM仍需要存活的对象先复制到幸存区TO中,并让其寿命加1,再交换FROM和TO。
伊甸园中不需要存活的对象将其清除:
交换FROM和TO:
- 同理:继续向伊甸园中新增对象,如果满了,则进行第二次的MInor GC:
流程相同,仍需要存活的对象寿命+1
;(下图中FROM中寿命为1的对象是从伊甸园复制过来的,而不是原来的幸存区FROM中的寿命为1的对象,这里只是静态图片不好展示,只能用文字描述了):
再次创建对象,若新生代的伊甸园又满了,则会再次触发Minor GC(会触发Stop The World,暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中,回收以后交换两个幸存区,并让幸存区中的对象寿命加1!
- 如果幸存区中的对象的寿命超过某个阙值(最大为15,4bit),就会被放入老年代中:
- 如果新生代老年代中的内存都满了,就会先触发Minor GC,再触发FULL GC,扫描新生代和老年代中所有不再使用的对象并回收:
分代收集算法流程小结:
- 新创建的对象首先会被分配在伊甸园区域。
- 新生代空间不足的时候,触发Minor GC,伊甸园和FROM幸存区需要存活的对象会被COPY到TO幸存区中,存活的对象寿命
+1
,并且交换FROM和TO。 - MInor GC会引发STOP THE WORLD:暂停其他用户的线程,等待垃圾回收结束后,用户线程才可以恢复执行。
- 当对象寿命超过阙值
15
的时候,会晋升至老年代。 - 如果是新生代、老年代中的内存都满了,就会先触发Minor GC,在触发FULL GC,扫描新生代和老年代中所有不再使用的对象并回收。
4、JVM垃圾回收机制有哪些?
在触发GC的时候,具体如下,这里只说常见的Young GC(Minor GC)和FULL GC。
4.1、Minor GC
- 当新生代中的伊甸园没有足够空间进行分配的时候会触发Minor GC。
- Minor GC其实就是一次复制垃圾回收算法将伊甸园和幸存区FROM仍需要存活的对象先复制到幸存区TO中,并让其寿命+1,再交换FROM和TO。这个时候伊甸园中不需要存活的对象就是将其清除。
4.2、FULL GC
- 如果新生代和老年代中的内存都满了,就会先触发Young GC,再触发FULL GC,扫描新生代和老年代中所有不再使用的对象并回收。
System.gc()
默认也是触发Full FC。- FULL GC使用标记-清理或者标记-整理算法来清除。
FULL GC为什么那么慢?
- 因为FULL GC要回收新生代、老年代、元数据区/永久代(方法区)
- 新生代复制算法比较快,老年代使用的
标记-清除
或者标记-整理
算法回收速度慢,且老年代本身内存大小久大于新生代。 - 在一些垃圾回收器中,FULL GC回收之前,会先触发一次Minor GC,然后再进行FULL GC。
5、说一下JVM有哪些垃圾回收器?
相关概念:
- 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
- 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序再继续运行,而垃圾收集程序运行在另一个CPU上。
- 吞吐量:即CPU用户运行用户代码的时间与CPU总消耗的时间的比值(
吞吐量 = 运行用户diamagnetic的时间/(运行用户时间+垃圾收集时间)
),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉一分钟,那么吞吐量就是99%
。
垃圾回收器的分类:
- 串行:单线程垃圾收集
- 吞吐量优先:多线程垃圾收集,单位时间内,让STW(stop the world,停掉其他工作线程)时间最短。
- 响应时间优先:多线程垃圾收集,尽可能让单次STW时间变短(尽量不影响其他线程运行)。
7种垃圾回收器
如图:
主要来了解下下面两个垃圾回收器:(7个回收器,就算背也难记住,先掌握两个再说!)
- CMS收集器:老年代并行收集器
- 以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- CMS收集器基于标记-清除算法实现,会产生内存碎片。
- G1收集器:JAVA堆并行收集器
- G1收集器是JDK1.7提供的一个新收集器,G1收集器基于标记-整理算法实现,也就是说不会产生内存碎片。
- 此外,G1收集器不同于之前的收集器的一个重要特点就是:G1回收的范围是整个JAVA堆(包括新生代,老年代),而其他六种收集器回收的范围仅限于新生代或者老年代。
CMS和G1都是属于响应优先的垃圾回收器:尽可能让单次STW时间变短(尽量不影响其他线程运行)。
7、分析题:a++、++a
案例代码如下:
/**
* 从字节码角度分析 a++ 相关题目
*/
public class Demo3_2 {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);// 11
System.out.println(b);// 34
}
}
上面的a、b结果是怎么样得来的呢?
分析:
iinc
指令是直接在局部变量桶位(slot)上进行运算。iload
指令是用于读取变量a++
和++a
的区别是先执行iload
还是先执行iinc
。a++
是先iload
再iinc
,++a
相反。
bipush 10
操作a=10
放入操作数栈:
istore 1
操作,是把操作数栈中的10弹出,放入到局部变量表的槽位1中:
接下来执行a++
操作,我们上边提前说明了,a++
是先执行iload
读取,再执行iinc
加1
iload 1
是将变量a=10
,读取到操作数栈stack中:
-
执行
iinc
指令,再局部变量表中对a进行+1操作,这时候a为11: -
下面再执行
++a
操作,先iinc
再iload
:
下面进行a++ + ++a
操作,再操作数栈中进行相加,得到结果22,这时候第一个加法完成:
下面执行第二个加法(a++ + ++a) + a--
操作:
-
a--
先执行iload
命令,在执行inc 1,-1
命令,如下图,先将局部变量表中的12读取到操作数栈: -
接下来执行
inc 1,-1
命令,再局部变量表中进行-1操作,此时局部变量表中的值由12减为11: -
再操作数栈中,进行第二次加法运算,得到结果为34:
-
最后将操作数栈中的数据弹出到局部变量表中,赋值2号槽位b=34:
因此程序运行结果得到:a为11,b为34。
8、HotSpot为什么要分为新生代和老年代?
HotSpot根据对象存活周期不同将内存划分为几块,一般是把JAVA堆分为新生代和老年代,这样久可以根据各个年代的特点采用最适当的收集算法。
- 在新生代中,每次垃圾收集的时候都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出善良存活对象的复制成本就可以完成收集。
- 而在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记-整理算法来进行回收。
其中新生代又分为伊甸园、幸存区FROM、幸存区TO。
9、说一下JVM的几个主要组成部分?
主要由4个部分组成:
- 运行时数据区域:就算通常我们所说的JVM内存。
- 执行引擎:执行
class
字节码文件中的指令。 - 类加载系统:根据给定的全限定类名(如:
java.lang.Object
)来装载.class
文件到运行时数据区中的方法区中。 - 本地接口:与本地方法库交互,是其他编程语言交互的接口。
10、JAVA会存在内存泄漏嘛?请简单描述
JAVA中会存在内存泄漏
JAVA中虽然存在GC垃圾回收机制,及时回收不再被使用的对象。但是依然存在内存泄漏的情况!
JAVA导致内存泄漏的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是JAVA中内存泄漏的发生场景。
11、JAVA中的异常体系
JAVA中所有异常都是来自顶级父类Throwable。
Throwable下有两个子类:Exception和Error,用于表示程序出现了不正常的情况。区别在于:
- Error是程序错误,通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用和程序会被终止,仅靠应用程序本身无法恢复;
- Exception是程序异常,是可以在应用程序中进行捕获并处理的,是一种设计或者实现问题,也就是说,它表示如果程序运行正常,从不会发生的情况。
- Exception又分为两个部分:运行时异常(RuntimeException)和检查异常(CheckException)。
- RunTimeException通常发生在程序运行过程中,会导致当前程序的线程执行失败(不会影响其他线程,例如空指针异常)。
- CheckedException通常发生在程序编译的过程中,会导致编译不通过(也可叫做编译时异常)。
12、发生Young GC的时候需要扫描老年代的对象嘛?
不会!
在分代收集中,新生代的规模一般比老年代都要小许多,新生代的收集也比老年代要频繁许多,如果回收新生代的时候也不得不同时扫描老年代的话,那么Young GC的效率可能会下降不少,显然不可能扫描老年代的。
13、JVM如何确定一个类?(JVM如何判断类相同)
在JAVA中,一个类的全名(包名+类名
)作为其标识,但在JVM中,一个类用其全名+类加载器
作为唯一标识,不同类加载器加载的类置于不同的命名空间中,这叫做类加载器隔离。