5

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)

 1 year ago
source link: https://blog.51cto.com/boxuegu/5706874
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)

推荐 原创

博学谷狂野架构师 2022-09-23 14:34:22 博主文章分类:技术干货 ©著作权

文章标签 后端 java 文章分类 Java 编程语言 私藏项目实操分享 阅读数299

大家好,,这篇文章咱们聊下JVM性能优化的问题

这篇文章主要介绍下JVM的运行数据区相关的内容,包括:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • 方法区
  • 案例 和总结

好了,开始干货环节~

作为一个常识性的知识,大家都知道字节码只是一个二进制文件存放在那里。要想在jvm里跑起来,先得有个运行的内存环境。

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

1)运行时数据区的位置

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

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_jvm

2)区域划分

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

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

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

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_java_02

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

3.1 程序计数器

3.1.1 概述

程序计数器(Program Counter Register)

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

<!---->

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

3.1.2 溢出异常

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

3.1.3 案例

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

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

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_jvm_03

3.2 虚拟机栈

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_jvm_04

3.2.1 概述

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

3.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,无非深度更小了。

3.2.3 案例一:进出栈顺序

 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运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_java_05

3.2.4 案例二:栈深度溢出

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

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运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_后端_06
【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_后端_07

3.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();
        }
    }
}

3.3 本地方法栈

3.3.1 概述

  • 本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点
  • 不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法
  • 虚拟机规范里对这块所用的语言、数据结构、没有强制规定,虚拟机可以自由实现它
  • 甚至,hotspot把它和虚拟机栈合并成了1个

3.3.2 溢出异常

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

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

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

3.4.1 概述

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

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

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

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

3.4.2 jdk1.7

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

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_jvm_08
  • 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区。

3.4.3 jdk1.8

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_jvm_09

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

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

年老代:OldGen (不变)

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

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

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_java_10

3.4.4 溢出异常

内存不足时,抛出

java.lang.OutOfMemoryError: Java heap space

3.4.5 案例:堆溢出

分配大量对象,超出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());
        }
    }
}

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

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_java_11
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)

3.5 方法区

3.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 才有),已成为历史

3.5.2 溢出异常

1.6:OutOfMemoryError: PermGen space

1.8:OutOfMemoryError: Metaspace

3.5.3 案例:1.6方法区溢出

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_java_12

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

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

/*
如果字符串常量池里有这个字符串,直接返回引用,不再额外添加
如果没有,加进去,返回新创建的引用
*/
String.intern()
/**
 * 方法区溢出,注意限制一下永久代的大小
 * 编译的时候注意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运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_jvm_13

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)

2.5.4 案例:1.8方法区溢出

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

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

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

2)配置运行环境

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_jvm_14

3)控制台信息

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

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_java_15

4)为什么呢?

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

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

那么,它在哪里呢?

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_jvm_16

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

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

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

-Xms10m
-Xmx10m

运行环境如下:

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_java_17

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

……
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运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_jvm_18

7)运行结果

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

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

3.6 一个案例

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

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

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_jvm_19
  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,方法继续往下运行

3.7 归纳总结

【预警】有点干,JVM运行时数据区(计数器、虚拟机栈、本地方法栈、堆、方法区)_jvm_20

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运行数据区的内容,就告一段落了,如果大家觉得有帮助的话,欢迎点赞 拍砖

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

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

转载请注明出处!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK