1. 什么是JVM?
就是Java虚拟机,是Java实现平台无关性的基石。
Java程序运行时,编译器将Java文件编译成平台无关的Java字节码文件(.class),然后用对应平台的JVM对字节码进行解释,转换成对应平台的机器指令。
2. 能说一下JVM的内存区域吗?
JVM内存区域分为堆和栈。详细可以分为线程私有区和线程共享区,其中方法区和堆是线程共享区,虚拟机栈、本地方法栈和程序计数器是线程隔离的数据区。
程序计数器:它是一个行号指示器,用来记录当前线程执行的位置,便于线程上下文切换时恢复到正确的位置。
Java虚拟机栈:线程在执行Java方法时创建的栈帧,用于存储局部变量、方法参数、操作数栈和动态链接等。
本地方法栈:与虚拟机栈类似,不过只为本地方法服务。
Java堆:用于存放对象实例和数组,所有线程共享。在JVM启动时创建,大小可以在运行时进行伸缩。使用垃圾回收器进行垃圾回收,所以也被称为GC堆。
方法区:也被称为永久代,用于存储已加载的类信息、常量、静态变量、编译后的代码等。JDK8之后被元空间替代,元空间使用本地内存来存储类的元数据。
3. JDK1.6、1.7和1.8内存区域的变化
JDK1.6使用永久代实现方法区,1.7开始将字符串常量池、静态变量存放在堆上,1.8之后元空间代替永久代,运行时常量池、类常量池都移动到元空间。
4. 为什么使用元空间替代永久代作为方法区的实现?
1)客观上使用永久代更容易造成内存溢出。
2)主观上JRockit跟HotSpot对方法区的实现差异太大。
5. 对象创建的过程了解吗?
1)首先先执行new指令,检查指令参数能否在常量池中定位到一个类的符号引用;
2)如果这个符号引用代表的类还没有被加载,那么就先进行类加载;
3)类加载完成后,虚拟机给新生对象分配内存(通过指针碰撞或空闲列表实现);
4)内存分配完内存后,虚拟机会对内存进行初始化,一般是初始化为0值;
5)设置对象头,对象头包括对象的元数据信息(对象所属的类、哈希码等);
6)执行构造函数。
6. 什么是指针碰撞?什么是空闲列表?
指针碰撞和空闲列表都是JVM内存分配的方式。
指针碰撞:假设Java堆中的内存是绝对规整的,一边是被使用过的内存,另一边是未被使用的内存,中间用一个指针分隔,那么分配内存时就只需要把指针向未分配的方向挪动与对象大小相等的距离即可。
空闲列表:假设Java堆中的内存不是绝对规整的,虚拟机需要维护一个列表(链表),记录哪些内存是可用的,然后在列表上找出一块足够大的区域给对象,并更新列表上的空闲记录。
7. JVM里new对象时,堆会发生抢占吗?JVM怎么设计保证线程安全的?
因为在JVM中,堆的内存是线程共享的,所以在多线程下可能会发生抢占现象。
解决:
1)原子操作:JVM使用CAS等原子操作来确保对共享数据的原子性访问。CAS会比较并交换共享变量的值,如果期望值与当前值相等,则更新为新值,否则重试。
2)本地线程分配缓冲(TLAB):JVM为每个线程预先分配一小块内存空间作为本地线程分配缓冲。线程在分配内存时,先尝试在自己的TLAB中分配,不够再竞争堆内存。
8. 能说一下对象的内存布局吗?
对象头、实例数据、对齐填充。
9. 什么是内存溢出、内存泄露?
内存泄露是指申请的内存空间没有被正确释放,导致内存被白白占用。
内存溢出是指申请的内存超过了可用内存。
关系:内存泄露可能会导致内存溢出。
10. 能手写出内存溢出的例子吗?
在JVM区域中,除了程序计数器外,其他运行时区域都可能会发生内存溢出(OOM)的可能。
Java堆溢出:不断创建不可被回收的对象(比如静态对象),就会导致内存溢出。
/**
* VM参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
虚拟机栈.outOfMemoryError
虚拟机栈内存大小是固定的,给他不断创建线程,因为操作系统给每个进程分配的内存是有限的,所以到最后会发生溢出异常。
代码太长了,不写了。
11. 内存泄露可能是由哪些原因导致的?
静态集合类:生命周期和JVM一样,所以它引用的对象不能被释放;
单例模式:生命周期和JVM一样;
连接(IO/数据)未释放:创建的连接使用完了需要close关闭,否则不会被回收;
变量作用域过大:定义作用域大于使用范围,可能会内存泄露;
hash值发生改变:如果hash值改变,则修改之后无法找到存入的对象,也就无法删除;
ThreadLocal使用不当:使用完毕后用remove进行清除。
12. 如何判断对象依然存活?
引用计数法
可达性分析法
13. 对象有哪几种引用?
强引用:垃圾回收器不会回收。
软引用:当内存不足时,垃圾回收器可能会回收。用于实现缓存,可以避免内存溢出。
弱引用:无论内存是否充足,都会回收。通常用于敏感数据缓存,避免内存泄露。
虚引用:追踪对象被垃圾回收的状态。
以上引用强度依次逐渐减弱。
14. finalize()方法了解吗?有什么作用?
如果对象在进行可达性分析的时候发现没有与GC Roots相连接的引用链,那么会第一次被标记,随后看这个对象是否有必要执行finalize()方法。如果对象在finalize中成功拯救自己,则主要重新与引用链上的任何一个对象建立连接关联即可,这样第二次就不会标记。否则就会被回收。
15. Java堆的内存分区了解吗?
Java堆的内存分区是按照垃圾收集的角度进行划分的,主要分为新生代和老年代。
新生代:用于存放存活时间较短的对象,它分为Eden区、Survivor区(通常是From区+To区)。当对象被创建时,首先被分到Eden区,在垃圾回收时,存活的数据会到Survivor区。经过多次垃圾回收后仍然存活的对象会被晋升到老年代。
老年代:存放存活时间较长的对象。它被垃圾回收的频率相对较低。
16. 垃圾收集算法了解吗?
· 1)标记-清除算法:先标记需要回收的对象,然后清除标记对象。
缺点:1° 执行效率不稳定,取决于标记和清除的执行效率。
2° 内存空间碎片化。
2)标记-复制算法:将容量分为两部分,每次只使用其中一块。当这块用完了,就将还存活的对象复制到另一块上面,再把已使用过的内存空间清理掉。(新生代)
缺点:一部分空间没有使用,存在空间浪费。
3)标记-整理算法:将所有存活对象都向内存空间一端移动,然后清理掉边界以外的内存。(老年代)
17. 新生代的区域划分?
主要是用标记-复制法。分为一块大的Eden和两块小的Survivor。发生垃圾回收时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor上,然后直接清理掉Eden和已用过的那块Survivor空间。Eden和Survivor的大小比例是8:1:1。