发布时间:2022-11-23 文章分类:编程知识 投稿人:李佳 字号: 默认 | | 超大 打印

运行数据区

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

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

1)运行时数据区的位置

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

JVM运行数据区深度解析

2)区域划分

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

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

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

JVM运行数据区深度解析

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

1.1 程序计数器

1.1.1 概述

程序计数器(Program Counter Register)

1.1.2 溢出异常

没有!

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

1.1.3 案例

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

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

JVM运行数据区深度解析

1.2 虚拟机栈

JVM运行数据区深度解析

1.2.1 概述

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方法
JVM运行数据区深度解析

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)栈结构:

JVM运行数据区深度解析

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 概述

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的内存模型。

JVM运行数据区深度解析

1.4.3 jdk1.8

JVM运行数据区深度解析

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

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

年老代:OldGen (不变)

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

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

JVM运行数据区深度解析

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)启动

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

JVM运行数据区深度解析

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来存放一部分内容(不是全部!定义的类对象在堆里)

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

小提示:

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

永久代是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)原理

JVM运行数据区深度解析
在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)创建启动环境

JVM运行数据区深度解析

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)配置运行环境

JVM运行数据区深度解析

3)控制台信息

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

JVM运行数据区深度解析

4)为什么呢?

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

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

那么,它在哪里呢?

JVM运行数据区深度解析

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

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

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

-Xms10m
-Xmx10m

运行环境如下:

JVM运行数据区深度解析

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

……
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)运行设置

JVM运行数据区深度解析

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文件到跑起来,大致经过如下步骤:

JVM运行数据区深度解析

  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 归纳总结

JVM运行数据区深度解析

1)独享/共享的角度:

2)error的角度:

3)归属:

本文由传智教育博学谷教研团队发布。

如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

转载请注明出处!