JVM
JVM内存模式和Java内存模型的关系:JVM内存模型是对Java内存模型的实现。
全面理解Java内存模型
1、 了解JVM内存区域嘛?
JDK1.8的JVM内存模型

线程共享
线程私有
- 虚拟栈
- 由一个个栈帧组成
- 栈帧中都有:局部变量表,操作数栈,动态链接,方法出口信息
- 本地方法栈
- 程序计数器
- 字节解释器通过程序计数器读取指令,实现代码流程控制
- 多线程情况下,程序计数器用于记录当前线程执行的位置
2、说说JDK1.6和JDK1.8对JVM的实现差别
方法区:
- JDK1.8摒弃了方法区,改成在直接内存中使用元空间
- 原方法区的作用:存储被虚拟机加载的类信息,常量,静态变量,即使编译器编译后的代码等数据
- 方法区,又被称为永久带,非堆(Java虚拟机规范中有方法区的概念,而hotspot使用永久带的方法实现了方法区这个概念,关系好比接口和实现类的关系)
常量池:
- JDK1.8 hotspot移除了永久代⽤元空间**(Metaspace)取⽽代之,** 这时候字符串常量池还在堆**,** 运⾏时常量池还在⽅法区**,** 只不过⽅法区的实现从永久代变成了元空间**(Metaspace)**
- 运行时常量池中存储的是对象的引用,真正的对象还是存储在堆中
3、Java对象创建的过程
-
类加载检查。检查类是否被加载,解析和初始化过
-
分配内存。对象所需的内存大小在类加载完成后便可确定。
- 指针碰撞(内存规整,挪动指针即可)
- 空闲列表(内存不规整,需要遍历寻找适合大小的内存块)
-
初始化0值。区分于构造函数,初始化0值指的是内存方面的初始化
-
设置对象头。对象头中包含哈希码等等
-
执行init方法。根据构造函数初始化
(分配内存会有线程安全问题,但为了在面试时不把自己坑了,还是不说了,但其中涉及的CAS了解一下)
CAS(比较并交换):乐观锁的一种实现
4、Java对象定位的方法
- 句柄
- 栈中的引用存储句柄地址,句柄存储对象实例地址和对象类型地址
- 直接指针
- 栈中引用存储对象实例地址,对象实例中还存储对象类型的地址
5、如何判读对象是否死亡
- 引用计数法
- 可达性分析算法
- GC Roots作为根节点,如果GC Roots无法到达该对象代表的节点,则代表对象可以被销毁
6、如何判断⼀个类是⽆⽤的类?
- 类所有实例被回收
- ClassLoader被回收
- Class没有被引用
7、GC算法及特点
标记-清除算法

空间效率:标记和清除的效率都不高
空间占用率:标记清除之后会产生大量不连续内存碎片,分配较大内存对象的时候无法得到足够的连续内存而不得不提前触发一次GC
复制算法

空间利用率:内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低,但不用考虑内存碎片的问题
标记-压缩算法

比标记-清除多了一步压缩的操作
分代收集算法
“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据对象的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收
过程:伊甸园区==>幸存者区==>老年区
8、什么是双亲委派机制?
工作流程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类
图解

源码
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 判断是否被缓存
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果存在父类加载器,就委托给父类加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果是BootStrap加载器,就调用本地方法,调用C++的方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 父类加载失败,尝试自己加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);// 这个方法会被每个加载器类重写
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
好处
- 系统类防止内存中出现多份同样的字节码(当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类)
- 保证Java程序安全稳定运行,不会被自定义类注入
9、看你的项目用到了SPI,你知道SPI和双亲委派机制有关吧?为什么说SPI破坏了双亲委派机制?
关系:SPI破坏了双亲委派机制
原因:双亲委派机制的==可见性==决定了子加载器可以查看父加载器的所有类,父加载器却无法查看子加载器的所有类,而我们常用的如JDBC,它的接口java.sql.DriverManager
是由启动类加载器加载的,而它的实现类则是由第三方提供,由应用加载器加载。
==最终矛盾出现在,要在BootstrapClassLoader加载的类里,调用AppClassLoader去加载实现类==
核心是通过SPI实现,所以我们前面才说SPI破坏了双亲委派机制
源码分析(DriverManager==>getConnection()==>loadInitialDrivers())
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
那SPI是如何解决的呢?
- jdk提供了两种方式,Thread.currentThread().getContextClassLoader()和ClassLoader.getSystemClassLoader()一般都指向AppClassLoader,他们能加载classpath中的类
- SPI则用Thread.currentThread().getContextClassLoader()来加载实现类,实现在核心包里的基础类调用用户代码
逻辑:双亲委派机制的特点(金字塔型)==>造成的矛盾==>SPI解决==>(源码)==>如何解决
10、垃圾收集器
(感觉应该不会问到,奶一波好吧)
- Serial收集器:单线程,新⽣代采⽤复制算法,⽼年代采⽤标记**-**整理算法;
- ParNew收集器:ParNew 收集器其实就是 Serial 收集器的多线程版本本,除了使⽤多线程进⾏垃圾收集外,其余一样;
- Parallel Scavenge 收集器:关注点是吞吐量(所谓吞吐量就是 CPU 中⽤于运⾏⽤户代码的时间与 CPU 总消耗时间的⽐值,JDK1.8默认收集器)。