理解JVM运行时数据区

 JVM与操作系统的关系

我们知道使用C/C++开发的程序,编译成二进制后,操作系统就能直接识别并运行,而Java程序编译成.class文件后,因为操作系统不认识这些.class文件,还需要通过JVM来翻译解析,再调用操作系统上的函数来执行。

从图中可以看到,有了 JVM 这个抽象层之后,Java 就可以实现跨平台了。JVM 只需要保证能够正确执行 .class 文件,就可以运行在Linux、Windows、MacOS 等平台上。

 JVM的运行过程

一个 Java 程序,首先经过 javac 编译成.class 文件,然后 JVM 将其加载到方法区,执行引擎将会执行这些字节码,执行时,会翻译成操作系统相关的函数,JVM 作为.class 文件的翻译存在,输入字节码,调用操作系统函数。

过程如下:Java 文件-->编译器-->字节码-->JVM->机器码

我们所说的 JVM,狭义上指的就 HotSpot(因为JVM有很多版本,但是使用最多的是HotSpot)。如非特殊说明,我们都以 HotSpot 为准。Java 之所以成为跨平台,就是由于 JVM 的存在。Java 的字节码,是沟通 Java 语言与 JVM 的桥梁,同时也是沟通 JVM 与操作系统的桥梁。

运行时数据区域  

定义:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

Java 引以为傲的就是它的自动内存管理机制,相比于 C++的手动内存管理、复杂难以理解的指针等,Java 程序写起来就方便的多。在 Java 中,JVM 内存主要分为程序计数器方法区虚拟机栈本地方法栈

程序计数器   

较小的内存空间,唯一不会OOM的区域。由于线程间的时间片轮转机制争夺CPU资源,被争夺的线程会暂停,由程序计数器记录执行到的字节码地址,下次获取到时间片后继续往下执行。

虚拟机栈

栈是一种后进先出(FILO)的数据结构。用来存储当前线程运行方法所需的数据,指令、返回地址 。Java 虚拟机栈是基于线程的。哪怕只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。

栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。

每个栈帧,都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址),栈的大小缺省为1M(取决于平台),可用参数–Xss调整大小,例如-Xss256k。

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

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

动态连接:Java语言特性多态(需要类运行时才能确定具体的方法)。

完成出口(返回地址):正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)。

本地方法栈

本地方法栈跟虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用 Java 实现的,而是由 C 语言实现的。

本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。甚至可以认为虚拟机栈和本地方法栈是同一个区域。虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot直接把本地方法栈和虚拟机栈合二为一 。

方法区/永久代

很多开发者都习惯将方法区称为“永久代”,其实这两者并不是等价的。HotSpot 虚拟机使用永久代来实现方法区,但在其它虚拟机中,例如,Oracle 的 JRockit、IBM 的 J9 就不存在永久代一说。因此,方法区只是 JVM 中规范的一部分,可以说,在 HotSpot 虚拟机中,设计人员使用了永久代来实现了 JVM 规范的方法区。

方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、静态变量、常量、运行时常量池、字符串常量池、即时编译期编译后的代码。

方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入 JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。在 HotSpot 虚拟机、Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地。

元空间大小参数: 

jdk1.7及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;

jdk1.8以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize

jdk1.8以后大小就只受本机总内存的限制(如果不设置参数的话)

JVM参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。

随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。

堆大小参数: 

-Xms:堆的最小值;

-Xmx:堆的最大值;

-Xmn:新生代的大小;

-XX:NewSize;新生代最小值;

-XX:MaxNewSize:新生代最大值;

例如- Xmx256m

直接内存

直接内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域;如果使用了NIO,这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作,这块内存不受java堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常。

深入理解堆和栈

HSDB ( Hotspot Debugger) ,JDK自带的检查调试Java进程的工具,具体用法

功能:

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

线程独享还是共享:

  • 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
  • 堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。

空间大小:栈的内存要远远小于堆内存

public class JVMObject {

    public final static String MAN_TYPE = "man"; //常量
    public static String WOMAN_TYPE = "woman"; //静态变量

    public static void main(String[] args){
        Teacher T1 =new Teacher();
        T1.setName("Mark");
        T1.setSexType(MAN_TYPE);
        T1.setAge(36);
        for (int i = 0; i < 15; i++) {
            System.gc(); //进行15次垃圾回收
        }
        T2 = new Teacher();
        T2.setName("JACK");
        T2.setSexType(WOMAN_TYPE);
        T2.setAge(20);
    }
}

当我们通过 Java 运行以上代码时,JVM 的整个处理过程如下:

  1. JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间。
  2. JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。
  3. 完成上一个步骤后, JVM 首先会执行构造器,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,静态变量和常量放入方法区
  4. 执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 Teacher 对象,对象引用T1 就存放在栈中。

内存溢出 

栈溢出

参数:-Xss1m, 具体默认值需要查看官网

HotSpot版本中栈的大小是固定的,是不支持拓展的。java.lang.StackOverflowError一般的方法调用是很难出现的,如果出现了可能会是无限递归。

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

OutOfMemoryError:不断建立线程,JVM申请栈内存,机器没有足够的内存。(一般演示不出,演示出来机器也死了)

堆溢出

内存溢出:申请内存空间,超出最大堆内存空间。

如果是内存溢出,则通过调大 -Xms,-Xmx参数。如果不是内存泄漏,就是说内存中的对象却是都是必须存活的,那么应该检查JVM的堆参数设置,与机器的内存对比,看是否还有可以调整的空间,再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行时的内存消耗。

方法区溢出

1. 运行时常量池溢出

2.方法区中保存的Class对象没有被及时回收掉或者Class信息占用的内存超过了我们配置。

注意Class要被回收,条件比较苛刻(仅仅是可以,不代表必然,因为还有一些参数可以进行控制):

  1. 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

本机直接内存溢出

直接内存的容量可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常;

由直接内存导致的内存溢出,一个比较明显的特征是在HeapDump文件中不会看见有什么明显的异常情况,如果发生了OOM,同时Dump文件很小,可以考虑重点排查下直接内存方面的原因。

虚拟机优化技术

编译优化技术——方法内联

方法内联的优化行为,就是把目标方法的代码原封不动的“复制”到调用的方法中,避免真实的方法调用而已。如下代码,在确定调用max的参数时,直接返回1>2结果,减少一次栈帧入栈。

栈的优化技术——栈帧之间数据的共享

在一般的模型中,两个不同的栈帧的内存区域是独立的,但是大部分的JVM在实现中会进行一些优化,使得两个栈帧出现一部分重叠。(主要体现在方法中有参数传递的情况),让下面栈帧的操作数栈和上面栈帧的部分局部变量重叠在一起,这样做不但节约了一部分空间,更加重要的是在进行方法调用时就可以直接公用一部分数据,无需进行额外的参数复制传递了。

使用HSDB工具查看栈空间也可以看到,栈的两根线重叠的地方即是共享数据。 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

永琪-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值