类加载的流程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为连接。
如果想要详细了解类加载的过程,可以参考我的另一篇文章——JVM面试题详解系列——类加载过程详解。
双亲委派机制
当一个类收到类加载请求时,它首先检查这个类有没有被加载,如果已经被加载就直接返回,如果被有,它首先不会自己家在这个类,而是会把类加载请求传递给父类加载器,父类加载器重复上面的流程,所以所有的类加载请求最终都会被传递给启动类加载器,如果启动类加载器可以加载这个类,就加载这个类并返回,如果不能加载再将类加载请求传递给子类加载器。
总结起来就是:自底向上的检查类是否被加载,自上向下的尝试加载类。
如果想要详细了解类加载的过程,可以参考我的另一篇文章——JVM面试题详解系列——类加载器和双亲委派模型详解。
判定对象回收的两种方法,引用计数器和可达性分析
如果想要详细了解这个问题,可以参考我的另一篇文章——VM面试题详解系列——垃圾回收详解。
常见的垃圾回收算法
-
标记—清除算法:首先标记出所有需要回收的对象,然后再回收所有需要回收的对像。缺点:效率、内存碎片。
-
标记—复制算法:将内存空间分为大小相等的两块,每次只使用其中一块,进行垃圾回收时,将所有存活的对象复制到另一块内存空间中,然后把已使用的这一块内存空间一次性清理掉。缺点:内存空间利用率低。
-
标记—整理算法:需要进行垃圾回收时,将所有存活的对象都向一端移动,然后直接清理掉端边界以外的对象。缺点:仍需移动局部对象。
-
分代收集:根据堆内存中对象存活时间的特点将堆内存分为新生代和老年代,新生代对象存活率比较低(需要复制的对象较少) ,采用标记—复制算法;老年代对象存活率高,采用标记—清除算法或者标记—整理算法。
如果想要详细了解这个问题,可以参考我的另一篇文章——JVM面试题详解系列——垃圾收集算法详解。
介绍一下指针碰撞和空闲列表
指针碰撞
-
使用场合:堆内存规整(即没有内存碎片)的情况下。
-
实现原理:将用过的内存都整合到一边,没有用过的内存放到另一边,中间有一个分界指针,当需要为新对象分配内存空间时,只需要将分界指针向没有用过的内存一侧移动对象内存大小位置即可。
空闲列表
-
使用场合:堆内存不规整的情况下。
-
实现原理:虚拟机会维护一个列表,该列表记录了那些内存是可用的,当需要为新对象分配内存空间时,只需要在列表中找一块足够大小的内存分配给对象实例,然后更新列表记录。
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器垃圾采用的垃圾收集算法,垃圾收集相关内容我会在后续文章详细介绍。
如果想要详细了解这个问题,可以参考我的另一篇文章——JVM面试题详解系列——Java 对象的创建过程。
死锁产生的四个必要条件
- 互斥条件:该资源任意一个时刻只能被一个线程占用;
- 请求与保持条件:线程因请求某个资源而阻塞时对已占有的资源保持不放;
- 不剥夺条件:线程已占有的资源在未使用完的情况下不能被其他线程剥夺,只能自己使用完后释放;
- 循环等待条件:若干线程之间形成一种头尾相接循环等待资源的关系。
如何预防死锁
破坏死锁的产生的必要条件即可:
破坏互斥条件 : 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件 : 一次性申请所有的资源。
破坏不剥夺条件 : 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件 : 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放,破坏循环等待条件。
锁排序法: 指定获取锁的顺序,比如某个线程只有获得A锁和B锁,才能对某资源进行操作,在多线程条件下,如何避免死锁? 通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。这通常被认为是解决死锁很好的一种方法。
如果想要详细了解这个问题,可以参考我的另一篇文章——Java并发常见面试题(二)
volatile 关键字的作用
- 保证变量可见性(可见性)
- 禁止指令重排序(有序性)
如何保证变量可见性?
在 Java 中,volatile 关键字可以保证变量的可见性,如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取(不使用缓存中的数据)。
volatile 关键字其实并非是 Java 语言特有的,在 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。如果我们将一个变量使用 volatile 修饰,这就指示 编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
如何禁止指令重排序?
在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令
执行的有序性。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障变量的可见性。
通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障的另外一个作用就是强制刷出各种CPU的缓存数据,因此在任何CPU上的线程都能读取
到这些数据的最新值。
如果想要详细了解这个问题,可以参考这篇文章——volatile关键字是如何禁止重排序的。
如果想要详细了解这个问题,可以参考这篇文章——Volatile如何保证有序性(禁止指令重排)。
实际案例——双重检测锁实现单例模式
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
总结:单例模式顾名思义就是单例类只能有一个实例,且该类需自行创建这个实例,并对其他的类提供调用这一实例的方法。
public class SingletonByDoubleCheckLock {
/**
* 私有实例,初始化的时候不加载(延迟加载/懒加载),使用volatile关键字,禁止指令重排序
*/
private volatile static SingletonByDoubleCheckLock singleton;
/**
* 私有构造
*/
private SingletonByDoubleCheckLock(){}
/**
* 唯一公开获取实例的方法
*
* @return
*/
public static SingletonByDoubleCheckLock getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(SingletonByDoubleCheckLock.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new SingletonByDoubleCheckLock();
}
}
}
return singleton;
}
}
关于单例模式以及为什么这样实现的详解解读,请移步观看我的另一篇博客——Java常见设计模式总结——单例模式