1、深入理解JVM内存区域

一、为什么要了解虚拟机

JVM 不单单只支持 Java 语言,也支持其他语言(Scala、Kotlin、Groovy 等等)区块链 2.0–以太坊(比特币是区块链 1.0) 中提供了 EVM 的虚拟机,它的实现和 JVM 类似,基于栈、生成脚本编译成字节码来执行。知 识通用。(理论大于实际)

二、虚拟机的内存区域

虚拟机内存区域
从线程的角度看
在这里插入图片描述

运行时数据区

这个是抽象概念,内部实现依赖寄存器、高速缓存、主内存(具体要分析 JVM 源码 C++语言实现,没必要看) 计算机的运行=指令+数据,指令用于执行方法的,数据用于存放数据和对象的。个人理解为java进程运行的地址空间和指令加数据

线程私有的区域

1、程序技术器

较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。
如果线程正在执行的是一个 Java 方法,则指明当前线程执行的代字节码行数
如果正在执行的是 Natvie 方法,这个计数器值则为空(Undefined)
此内存区域是唯一一个不会出现 OutOfMemoryError 情况的区域。

2、虚拟机栈

概念:每个线程私有的,线程在运行时,在执行每个方法的时候都会打包成一个栈帧,存储了局部变量表,操作数栈,动态链接,方法出口等信息,然后放入 栈。每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。

栈的大小缺省为 1M,可用参数 –Xss 调整大小,例如-Xss256k

在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性之中,因此一个栈帧需要分 配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

**局部变量表:**顾名思义就是局部变量的表,用于存放我们的局部变量的。首先它是一个 32 位的长度,主要存放我们的 Java 的八大基础数据类型,一般 32 位就可以存放下,如果是 64 位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的 Object 对象,我们只需要存放它的一个引用 地址即可。(基本数据类型、对象引用、returnAddress 类型)

操作数据栈:存放我们方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的 java 数据类 型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的,操作数栈运行方法是会一直运行入栈/出栈的操作

动态连接:Java 语言特性多态(需要类加载、运行时才能确定具体的方法,后续有详细的讲解)

返回地址:正常返回(调用程序计数器中的地址作为返回)

3、本地方法栈

各虚拟机自由实现,本地方法栈 native 方法调用 JNI 到了底层的 C/C++(c/c++可以触发汇编语言,然后驱动硬件)

线程共享的区域

方法区

用于存储已经被虚拟机加载的类信息,常量(“zdy”,"123"等),静态变量(static 变量)等数据,可用以下参数调整: jdk1.7 及以前:-XX:PermSize;-XX:MaxPermSize;
jdk1.8 以后:-XX:MetaspaceSize; -XX:MaxMetaspaceSize
jdk1.8 以后大小就只受本机总内存的限制
如:-XX:MaxMetaspaceSize=3M

用于存储已经被虚拟机加载的类信息,常量(“zdy”,"123"等),静态变量(static 变量)等数据,可用以下参数调整: jdk1.7 及以前:-XX:PermSize;-XX:MaxPermSize;
jdk1.8 以后:-XX:MetaspaceSize; -XX:MaxMetaspaceSize
jdk1.8 以后大小就只受本机总内存的限制
如:-XX:MaxMetaspaceSize=3M

运行时常量池

存储符号引用等数据。
一个 java 类(假设为 People 类)被编译成一个 class 文件时,如果 People 类引用了 Tool 类,但是在编译时 People 类并不知道引用类的实际内存地址,因 此只能使用符号引用来代替。

而在类装载器装载 People 类时,此时可以通过虚拟机获取 Tool 类的实际内存地址,因此便可以既将符号 org.simple.Tool 替换为 Tool 类的实际内存地址, 及直接引用地址。

即在编译时用符号引用来代替引用类,在加载时再通过虚拟机获取该引用类的实际地址. 以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局是无关的, 引用的目标不一定已经加载到内存中
字面量
文本字符串 String a = “abc”,这个 abc 就是字面量 八种基本类型 int a = 1; 这个 1 就是字面量 声明为 final 的常量

三、直接内存

在这里插入图片描述
使用 Native 函数库直接分配堆外内存(NIO)
并不是 JVM 运行时数据区域的一部分,但是会被频繁使用(可以通过-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样,也会 出现 OOM 异常)

避免了在 Java 堆和 Native 堆中来回复制数据,能够提高效率
测试用例 JavaStack:设置 JVM 参数-Xmx100m,运行异常,因为如果没设置-XX:MaxDirectMemorySize,则默认与-Xmx 参数值相同,分 配 128M 直接内存超出限制范围

四、堆、栈辨析

  • 站在线程角度来看
    虚拟机栈、本地方法栈、程序计数器三个区域的生命周期和线程相同。
    线程共享区域:就复杂多了

  • 功能
     以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、
    double、boolean、char 等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
     而堆内存用来存储 Java 中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内
    存中;

  • 线程独享还是共享
    栈内存归属於单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可
    以理解成线程的私有内存。

    堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问

栈溢出
java.lang.StackOverflowError
指的是单个线程的栈数据被压满,例如死递归,会不断的创建栈帧压入虚拟机栈,直到虚拟机栈被打满。

虚拟机栈带给我们的启示:方法的执行因为要打包成栈桢,所以天生要比实现同样功能的循环慢,所以树的遍历算 法中:递归和非递归(循环来实现)都有存在的意义。递归代码简洁,非递归代码复杂但是速度较快。

OutOfMemoryError
整个栈区域被打满,不断的创建线程就可以达到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章