JVM内存结构,看这篇就够了

JVM JRE JDK

java程序在经过javac编译成.class文件后,需要JVM将这些字节码文件放到方法区翻译成机器码让执行引擎来执行,随后JVM调用操作系统函数完成操作。

JVM是一个将class翻译成机器码。在生成机器码的过程中需要依赖很多类库(jar包)。JVM标准加上这些类库就组成了JRE(java runtime environment)。
但是对代码的编译 反编译 调试 打包等等操作就需要用到JDK。

其实JVM是一个虚拟化的操作系统,在理解JVM时将他和Windows或Linux操作系统进行对比理解将会事半功倍。

现在常见的JVM实现有Hotspot 、 Jrocket、 J9、 TaoHbaoVM、 LiquidVM 、zing

JVM内存区域

运行时数据区:java虚拟机在执行java程序过程中将他管理的内存区域划分为若干个不同的数据区域。

jvm内存主要分为堆 、 方法区 、虚拟机栈 、本地方法栈 、 程序计数器,
还有一块就是直接内存,是没有被虚拟机虚拟化的操作系统上的其他内存;
按照与线程的关系划分可划分为线程私有区域和线程共享区域,如下图:
在这里插入图片描述

虚拟机栈

虚拟机栈是线程运行java方法所需要的数据、指令和返回地址。每个线程的方法执行时在虚拟机栈中就会打包成一个栈帧,每个方法的执行就是栈帧在虚拟机栈中进行出栈入栈操作。
虚拟机栈的缺省值为1M,可通过设置–Xss 调整大小。栈帧过多而一直不出栈的话这个空间会爆掉(java.lang.StackOverflowError)。
JDK1.8参数官方文档地址https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

下面介绍栈帧的四大区域:(局部变量表、操作数栈、动态连接、返回地址)
1. 局部变量表
用于存放方法中局部变量的,一般是32位的长度,如果是 64 位的就使用高低位占用两个也可以存放,主要存放八大基础类型数据,一些对象则存放他们的引用地址
2. 操作数栈
存放方法执行的操作数,本质上是JVM执行引擎的一个工作区,一开始是空的,在方法执行才会有数据。
3. 动态连接
处理java动态分派中多态的
4. 返回地址
正常返回:调用程序计数器中的返回地址
异常返回:调用异常处理器表来确定返回地址

程序计数器

作为当前线程字节码执行的行号指示器,分支 跳转 异常 循环 线程恢复等都依赖于程序计数器。在线程资源竞争发生时,各个线程的程序计数器就会记录下一条运行的指令。这个区域很小,不会发生OOM。
遇到本地方法时,不是jvm来执行,而是操作系统的程序计数器来记录,线程内程序计数器为空。

线程私有区域如图示:
在这里插入图片描述

本地方法栈

管理本地方法(native修饰)的调用,本地方法不是由java实现的,而是由C语言或C++实现,比如hashcode()

方法区

线程共享区域,他存储了每个类的结构信息,比如运行时常量池、字段、方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法,jdk1.7hotspot虚拟机使用永久代来实现,1.8使用元空间来实现。
元空间存储位置是本地内存。
元空间大小设置:
jdk1.7 及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
jdk1.8 以后(初始和最大值):
-XX:MetaspaceSize; -XX:MaxMetaspaceSize
元空间替代永久代的好处:jdk1.7中永久代指定的PermGen区大小为8M,不仅容易发生内存泄漏,而且PermGen区数据在fullGC时回收率偏低,在为PermGen区分配空间时大小很难确定。

JVM中最大的内存区域,我们申请的所有对象,基本都是在堆上存储的,我们创建的对象,分两种,一种是基本数据类型和普通对象。
对于普通对象,jvm在堆上创建对象,其他地方使用都是保存他的引用;
对于基本数据类型对象,当在方法体内声明时,是在栈上直接分配,其他地方都是在堆上分配。

堆大小参数:
-Xms:堆的最小值;
-Xmx:堆的最大值;
-Xmn:新生代的大小;
-XX:NewSize;新生代最小值;
-XX:MaxNewSize:新生代最大值

直接内存(堆外内存)

不属于运行时数据区,也不是 java 虚拟机规范中定义的内存区域。
它直接内存主要是通过 DirectByteBuffer 申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小;
其他堆外内存,主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。

运行时数据区

JVM 在操作系统上启动,申请内存,先进行运行时数据区的初始化,然后把类加载到方法区,最后执行方法。 方法的执行和退出过程在内存上的体现上就是虚拟机栈中栈帧的入栈和出栈。 同时在方法的执行过程中创建的对象一般情况下都是放在堆中,最后堆中的对象也是需要进行垃圾回收清理的。

辨析堆和栈

  1. 功能上
    1.1 以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char 等)以 及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
    1.2 而堆内存用来存储 Java 中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;
  2. 线程独享还是共享
    2.1 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
    2.2 堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
  3. 空间大小
    栈的内存要远远小于堆内存

虚拟机内存优化技术
栈帧之间数据共享:如果一个方法调用另一个方法传参数,这时两个栈帧之间就会存在各自的局部变量和操作数栈区域存在部分的共享,如图:
在这里插入图片描述
使用JHSDB工具查看的话也能看到存在栈帧间的空间数据共享。

内存溢出

- 栈溢出
一般出现java.lang.StackOverflowError时,大概率出现了无限递归
参数:-Xss1m
在这里插入图片描述

  • 堆溢出
    如果是内存溢出,可以通过调整堆空间大小 -Xms,-Xmx 参数。
    如果不是内存泄漏,那内存中的对象都是存活的,则应该检查jvm参数设置,和机器内存对比来调整;或者查看代码中某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行时的内存消耗。

  • 方法区溢出
    可能是运行时常量池溢出;
    或者是方法区中保存的 Class 对象没有被及时回收掉或者 Class 信息占用的内存超过了我们配置。
    --------但是Class对象要回收条件十分苛刻:

  • 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例;

  • 加载该类的 ClassLoader 已经被回收;

  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  • 本机直接内存溢出
    直接内存的容量可以通过 MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异常;
    由直接内存导致的内存溢出,一个比较明显的特征是在 HeapDump 文件中不会看见有什么明显的异常情况,如果发生了 OOM,同时 Dump 文件很小,可 以考虑重点排查下直接内存方面的原因。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值