JVM-1 内存管理

111 阅读8分钟

JVM

JIT(Just In Time)编译器 Fork/Join模式是处理并行的一个经典方法

1. 内存管理

自动内存管理机制

1.1 内存区域

虚拟机运行时数据区

说明:

  • 1 程序计数器
线程私有。
是一块较小的内存区域。可以看做当前线程所执行的字节码的行号指示器。
字节码解释器工作时就是通过改变这个计数器的值来选择下一跳需要执行的字节码指令。
分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
  • 2 虚拟机栈
线程私有。
生命周期与线程相同。
是方法执行的内存模型:每个方法在执行的同时会创建一个栈帧,用来存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法从调用到完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的工程。

局部变量表存放了编译期可知的各种基本数据类型和对象引用类型。

如果线程请求的栈深度大于虚拟机栈允许的深度,报StackOverflowError。
如果虚拟机栈动态扩展时无法申请到足够的内存,报OutOfMemoryError。
  • 3 本地方法栈
与虚拟机栈作用类似。
虚拟机栈是为执行Java代码方法(字节码)服务的。
本地方法栈是为Native方法服务的。

本地方法栈也会有StackOverflowError和OutOfMemoryError异常。
  • 4 堆
线程共享。
基本是虚拟机管理的内存中最大的一块。
在虚拟机启动时创建。

唯一的作用是存放对象实例。
几乎所有的对象都在这里分配内存。

堆是GC管理的主要区域。
从内存回收的角度来看,由于现代收集器基本都采用分代收集算法,所以Java堆可以细分为:新生代和老年代,再细分一点有Eden空间、From Survivor空间、To Survivor空间等。

堆在物理上是不连续的内存空间。只要逻辑上连续即可。

-Xms
-Xmx

如果堆中没有内存来完成分配,也无法再扩展时,报OutOfMemoryError。
  • 5 方法区
线程共享。

存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

别名叫做非堆(Non-Heap)。与堆区分开。

对于HotSpot虚拟机来说,叫永久代(Permanent Generation)

-XX:MaxPermSize

JDK1.7 HotSpot中,将字符串常量池从永久代中移除。

当方法区无法满足内存分配时,报OutOfMemoryError。
  • 6 运行时常量池
Runtime Constant Pool。
是方法区的一部分。

Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项就是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,
在类加载后进入方法区的运行时常量池中存放。

具备动态性。
Java语言并不要求常量一定只有在编译期才能产生,也就是并非预置入class文件中常量池的内容才能进方法区的运行时常量池。
运行期间也可以将新的常量放入池中,比如String类的intern()方法。
  • 7 直接内存
Direct Memory。
不是虚拟机运行时数据区的部分,也不是JVM规范中定义的内存区域。

在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(channel)与缓冲区(buffer)的IO方式,
可以使用Native库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

1.2 对象创建过程

JVM遇到一条new指令时:
1 首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,
并且检查这个符号引用代表的类是否已被加载、解析和初始化过。
如果没有,必须先执行相应的类加载过程。

2 类加载检查通过后,JVM为新生对象分配内存。对象所需的内存大小在类加载完后便可完全确定。

为对象分配空间的任务等同于把一块确定大小的内存从堆中划分出来。

假设堆中内存是绝对规整的,所有用过的内存放一边,空闲内存放另一边,中间放着一个指针作为分界点的指示器。
那么分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
这种分配方式称为“指针碰撞”。

如果堆中内存不是规整的,已使用的内存和空闲内存互相交错。JVM维护一个列表,记录了那些内存块是可用的,
在分配的时候从列表里找到一块足够大的空间划分给对象,并更新列表上的记录。
这种分配方式称为“空闲列表”。

选择哪种方式由堆是否规整决定。
堆是否规整由GC是否带有压缩整理功能决定。

JVM采用CAS配上失败重试的方式来保证更新操作的原子性。

3 接下来,JVM需要对对象进行必要的设置。
例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的gc分代年龄,是否启用偏向锁等信息。
这些信息存放在对象的对象头(Object Header)中。

4 上面完成之后,从虚拟机的角度来看,一个对象已经创建完毕。
从程序的角度来看,对象创建才刚开始,<init>方法还没执行,所有字段还是0。
执行完new指令后,接着执行<init>方法,把对象按照代码进行初始化。

初始化完毕后,一个真正可用的对象才算完全产生出来。

1.3 对象的内存布局

对象在内存中有3块区域:
对象头(Header)
实例数据(Instance Data)
对齐填充(Padding)

对象头里一部分存储对象自身的运行时数据,如哈希码,gc分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
另一部分是类型指针,即对象指向它的类元数据的指针,JVM通过这个指针来确定这个对象是哪个类的实例。
如果对象是数组类型,对象头中还必须有一块用于记录数组长度的数据。

实例数据部分是对象真正存储的有效信息。
也就是在代码中定义的各种类型的字段内容。包含从父类继承下来的,还有在子类中定义的。

对齐部分并不是必然存在的,没有特别的含义。
仅仅起着占位符的作用。

1.4 对象的访问定位

程序需要通过栈上的reference引用来操作堆上的具体对象。
reference类型在JVM规范中只规定了一个指向对象的引用,
并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置。

对象访问方法取决于JVM的实现。
目前主流的访问方式有使用句柄和直接指针两种。
  • 1 使用句柄
reference中存储的是对象的句柄地址。
句柄中包含了对象的实例数据与类型数据各自的具体地址。

好处:
对象移动时,reference不需要修改,只需要修改句柄中的实例数据指针

  • 2 直接指针
reference中存储的直接就是对象地址。

好处:
速度更快,节省了一次指针定位的时间开销。(HotSpot使用这种方式)

1.4 内存溢出异常实战

VM参数
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+printGCDetails -XX:SurvivorRatio=8

将-Xms和-Xmx参数设置一样即可避免堆自动扩展

通过参数
-XX:+HeapDumpOnOutOfMemoryError
可以让JVM出现内存溢出时Dump出当前的内存转储快照。

-Xss 参数设置栈的大小
-Xss128k
-Xss2M

设置方法区大小(限制常量池大小)
-XX:PermSize
-XX:MaxPermSize

设置直接内存
-XX:MaxDirectMemorySize=10M
-Xmx20M
如果不指定,默认与-Xmx一样
  • 1 模拟堆溢出
public class HeapOOM {
    static class OOMObject {}
    
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while(true) {
            list.add(new OOMObject());
        }
    }
}

$ javac HeapOOM.java
$ java HeapOOM -Xms20M -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid34355.hprof ...
Heap dump file created [4324232 bytes in 0.6343 secs]
分析内存快照文件。
确定是否是内存泄漏引起的。
如果不是,考虑-Xms -Xmx来设置堆的大小。
  • 2 栈溢出
public class JavaVMStackSOF {
    private int stackLength = 1;
    
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    
    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch(Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}
  • 3 方法区OOM
利用String.intern()

利用CGLib直接操作字节码运行时生成大量的动态类。

  • 4 直接内存OOM
Java NIO部分可能会产生