JVM
1、什么是 JVM
内存结构?
JVM
将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区;
- 程序计数器:线程私有的,是一块很小的内存空间,作为当前线程执行的字节码的行号指示器,用来存储下一条将要执行的字节码指令的地址;
- 虚拟机栈:线程私有的,每个线程在创建时都会创建一个虚拟机栈,虚拟机栈由许多栈帧组成,每个方法被执行的时候,JVM 都会同步创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息。方法执行时,栈帧入栈,执行完成后,栈帧出栈。当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;当创建线程时没有足够的栈空间,会抛出 OOM 异常。
- 本地方法栈:线程私有的,保存的是native方法的信息,当一个
JVM
创建的线程调用native方法后,JVM
不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法; - 堆:java 堆是所有线程共享的一块 内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;
- 方法区:是各个线程共享的内存区域,用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中;元数据区使用的是本地内存。
- 为什么需要程序计数器呢?
- 通过修改程序计数器的值来确定下一条应该执行什么样子的指令。
- 为什么程序计数器是线程私有的?
- 为了准确记录各个线程正在执行的当前字节码指令的地址,采用线程独立的设计可以独立计算,不会出现相互干扰的情况。
2、什么是 Java 内存模型?
Java 内存模型规定了变量的访问规则,保证了操作的原子性、可见性、有序性。
3、heap 和 stack 有什么区别?
先介绍下堆和栈分别是什么,有什么作用。
虚拟机栈:线程私有的,每个线程在创建时都会创建一个虚拟机栈,虚拟机栈由许多栈帧组成,每个方法被执 行的时候,JVM 都会同步创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息。方法执行时,栈帧入栈,执行完成后,栈帧出栈。当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;当创建线程时没有足够的栈空间,会抛出 OOM 异常。
堆:java 堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;
- 栈线程私有,堆线程共享
- 存储的内容不同,栈存储的是栈帧,堆存储的是对象。
- 对象的申请方式不同,栈是 jvm 自动生成,堆需要程序主动申请
- 申请的效率不一样,栈是由系统自动创建,效率较高,堆由 new 分配的内存,速度一般较慢。
- 申请后系统的响应不同,栈是有空间就分配,没空间就提示栈溢出。堆分配内存有两种方式,指针碰撞和空闲列表。
- 指针碰撞:Java 堆的内存是规整的,使用的内存放在一边,未使用的内存放在另一边,中间使用一个指针进行分隔,当分配内存时,指针向空闲内存方向移动一个等于对象内存大小的距离。
- 空闲列表:Java 堆的内存不是规整的,JVM 采用一个列表来记住内存空闲节点,在分配内存时找到一个足够大内存节点,将其分配给程序,并更新空闲列表。
- 栈内存比较小,堆内存比较大
- 栈不存在垃圾回收,堆需要进行垃圾回收。 栈和堆都会出现内存溢出的问题。
4、什么情况下会发生栈内存溢出?
栈是线程私有的,它的生命周期和线程一样,随着线程的启动而创建,在线程终止时销毁。每个方法在执行时会创建一个栈帧,用于存放局部变量表、操作数、动态链接、方法返回等信息。
- 当线程请求的栈深度超过
JVM
允许的最大深度时会抛出StackOverFlowError
异常。 一般是递归的时候出现该问题。可以使用-Xss
来设置JVM
栈的大小。- 当创建线程时,如果栈的内存不足,会抛出 OOM 异常。
5、谈谈对 OOM 的认识?如何排查 OOM 的问题?
StackOverFlowError:递归调用、方法内部定义了大量的局部变量导致栈空间不足。 可以通过
-Xss
来设置 JVM 栈的大小。
Java heap space:创建一个超大的对象、大量对象引用没有释放,JVM 无法对其自动回收,通过-Xmx
来设置JVM
堆内存空间
GC overhead limit exceeded:JVM 花费了 98% 以上的时间执行 GC ,但只恢复了不到 2% 的内存就会报错。
Direct buffer memory:通过 native 方法直接分配对堆外内存,直接内存溢出。
Unable to create new native thread:不能创建更多的线程了
Metaspace: 元空间不足
Requested array size exceeds VM limit
Out of swap space
kill process or sacrifice child
排查 OOM 的方法:
- 增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
- 同时 jstat 查看监控
JVM
的内存和 GC 情况,先观察问题大概出在什么区域; - 使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。
6、谈谈 JVM
中的常量池? JVM
常量池主要分为Class文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池。
- class 文件常量池:主要存放字面量和符号引用。字面量包括字符串和用 final 修饰的成员变量(包括静态变量、实例变量和局部变量)。符号引用包括类和接口的全限定名称、字段的名称和描述符、方法的名称和描述符。在类加载是会被加载到运行时常量池中。
- 运行时常量池:运行时常量池是基于class 文件常 量池创建的,运行时常量池时可以动态扩展的,在运行时可以通过代码生成常量并将其放入运行时常量池,这种特性被用的最多的是 String.intern(). 运行时常量池的数据有类和接口的数量决定,每个类或者接口被 JVM 加载时都会创建一个运行时常量池。
- 全局字符串常量池:字符串常量池是
JVM
所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。- 基本类型包装类对象常量池:java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。
常量池
constant_pool_count
常量池的数量,从 1 开始。0 表示不引用任何一个常量池。constant_pool[constant_pool_count-1]
常量池
常量池
主要存放:字面量
和符号引用
,这些内容在类加载阶段被加载到运行时常量池中。
- 字面量
- 文本字符串
public String s = "abc";
中的abc
- 用
final
修饰的成员变量,包括静态变量、实例变量和局部变量。
- 符号引用
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
运行时常量池
运行时常量池是方法区的一部分。不同的类公用一个运行时常量池。
Class 文件中除了包含类的版本、字段、方法、接口等描述信息外还有一个 用于存放编译期生成的各种字面量和符号引用
的常量池,这部分内容将在类加载后放到方法区的**运行时常量池
**中。
运行时常量池的作用?
- 存储字面量和符号引用
- 支持动态链接,在类加载的链接-解析阶段,会将符号引用转成直接引用。
- 支持 String.intern() 方法,可以确保字符串常量池只有一个字符串的唯一实例。
- 促进代码的优化,JIT 可以根据运行时常量池中的信息进行代码优化。(通过静态链接代替动态链接)
字符串常量池
为了减少在 jvm 中创建的字符串的数量,虚拟机维护了一个 字符串常量池。 当创建 String 对象时,jvm 会先检查字符串常量池,如果这个字符串的常量值已经存在字符串常量池中,就直接返回池中的对象,如果不在池中,就会实例化一个字符串并放入池中。 字符串常量池中是不会存储相同的字符串 字符串常量池是一个固定大小的 HashTable
String s = new String("wangzhy");
创建了两个对象(先在字符串常量池中创建对象,再去堆上创建对象):使用 new 关键字创建一个字符串对象时,JVM 会现在字符串常量池中查找有没有这个字符串,如果有就不会在字符串常量池中创建这个对象,直接在堆中创建这个字符串对象,然后将堆中这个对象的地址返回赋值给 s。
String s = "wangzhy";