关于后端:JVM运行数据区深度解析

2次阅读

共计 8866 个字符,预计需要花费 23 分钟才能阅读完成。

运行数据区

字节码只是一个二进制文件寄存在那里。要想在 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 操作,防止流在堆内外的拷贝。咱们下一步的调优不会波及到它,理解即可。

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

如果本文对您有帮忙,欢送 关注 点赞 ;如果您有任何倡议也可 留言评论 私信,您的反对是我保持创作的能源。

转载请注明出处!

正文完
 0