详细讲解JVM(Hotspot)运行时数据区

Posted 晏霖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详细讲解JVM(Hotspot)运行时数据区相关的知识,希望对你有一定的参考价值。


曾经有人关注了我

后来他有了女朋友

比这个还漂亮



前言

很多人对于学习jvm都有些抵触,枯燥的概念很难理解,工作中也用不到…等等原因,一些人从入门到放弃,或半途而废。

做一件事一定要有目的,否则就失去了意义,无论为了什么最终目的是做好它。我当初学习jvm就是为了面试,我发誓我不会再让虚拟机的问题难住,我要让那些面试官看看,于是我拿着周志明的《深入理解Java虚拟机》一遍一遍的看,半年的时间,80%的内容我至少看4遍。东西学了不用就会忘,过程中看了忘,忘了又看,非常痛苦,但是我在其中也慢慢发现了技巧,学习jvm最好是拿生活中的事物关联起来会容易理解,慢慢的你会苦中作乐,理解了一个难点就会有信心理解下一个。学习的过程是痛苦的,但是学成之后就会很快乐。

这次我在前言部分说了很多,其实就是想告诉各位,不仅限于学JVM,学任何东西只要坚持下来都会有收获,对于像我一样喜欢在书上获取知识的人来说,碰到一个很绕口的概念时我们就多读,隔段时间翻回来接着多读,慢慢就会理解,切记要有耐心。

我这么笨的人都在坚持,你为什么不呢。

我也会在业余时间持续更新这方面的内容。


正文

本文针对Java8讲解,如有出入请指出。

本文讲解虚拟机内存模型以及这里面到底都是干什么的。

JVM运行时数据区分为五块,不少网友看到的资料有说六块的,那是因为多了一个叫直接内存的地方,这个区域并不属于运行时数据区的一部分,也不是java虚拟机规范中的区域,他的大小不受java堆大小限制,但是他也占用着物理机的内存,他是实际存在的,说直白他是堆与本地方法之间沟通的桥梁,储存的数据也是和堆与本地方法相关的。

还有一点需要注意的是,Java 7之后,常量池已经不在方法区中进行分配了,而是移到了堆中,Java 8之后方法区(持久代)已经被永久移除,取而代之的是Metaspace


言归正传,下面详细讲解五块内存区域。



这块区域是虚拟机管理内存最大的一块,是线程共享的区域,在虚拟机启动的时候创建,他主要是存放对象实例、数组、静态变量以及常量池的,这块区域也是GC最重要的区域(关于这部分GC后面会有文章讲解)。堆在生成时为了提高效率问题分配了缓冲区,提前给对象预留了空间,如果空间不足会根据CAS算法扩充。堆的大小在启动前可以指定,通过 -Xmx和-Xms控制最大和最小内存,如果运行中超过了控制范围就会抛出OOM。


Metaspace


Metaspace并不在虚拟机内存中而是使用本地内存。此区域是线程共享区域。Metaspace用来存储加载的类信息、编译器编译后的代码等数据。

jvm把方法区移除也是为了利用本地内存空间来满足类元数据信息的存储,尽量减少OOM。比方说一台Linux服务器有8G内存,内核空间用掉2G,JVM应用用掉2G,理论上Metaspace有4G可以用(实际肯定不会有这么多)  ,如果在java7之前,方法区还需要在jvm所占的2G再掠取一块内存,通常都是几百兆,可见是不是不大够。当然不止这一个优点,比如提升GC效率(因为方法区GC条件苛刻,性价比不高)等,其实移除永久代最重要的原因是hotspot为了和JRockit进行融合而做的努力,因为JRockit用户并不需要配置持久代(因为JRockit就没有持久代)。

自从oracle公司收购了sun,就自己出了一个Java虚拟机(JRockit JVM)。

Metaspace通过参数 -XX:MetaspaceSize -XX:MaxMetaspaceSize进行限制,超过会出现OOM,JVM会忽略PermSize和MaxPermSize这两个参数

Metaspace相关JVM参数


参数名 作  用
MetaspaceSize 初始化的Metaspace大小,控制Metaspace发生GC的阈值。GC后,动态增加或者降低MetaspaceSize,默认情况下,这个值大小根据不同的平台在12M到20M之间浮动
MaxMetaspaceSize 限制Metaspace增长上限,防止因为某些情况导致Metaspace无限使用本地内存,影响到其他程序,默认为4096M
MinMetaspaceFreeRatio 当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数,那么虚拟机增长Metaspace的大小,默认为40,即70%
MaxMetaspaceFreeRatio 当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放部分Metaspace空间,默认为70,即70%
MaxMetaspaceExpanison Metaspace增长时的最大幅度,默认值为5M
MinMetaspaceExpanison Metaspace增长时的最小幅度,默认为330KB



栈是线程私有的,所以他的生命周期与线程相同,他描述的是java的内存模型,方法执行就会创建一个栈帧,栈帧是方法运行的基础数据结构,用于存放局部变量表、操作数栈、动态链接、方法出口等,方法调用与结束的过程就是栈帧入栈与出栈的过程。在编译代码的时候,栈帧需要多大的局部变量表,还有栈的深度就已经确定了,所以栈帧需要多大的内存不会受运行时数据改变而改变。栈是一个先进后出的结构,好比一个桶,一个人向桶里扔东西,一个人从桶口拿东西。


局部变量表


操作数栈

操作数栈即方法操作使用的,32位数据类型占用的容量为1,64位数据类型占用的容量为2。方法刚开始是空的,随着方法执行会有各种字节码指令往操作数栈中写入和提取内容,也就是入栈和出栈操作。例如一个方法计算两个数相加,先把两个int类型的数据出栈执行iadd指令,然后把结果入栈

另外这部分区域由于被jvm优化后会出现相邻两个栈帧重合区域,可以拿上面栈的图来比喻一下。重合区域,其实是用来储存两个相邻栈公用的局部变量表的数据,免去重复数据来回复制,提高空间利用率和效执行率。


动态链接


每个方法都会引用了很多变量,引用的这些变量首先是通过符号引用,符号引用的东西不能直接用啊,所以动态链接就是把这些符号引用的数据变成直接引用,这种转化叫动态分派。

我们知道jvm在类加载阶段的过程中有一步就是把符号引用变成直接引用,或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分也就是动态链接的作用就是将在每一次运行期间转化为直接引用,这部分称为动态连接。(静态分派,动态分派)后面详细讲解类加载。


方法退出的过程等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。


程序计数器

程序计数器是一个记录着当前线程所执行的字节码的行号指示器,可以理解为一个指针,告诉程序按照我指示的顺序进行执行。

后面文章我会让大家看到java文件翻译成字节码是什么样子的,就清楚知道程序计数器工作的环境。

程序计数器占用内存很小,可以忽略不计,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域,并且此区域是线程独享。

JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,这时候就需要程序计数器来记录某个线程的字节码执行位置,如果虚拟机是单线程也就没必要用程序计数器记录每个线程的位置了。

以上是关于详细讲解JVM(Hotspot)运行时数据区的主要内容,如果未能解决你的问题,请参考以下文章

JVM 专题六:运行时数据区概述

JVM_03 运行时数据区1-[程序计数器+虚拟机栈+本地方法栈]

20张图助你了解JVM运行时数据区,你还觉得枯燥吗?

Jvm(11),运行时数据---独占区---本地方法栈

JVM -- Java虚拟机自动内存管理机制(运行时数据区域HotSpot虚拟机对象探秘)

JVM -- Java虚拟机自动内存管理机制(运行时数据区域HotSpot虚拟机对象探秘)