[转帖]JVM 运行数据区深度解析

jvm,运行,数据,深度,解析 · 浏览次数 : 0

小编点评

1. **方法区溢出**:由于 1.6 方法区被限制在堆空间中,在类超出控制的时候,仍然会打爆方法区。 2. **永久代**:1.8 里,字符串常量池被移到了堆空间,而其他对象仍然在元空间中。 3. **错误角度**:由于 1.6 方法区被放在了堆的永久代,而元空间大小限制在 6MB,当对象超过 6MB 时就会抛出 `GC overhead limit exceeded` 的异常。 4. **归属**: * **计数器、虚拟机栈、本地方法栈**:这些空间共享,当对象数量足够时就会发生溢出。 * **永久代**:这是一个逻辑概念,它存储的是方法区,而方法区在堆空间中。 * **方法区**:方法区是在方法区中创建的,因此在方法区中创建的类对象也会被存储在堆空间中。

正文

https://my.oschina.net/jiagoushi/blog/5597878

 

 

运行数据区

字节码只是一个二进制文件存放在那里。要想在 jvm 里跑起来,先得有个运行的内存环境。

也就是我们所说的 jvm 运行时数据区。

1)运行时数据区的位置

运行时数据区是 jvm 中最为重要的部分,执行引擎频繁操作的就是它。类的初始化,以及后面我们讲的对象空间的分配、垃圾的回收都是在这块区域发生的。

 

2)区域划分

根据《Java 虚拟机规范》中的规定,在运行时数据区将内存细分为几个部分

线程私有的:Java 虚拟机栈(Java Virtual Machine Stack)、程序计数器(Program Counter Register)、本地方法栈(Native Method Stacks)

大家共享的:方法区(Method Area)、Java 堆区(Java Heap)

 

接下来我们分块详细来解读,每一块是做什么的,如果溢出了会发生什么事情

1.1 程序计数器

1.1.1 概述

程序计数器(Program Counter Register)

  • 每个线程一个。是一块较小的内存空间,它表示当前线程执行的字节码指令的地址。

  • 字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,所以整个程序无论是分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

  • 由于线程是多条并行执行的,互相之间执行到哪条指令是不一样的,所以每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为 “线程私有” 的内存。

  • 如果是 native 方法,这里为空

1.1.2 溢出异常

没有!

在虚拟机规范中,没有对这块区域设定内存溢出规范,也是唯一一个不会溢出的区域

1.1.3 案例

因为它不会溢出,所以我们没有办法给它造一个,但是从 class 类上可以找到痕迹。

回顾上面 javap 的反汇编,其中 code 所对应的编号就可以理解为计数器中所记录的执行编号。

 

1.2 虚拟机栈

 

1.2.1 概述

  • 也是线程私有的!生命周期与线程相同。
  • 它描述的是 Java 方法执行的当前线程的内存模型,每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

1.2.2 溢出异常

1)栈深度超出设定

如果是创建的栈的深度大于虚拟机允许的深度,抛出

Exception in thread "main" java.lang.StackOverflowError

2)内存申请不足

如果栈允许内存扩展,但是内存申请不够的时候,抛出 OutOfMemoryError

注意!这一点和具体的虚拟机有关,hotspot 虚拟机并不支持栈空间扩展,所以单线程环境下,一个线程创建时,分配给它固定大小的一个栈,在这个固定栈空间上不会出现再去扩容申请内存的情况,也就不会遇到申请不到一说,只会因为深度问题超出固定空间造成上面的 StackOverflowError

如果换成多线程,毫无节制的创建线程,还是有可能造成 OutOfMemoryError。但是这个和 Xss 栈空间大小无关。是因为线程个数太多,栈的个数太多,导致系统分配给 jvm 进程的物理内存被吃光。

这时候虚拟机会附带相关的提示:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread

ps: 每个线程默认分配 1M 空间(64 位 linux,hotspot 环境)

疑问:是不是改小 Xss 的值就可以得到栈空间溢出呢?

答:根据上面的分析,hotspot 下不可以,还是会抛出 StackOverflowError,无非深度更小了。

1.2.3 案例一:进出栈顺序

1)代码

package com.itheima.jvm.demo;

/**
 * 程序模拟进栈、出栈过程
 * 先进后出
 */
public class StackInAndOut {
    /**
     * 定义方法一
     */
    public static void A() {
        System.out.println("进入方法A");
    }

    /**
     * 定义方法二;调用方法一
     */
    public static void B() {
        A();
        System.out.println("进入方法B");
    }

    public static void main(String[] args) {

        B();
        System.out.println("进入Main方法");
    }
}



2)运行结果:

进入方法A
进入方法B
进入Main方法

3)栈结构:

main 方法 ---->B 方法 ---->A 方法

1.2.4 案例二:栈深度溢出

1)代码

这个容易实现,方法嵌套自己就可以:

package com.itheima.jvm.demo;

/**
 * 通过一个程序模拟线程请求的栈深度大于虚拟机所允许的栈深度;
 * 抛出StackOverflowError
 */
public class StackOverFlow {
    /**
     * 定义方法,循环嵌套自己
     */
    public static void B() {
        B();
        System.out.println("进入方法B");
    }

    public static void main(String[] args) {

        B();
        System.out.println("进入Main方法");
    }
}


2)运行结果:

Exception in thread "main" java.lang.StackOverflowError
	at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)
	at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)
	at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)
	at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)
	at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)

3)栈结构:

 

1.2.5 案例三:栈内存溢出

一直不停的创建线程就可以堆满栈

但是!这个很危险,到 32 系统的 winxp 上勇敢的小伙伴可以试一试,机器卡死不负责!

package com.itheima.jvm.demo;

/*
* 栈内存溢出,注意!很危险,谨慎执行
* 执行时可能会卡死系统。直到内存耗尽
* */
public class StackOutOfMem {
    public static void main(String[] args) {
        while (true) {
            new Thread(() -> {
                while(true);
            }).start();
        }
    }
}

1.3 本地方法栈

1.3.1 概述

  • 本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点

  • 不同的是,本地方法栈服务的对象是 JVM 执行的 native 方法,而虚拟机栈服务的是 JVM 执行的 java 方法

  • 虚拟机规范里对这块所用的语言、数据结构、没有强制规定,虚拟机可以自由实现它

  • 甚至,hotspot 把它和虚拟机栈合并成了 1 个

1.3.2 溢出异常

和虚拟机栈一样,也是两个:

如果是创建的栈的深度大于虚拟机允许的深度,抛出 StackOverFlowError

内存申请不够的时候,抛出 OutOfMemoryError

1.4 堆

1.4.1 概述

与上面的 3 个不同,堆是所有线程共享的!所谓的线程安全不安全也是出自这里。

在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里 “几乎” 所有的对象实例都在这里分配内存。

需要注意的是,《Java 虚拟机规范》并没有对堆进行细致的划分,所以对于堆的讲解要基于具体的虚拟机,我们以使用最多的 HotSpot 虚拟机为例。

Java 堆是垃圾收集器管理的内存区域,因此它也被称作 “GC 堆”,这就是我们做 JVM 调优的重点区域部分。

1.4.2 jdk1.7

jvm 的内存模型在 1.7 和 1.8 有较大的区别,虽然 1.7 目前使用的较少了,但是我们也是需要对 1.7 的内存模型有所了解,所以接下里,我们将先学习 1.7 再学习 1.8 的内存模型。

 

  • Young 年轻区(代)

    Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区

    其中,Survivor 区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用

    在 Eden 区间变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾收集后,任然存活于 Survivor 的对象将被移动到下面的 Tenured 区间。

  • Tenured 年老区

    Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区,一般如果系统中用了 application 级别的缓存,缓存中的对象往往会被转移到这一区间。

  • Perm 永久区

    hotspot 1.6 才有这货,现在已经成为历史

    Perm 代主要保存 class,method,filed 对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到 java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的 class 没有被卸载掉,这样就造成了大量的 class 对象保存在了 perm 中,这种情况下,一般重新启动应用服务器可以解决问题。另外一种可能是创建了大批量的 jsp 文件,造成类信息超出 perm 的上限而溢出。这种重启也解决不了。只能调大空间。

  • Virtual 区:

    jvm 参数可以设置一个范围,最大内存和初始内存的差值,就是 Virtual 区。

1.4.3 jdk1.8

 

由上图可以看出,jdk1.8 的内存模型是由 2 部分组成,年轻代 + 年老代。永久代被干掉,换成了 Metaspace(元数据空间)

年轻代:Eden + 2*Survivor (不变)

年老代:OldGen (不变)

元空间:原来的 perm 区 (重点!)

需要特别说明的是:Metaspace 所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与 1.7 的永久代最大的区别所在。

 

1.4.4 溢出异常

内存不足时,抛出

java.lang.OutOfMemoryError: Java heap space

1.4.5 案例:堆溢出

1)代码

分配大量对象,超出 jvm 规定的堆范围即可

package com.itheima.jvm.demo;

import java.util.ArrayList;
import java.util.List;

/**
 * 堆溢出
 *   -Xms20m -Xmx20m
 */
public class HeapOOM {
    Byte[] bytes = new Byte[1024*1024];
    public static void main(String[] args) {
        List list = new ArrayList();
        int i = 0;
        while (true) {
            System.out.println(++i);
            list.add(new HeapOOM());
        }
    }
}

2)启动

注意启动时,指定一下堆的大小:

 

2)输出

1
2
3
4
5
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.itheima.jvm.demo.HeapOOM.<init>(HeapOOM.java:7)
	at com.itheima.jvm.demo.HeapOOM.main(HeapOOM.java:13)

1.5 方法区

1.5.1 概述

同样,线程共享的。

它主要用来存储类的信息、类里定义的常量、静态变量、编译器编译后的代码缓存。

注意!方法区在虚拟机规范里这是一个逻辑概念,它具体放在那个区域里没有严格的规定。

所以,hotspot 1.7 将它放在了堆的永久代里,1.8 + 单独开辟了一块叫 metaspace 来存放一部分内容(不是全部!定义的类对象在堆里)

具体方法区主要存什么东西呢?粗略的分,可以划分为两类:

  • 类信息:主要指类相关的版本、字段、方法、接口描述、引用等

  • 运行时常量池:编译阶段生成的常量与符号引用、运行时加入的动态变量

    (常量池里的类变量,如对象或字符串,比较特殊,1.6 和 1.8 位置不同,下面会讲到)

小提示:

这里经常会跟上面堆里的永久代混为一谈,实际上这是两码事

永久代是 hotspot 在 1.7 及之前才有的设计,1.8+,以及其他虚拟机并不存在这个东西。

可以说,永久代是 1.7 的 hotspot 偷懒的结果,他在堆里划分了一块来实现方法区的功能,叫永久代。因为这样可以借助堆的垃圾回收来管理方法区的内存,而不用单独为方法区再去编写内存管理程序。懒惰!

同时代的其他虚拟机,如 J9,Jrockit 等,没有这个概念。后来 hotspot 认识到,永久代来做这件事不是一个好主意。1.7 已经从永久代拿走了一部分数据,直到 1.8 + 彻底去掉了永久代,方法区大部分被移到了 metaspace(再强调一下,不是全部!)

结论:

方法区是一定存在的,这是虚拟机规定的,但是是个逻辑概念,在哪里虚拟机自己去决定

而永久代不一定存在(hotspot 1.7 才有),已成为历史

1.5.2 溢出异常

1.6:OutOfMemoryError: PermGen space

1.8:OutOfMemoryError: Metaspace

1.5.3 案例:1.6 方法区溢出

1)原理

在 1.6 里,字符串常量是运行时常量池的一部分,也就是归属于方法区,放在了永久代里。

所以 1.6 环境下,让方法区溢出,只需要可劲造往字符串常量池中造字符串即可,这里用到一个方法:

/*
如果字符串常量池里有这个字符串,直接返回引用,不再额外添加
如果没有,加进去,返回新创建的引用
*/
String.intern()

2)代码

/**
 * 方法区溢出,注意限制一下永久代的大小
 * 编译的时候注意pom里的版本,要设置1.6,否则启动会有问题
 * jdk1.6  :     -XX:PermSize=6M -XX:MaxPermSize=6M
 */
public class ConstantOOM {

    public static void main(String[] args) {
        ConstantOOM oom = new ConstantOOM();
        Set<String> stringSet = new HashSet();
        int i = 0;
        while (true) {
            System.out.println(++i);
            stringSet.add(String.valueOf(i).intern());
        }
    }
}

3)创建启动环境

 

4)异常信息:

...
19118
19119
19120
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
	at java.lang.String.intern(Native Method)
	at com.itheima.jvm.demo.ConstantOOM.main(ConstantOOM.java:19)

1.5.4 案例:1.8 方法区溢出

1)到了 1.8,情况发生了变化

可以测试一下,1.8 下无论指定下面的哪个参数,常量池运行都不会溢出,会一直打印下去

-XX:PermSize=6M -XX:MaxPermSize=6M
-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M

2)配置运行环境

 

3)控制台信息

不会抛出异常,只要你 jvm 堆内存够,理论上可以一直打下去

 

4)为什么呢?

永久代我们加了限制,结果没意义,因为 1.8 里已经没有这货了

元空间也加了限制,同样没意义,那说明字符串常量池它不在元空间里!

那么,它在哪里呢?

 

jdk1.8 以后,字符串常量池被移到了堆空间,和其他对象一样,接受堆的控制。

其他的运行时的类信息、基本数据类型等在元空间。

我们可以验证一下,对上面的运行时参数再加一个堆上限限制:

-Xms10m
-Xmx10m

运行环境如下:

 

运行没多久,你会得到以下异常:

……
84014
84015
84016
84017
84018
84019
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.lang.Integer.toString(Integer.java:403)
	at java.lang.String.valueOf(String.java:3099)
	at com.itheima.jvm.demo.ConstantOOM.main(ConstantOOM.java:18)

说明:1.8 里,字符串 inter () 被放在了堆里,受最大堆空间的限制。

5)那如何才能让元空间溢出呢?

既然字符串常量池不在这里,那就换其他的。类的基本信息总在元空间吧?我们来试一下

cglib 是一个 apache 下的字节码库,它可以在运行时生成大量的对象,我们 while 循环同时限制 metaspace 试试:

附:https://gitee.com/mirrors/cglib (想深入了解这个工具的猛击左边,这里不做过多讨论)

package com.itheima.jvm.demo;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * jdk8方法区溢出
 *   -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
 */
public class ConstantOOM8 {
    public static void main(final String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOM.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(objects,args);
                }
            });
            enhancer.create();
        }
    }

    static class OOM{

    }
}

6)运行设置

 

7)运行结果

Caused by: java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)

结论:

jdk8 引入元空间来存储方法区后,内存溢出的风险比历史版本小多了,但是在类超出控制的时候,依然会打爆方法区

1.6 一个案例

为便于大家理解和记忆,下面我们用一个案例,把上面各个区串通起来。

假设有个 Bootstrap 的类,执行 main 方法。在 jvm 里,它从 class 文件到跑起来,大致经过如下步骤:

 

  1. 首先 JVM 会先将这个 Bootstrap.class 信息加载到内存中的方法区
  2. 接着,主线程开辟一块内存空间,准备好程序计数器 pc,虚拟机栈、本地方法栈
  3. 然后,JVM 会在 Heap 堆上为 Bootstrap.class 创建一个 Bootstrap.class 的类实例
  4. JVM 开始执行 main 方法,这时在虚拟机栈里为 main 方法创建一个栈帧
  5. main 方法在执行的过程之中,调用了 greeting 方法,则 JVM 会为 greeting 方法再创建一个栈帧,推到虚拟机栈顶,在 main 的上面,每次只有一个栈帧处于活动状态,当前为 greeting
  6. 当 greeting 方法运行完成后,则 greeting 方法出栈,当前活动帧指向 main,方法继续往下运行

1.7 归纳总结

 

1)独享 / 共享的角度:

  • 独享:程序计数器、虚拟机栈、本地方法栈
  • 共享:堆、方法区

2)error 的角度:

  • 程序计数器:不会溢出,比较特殊,其他都会
  • 两个栈:可能会发生两种溢出,一是深度超了,报 StackOverflowError,空间不足:OutOfMemoryError
  • 堆:只会在空间不足时,报 OutOfMemoryError,会提示 heapSpace
  • 方法区:空间不足时,报 OutOfMemoryError,提示不同,1.6 是 permspace,1.8 是元空间,和它在什么地方有关

3)归属:

  • 计数器、虚拟机栈、本地方法栈:线程创建必须申请配套,真正的物理空间
  • 堆:真正的物理空间,但是内部结构的划分有变动,1.6 有永久代,1.8 被干掉
  • 方法区:最没归属感的一块,原因就是它是一个逻辑概念。1.6 被放在了堆的永久代,1.8 被拆分,一部分在元空间,一部分(方法区的运行时常量池里面的类对象,包括字符串常量,被设计放在了堆里)
  • 直接内存:这块实际上不属于运行时数据区的一部分,而是直接操作物理内存。在 nio 操作里 DirectByteBuffer 类可以对 native 操作,避免流在堆内外的拷贝。我们下一步的调优不会涉及到它,了解即可。

与[转帖]JVM 运行数据区深度解析相似的内容:

[转帖]JVM 运行数据区深度解析

https://my.oschina.net/jiagoushi/blog/5597878 运行数据区 字节码只是一个二进制文件存放在那里。要想在 jvm 里跑起来,先得有个运行的内存环境。 也就是我们所说的 jvm 运行时数据区。 1)运行时数据区的位置 运行时数据区是 jvm 中最为重要的部分,

[转帖]JVM(3)之垃圾回收(GC垃圾收集器+垃圾回收算法+安全点+记忆集与卡表+并发可达性分析......)

《深入理解java虚拟机》+宋红康老师+阳哥大厂面试题2总结整理 一、堆的结构组成 堆位于运行时数据区中是线程共享的。一个进程对应一个jvm实例。一个jvm实例对应一个运行时数据区。一个运行时数据区有一个堆空间。 java堆区在jvm启动的时候就被创建了,其空间大小也就被确定了(堆是jvm管理的最大

[转帖]JVM系列之:你知道Java有多少种内存溢出吗

本文为《深入学习 JVM 系列》第二十五篇文章 Java内存区域 关于这部分内容大多来源于《深入理解Java虚拟机》一书。 Java 运行时数据区域(JDK8)如下图所示: 关于上述提到的线程共享和线程隔离区域,下图做详细讲解: 程序计数器 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的

[转帖]12.JVM运行时数据区之虚拟机栈概述

`https://blog.csdn.net/u011069294/article/details/107050001` 目录 1. 内存中的栈与堆2.栈的优点 1. 内存中的栈与堆 栈是运行时单位,堆是存储的单位。 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。 堆解决的是数据存储的问

[转帖]【JVM】类加载机制

什么是类的加载 将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供

[转帖]JVM类加载机制

概述 虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Cl

[转帖]【JVM】Java内存区域与OOM

引入 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。 Java虚拟机运行时数据区 如图所示 1.程序计数器(线程私有) 作用 记录当前线程所执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节

[转帖]【JVM】Java内存区域与OOM

引入 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。 Java虚拟机运行时数据区 如图所示 1.程序计数器(线程私有) 作用 记录当前线程所执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节

[转帖]docker容器自动重启,看完这篇彻底明白了

一. JVM内存区域的划分 1.1 java虚拟机运行时数据区 java虚拟机运行时数据区分布图: JVM栈(Java Virtual Machine Stacks): Java中一个线程就会相应有一个线程栈与之对应,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈,因此栈存储的信息都是跟当

[转帖]JVM——内存区域:运行时数据区域详解

https://www.jianshu.com/p/cded765cfd1b 关注:CodingTechWork,一起学习进步。 引言 我们经常会被问到一个问题是Java和C++有何区别?我们除了能回答一个是面向对象、一个是面向过程编程以外,我们还会从底层内存管理和垃圾收集方面作出比较。 对于C++