深入理解JVM虚拟机-JVM内存区域与内存溢出

栏目: Java · 发布时间: 5年前

内容简介:程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机中,字节码解释器工作时就是通过改变计数器值来选取下一条执行的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 在任何时刻,一个处理器(内核)都只会执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,这类内存区域为Java虚拟机栈也是线程私有的,它的生命周期与线程相同。 每个方法在执行时都会创建一个栈帧,用于

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机中,字节码解释器工作时就是通过改变计数器值来选取下一条执行的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 在任何时刻,一个处理器(内核)都只会执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,这类内存区域为 线程私有的内存此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

1.2 java虚拟机栈

Java虚拟机栈也是线程私有的,它的生命周期与线程相同。 每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口 等信息。一个方法从调用到执行完成就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 虚拟机栈中的局部变量表存放着编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double),对象引用(reference类型),returnAddress类型(指向了一条字节码指令的地址)。 StackOverflowError异常:线程请求的栈深度大于虚拟机所允许的深度。 OutOfMemoryError异常:虚拟机栈可以动态扩展无法申请到足够的内存。

1.3 本地方法栈

本地方法栈与虚拟机栈的作用是类似的,只不过虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 有的虚拟机(例如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

1.4 java 堆

Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆的唯一目的就是 存放对象实例 ,几乎所有的对象实例都在这里分配内存。 Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。 OutOfMemoryError异常:堆中没有内存完成实例分配,并且堆也无法再扩展。

1.5 方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 在Hotspot虚拟机上,很多人更愿意把方法区称为“永久代”。 方法区和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对来说垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。 这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

1.5.1 jdk6-8中方法区的不同

在jdk7之前,HotSpot虚拟机中将GC分代收集扩展到了方法区,使用永久代来实现了方法区。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,但是在之后的HotSpot虚拟机实现中,逐渐开始将方法区从永久代移除。jdk7中已经将运行时常量池从永久代移除,在Java堆(Heap)中开辟了一块区域存放运行时常量池。而在jdk8中,已经彻底没有了永久代,将方法区直接放在一个与堆不相连的本地内存区域,这个区域叫元空间。

深入理解JVM虚拟机-JVM内存区域与内存溢出

Metaspace(元空间) 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过参数来指定元空间的大小。

1.6 运行时常量池

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

2. HotSpot虚拟机对象探秘

2.1 对象的创建

  1. 当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,
  2. 接下来虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后便可以完全确定。对象内存分配分为两种: 指针碰撞 :设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。 空闲列表 :如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

对象创建在虚拟机中是非常频繁的行为,在并发情况下也并不是线程安全的。解决这个问题有两种方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。 3. 接下来虚拟机要对对象进行必要的设置,,如对象的哈希码,GC的分代年龄等,这些信息存放在对象的对象头中。 4. 当上面的步骤走完后,从虚拟机的角度看一个新的对象已经产生了,但从java程序来看,这个对象才刚刚开始---- init 方法还没有执行。 当执行new指令之后会接着执行 init 方法,把对象按照 程序员 的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

2.2 对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

2.2.1 对象头

对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,锁状态标志等。另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

2.2.2 实例数据

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。

2.2.3 对齐填充

对齐填充并不是必然存在的,仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍,不足就由对象填充来补齐。

2.3 对象的访问定位

我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。 目前主流的访问方式有使用句柄和直接指针两种。

2.3.1 句柄访问

使用句柄访问,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。 句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。

深入理解JVM虚拟机-JVM内存区域与内存溢出

2.3.2 直接指针访问

使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。 直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。

深入理解JVM虚拟机-JVM内存区域与内存溢出

Hotpost虚拟机使用的是直接指针访问的方式。

3 内存溢出

内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

3.1 Java堆溢出

Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。 JVM得拓展参数在注释中。

public class HeapOOM {

    /**
     *java堆溢出
     * 
     * -verbose:gc -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
     */
    public static void main(String[] args) throws InterruptedException {
        ArrayList<OOMObject> oomObjects = new ArrayList<>();
        while (true){
            oomObjects.add(new OOMObject());
        }
    }

    static class OOMObject{}
}
复制代码

输出结果如下:

深入理解JVM虚拟机-JVM内存区域与内存溢出

Java堆内存的OOM异常是实际应用中常见的内存溢出异常情况。当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。

3.2 虚拟机栈溢出

关于虚拟机栈在Java虚拟机规范中描述了两种异常: 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

public class JavaVmStackSOF {

    private int stackLength = 1;
    public void stackLeak(){
        stackLength++;
        stackLeak();
    }

    /** 
     *虚拟机栈溢出
     * -Xss128k
     */
    public static void main(String[] args) {
        JavaVmStackSOF javaVmStackSOF = new JavaVmStackSOF();
        try{
            javaVmStackSOF.stackLeak();
        }catch (Throwable t){
            System.out.println("stack length:"+javaVmStackSOF.stackLength);
            throw t;
        }
    }

}
复制代码

运行结果:

深入理解JVM虚拟机-JVM内存区域与内存溢出

4 常用JVM命令参数

-Xms20M
表示设置JVM启动内存的最小值为20M,必须以M为单位

-Xmx20M
表示设置JVM启动内存的最大值为20M,必须以M为单位。将-Xmx和-Xms设置为一样可以避免JVM内存自动扩展。大的项目-Xmx和-Xms一般都要设置到10G、20G甚至还要高

-verbose:gc
表示输出虚拟机中GC的详细情况

-Xss128k
表示可以设置虚拟机栈的大小为128k

-Xoss128k
表示设置本地方法栈的大小为128k。不过HotSpot并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说这个参数是无效的

-XX:PermSize=10M
表示JVM初始分配的永久代的容量,必须以M为单位

-XX:MaxPermSize=10M
表示JVM允许分配的永久代的最大容量,必须以M为单位,大部分情况下这个参数默认为64M

-Xnoclassgc
表示关闭JVM对类的垃圾回收

-XX:+TraceClassLoading
表示查看类的加载信息

-XX:+TraceClassUnLoading
表示查看类的卸载信息

-XX:NewRatio=4
表示设置年轻代:老年代的大小比值为1:4,这意味着年轻代占整个堆的1/5

-XX:SurvivorRatio=8
表示设置2个Survivor区:1个Eden区的大小比值为2:8,这意味着Survivor区占整个年轻代的1/5,这个参数默认为8

-Xmn20M
表示设置年轻代的大小为20M

-XX:+HeapDumpOnOutOfMemoryError
表示可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照

-XX:+UseG1GC
表示让JVM使用G1垃圾收集器

-XX:+PrintGCDetails
表示在控制台上打印出GC具体细节

-XX:+PrintGC
表示在控制台上打印出GC信息

-XX:PretenureSizeThreshold=3145728
表示对象大于3145728(3M)时直接进入老年代分配,这里只能以字节作为单位

-XX:MaxTenuringThreshold=
表示对象年龄大于1,自动进入老年代

-XX:CompileThreshold=1000
表示一个方法被调用1000次之后,会被认为是热点代码,并触发即时编译

-XX:+PrintHeapAtGC
表示可以看到每次GC前后堆内存布局

-XX:+PrintTLAB
表示可以看到TLAB的使用情况

-XX:+UseSpining
开启自旋锁

-XX:PreBlockSpin
更改自旋锁的自旋次数,使用这个参数必须先开启自旋锁
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Effective Engineer

The Effective Engineer

Edmond Lau / The Effective Bookshelf, Palo Alto, CA. / 2015-3-19 / USD 39.00

Introducing The Effective Engineer — the only book designed specifically for today's software engineers, based on extensive interviews with engineering leaders at top tech companies, and packed with h......一起来看看 《The Effective Engineer》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具