引言
1、什么是JVM?
JVM——Java虚拟机,它是Java实现平台无关性的基石。
Java程序运行的时候,编译器将Java文件编译成平台无关的Java字节码文件(.class),接下来对应平台JVM对字节码文件进行解释,翻译成对应平台匹配的机器指令并运行。
同时JVM也是一个跨语言的平台,和语言无关,只和class的文件格式关联,任何语言,只要能翻译成符合规范的字节码文件,都能被JVM运行。
内存管理
1、什么是JVM内存结构?
Java1.8 之后的内存结果图:
JVM将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区,其中方法区和堆是线程共享区,虚拟机栈、本地方法栈和程序计数器是线程私有的。
- 程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址;
- 虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;
- 本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法;
- 堆:Java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;
- 方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。jdk1.8之前使用永久代实现,在jdk1.8中使用元数据区实现,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中;
2、JDK1.6、1.7、1.8内存区域的变化?
JDK1.6、1.7/1.8内存区域发生了变化,主要体现在方法区的实现:
- JDK1.6使用永久代实现方法区:
- JDK1.7时发生了一些变化,将字符串常量池、静态变量,存放在堆上:
- 在JDK1.8时彻底干掉了永久代,而在直接内存中划出一块区域作为元空间,运行时常量池、类常量池都移动到元空间。
3、方法区常见面试题
什么是方法区?
- 方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。
- 存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。
- jdk1.8之前使用永久代实现,在jdk1.8中使用元数据区实现,原方法区被分成两部分:
- 加载的类信息;
- 运行时常量池,加载的类信息被保存在元数据区中,运行时常量池保存在堆中;
方法区和永久代以及元空间有什么关系?
永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式 ,并且永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现便成为元空间。
方法区常用参数有哪些?
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小。
-XX:PermSize=N //方法区 (永久代) 初始大小 -XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError //异常:java.lang.OutOfMemoryError: PermGen
JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。下面是一些常用参数:
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小) -XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
为什么使用元空间替代永久代作为方法区的实现?
- 整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
- 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
- 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。
什么是运行时常量池?
- Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量和符号引用的常量池表。常量池表会在类加载后存放到方法区的运行时常量池中。
- 字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量,符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用。
- 运行时常量池的功能类似于传统编程语言的符号表,尽管它包含了比典型符号表更广泛的数据。
- 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误
字符串常量池有什么作用?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String aa = "ab"; // 放在常量池中 String bb = "ab"; // 从常量池中查找 System.out.println(aa==bb);// true
JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区。JDK1.7 的时候,字符串常量池被从方法区拿到了堆中。
这里的字符串其实就是我们前面提到的字符串字面量。在声明一个字符串字面量时,如果字符串常量池中能够找到该字符串字面量,则直接返回该引用。如果找不到的话,则在常量池中创建该字符串字面量的对象并返回其引用。
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
4、讲一讲对象的创建过程?
在JVM中对象的创建,我们从一个new指令开始:
- 首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用;
- 检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就先执行相应的类加载过程;
- 类加载检查通过后,接下来虚拟机将为新生对象分配内存。
- 内存分配完成之后,虚拟机将分配到的内存空间(但不包括对象头)都初始化为零值。
- 接下来设置对象头,请求头里包含了对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
5、对象的内存分配方式?
内存分配有两种方式,指针碰撞(Bump The Pointer)、空闲列表(Free List)。
- 指针碰撞
- 堆内存规整,将堆分为空闲和使用过两部分,空闲的放一边,用过的放一边,中间放一个指针指向分界处,分配对象内存时就将指针向空闲部分移动相应大小。
- 空闲列表
- 堆内存不规整,需借助列表存放可用空间,分配对象内存时查看列表找到足够的空间分配给对象,并更新列表。
6、对象的访问定位?
流的访问方式主要有句柄访问和直接指针两种:
- 句柄访问
- 句柄访问就是说栈的局部变量表中,记录的对象的引用,然后在堆空间中开辟了一块空间,也就是句柄池。
- 句柄池中包含了对象实例数据指针(指向堆对象实例数据)+ 对象实例类型指针(指向方法区对象类型数据)
优点: 稳定,对象移动时只改变句柄中对象实例数据指针,而引用本身不变指向句柄;
- 直接指针
- 直接指针是栈的局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据。
优点: 访问速度快,一次定位。
7、对象并发安全问题
分配内存需考虑并发问题 :一个线程可能正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况,这就发生了抢占。
有两种可选方案来解决这个问题:
- CAS+失败重试保证更新原子性: 对分配内存空间动作进行同步处理
- 本地线程分配缓冲: 每个线程在Java堆中预先分配一小块内存(本地线程分配缓冲)哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,本地缓冲区用完了分配新的缓冲区才需要同步锁定。
8、对象的内存布局?
Java对象 = 对象头 + 实例数据 + 对齐填充
对象头主要由两部分组成:
- 第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称它为Mark Word,它是个动态的结构,随着对象状态变化。
- 第二部分是类型指针,指向对象的类元数据类型(即对象代表哪个类)。
实例数据用来存储对象真正的有效信息,也就是我们在程序代码里所定义的各种类型的字段内容,无论是从父类继承的,还是自己定义的。
对齐填充不是必须的,没有特别含义,仅仅起着占位符的作用。
9、内存溢出和内存泄漏是什么意思?
内存泄露:申请的内存空间没有被正确释放,导致内存被白白占用。
内存溢出:申请的内存超过了可用内存,内存不够了。
两者关系:内存泄露可能会导致内存溢出。
10、什么情况下会发生内存溢出?
在JVM的几个内存区域中,除了程序计数器外,其他几个运行时区域都有发生内存溢出(OOM)异常的可能,重点关注堆和栈。
Java堆溢出
- 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()); } } }
栈内存溢出
- 栈帧过多导致栈内存溢出,方法递归调用时,没有设置正确的递归结束条件,导致无限递归。
- 栈帧过大导致栈内存溢出。
- 无限递归,第三方代码中出现两个类中循环引用的问题,导致无限递归。