面试官上来一个平A,”你对JVM了解多少?"。我心想:好家伙,我直接开始吟唱!
JVM内存划分
线程不共享区域
程序计数器
当前线程所执行的字节码的行号指示器。每个线程之间独立存储运行。
⚠️ 注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
方法栈
JVM 运行时数据区域的一个核心,存放方法调用的数据。
方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。
局部变量表
存放方法中的基本数据类型和对象引用。
注意: 在new一个对象的时候,实际是创建了一个对象引用和实际的对象内存地址。
(前半部分为对象引用,存放在栈的局部变量表中)
Person person = new Person();
(后半部分才是实际的对象,存放在jvm堆中)
坑点!: 虽然栈中会存放基本数据类型,但作为实例变量的基本数据类型是存放在堆中的,因为实例变量并不属于方法,而是类,栈中存放的是方法调用过程中的数据。
程序运行中栈可能会出现两种错误:
StackOverFlowError: 若栈的内存大小不允许动态扩展,无限个方法调用作为栈帧存入栈,就会发生StackOverFlowError错误。OutOfMemoryError: 如果栈的内存大小可以动态扩展, 栈变得大于了Jvm内存限制,则抛出OutOfMemoryError异常。
线程共享区域
堆
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap) 。从垃圾回收的角度,由于现在收集器(CMS,G1)基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代(CMS);再细致一点有:Eden、Survivor、Old 等空间(G1)。
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
-
设置堆内存大小:
-Xms<size>: 设置 JVM 的初始堆大小。-Xmx<size>: 设置 JVM 最大堆大小。
示例:java -Xms512m -Xmx1024m YourMainClass
堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。java.lang.OutOfMemoryError: Java heap space:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。
方法区
JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。
1、整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
当元空间溢出时会得到如下错误:
java.lang.OutOfMemoryError: MetaSpace
你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
2、元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
方法区内容分析
- 类的元数据信息:包括类的结构信息、方法信息、字段信息、方法字节码等。每个类在方法区中都有一个对应的 Class 对象,用于表示这个类的结构信息。
- 运行时常量池:方法区包含了每个类的运行时常量池,其中存放着编译期生成的各种字面量和符号引用。例如字符串常量、类和接口的全限定名、字段和方法的名称和描述符等。
- 静态变量:方法区也包含了所有类中的静态字段,无论这些字段是否被声明为 final。
JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存