从Java程序运行的角度分析JDK1.8下JVM的内存区域划分及变量存储

(内容归纳于网络,不妥之处可共同商讨)

Java程序运行

一个Java的源代码文件的运行如下图所示。当我们写好一个 .java程序,首先要经过编译。这里,就要说到编译器。
在这里插入图片描述

Java编译器

当安装好jdk后,打开bin目录,有两个重要的exe文件:javac.exe(编译器)和java.exe(.class文件执行器)。
在这里插入图片描述图片来源于网络
java编译器将java源码(.java文件)通过编译器(javac.exe)编译成字节码文件(.class文件)。然后就进入JVM。

JVM

说到JVM就要提到HotSpot。

HotSpot

JVM主要有三种:sun公司的HotSpot;Oracle的JrockitIBM的J9

我们常说的JVM事实上指的是Sun公司的HotSpot。

接下来进入正题:字节码文件进入JVM依次经过类加载器、JVM内存和执行引擎。其中最重要的就是JVM内存划分。但我们按照顺序先说类加载器。

类加载器

JVM程序在第一次主动使用某类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

我们开始加载类,首先把主类的类信息通过类加载器加载到运行时数据区的方法区内。但加载之前其实还有一步,那就是校验,这就说到了字节码校验器。

字节码校验器

字节码校验器也是JVM的组成部分,装载class文件之前或之后,class文件实际上还需要被校验,这就是class文件校验器。java虚拟机的class文件检验器在字节码执行之前对文件进行校验。字节码校验器,可以保证class文件内容有正确的内部结构。

校验之后,就可以把类信息加载进运行时数据区了。运行时数据区就是JVM内存。

类信息包括:构造器、成员变量、成员方法信息。

JVM内存五大区域

JVM内存有五大区域,就是耳熟能详的:堆、栈、方法区、本地方法栈、PC寄存器。

然而这五大区域也并不是都在JVM内存中,它们在计算机内存条中的分布如下:
在这里插入图片描述可以看出本地方法栈并不在JVM内存中,为什么还是JVM内存的五大部分之一?此处笔者也略有不解,望大佬解惑。

言归正传,我们继续加载主类。主类信息加载进运行时数据区后保存在方法区。
如:public class Main{ public static void main(String[] args) } 就在方法区

JDK1.8 下的方法区

为什么要说JDK1.8呢?因为JDK1.8中的方法区是在元空间中实现的。

元空间与永久代

【重点】方法区是一个逻辑概念,其具体实现为jdk1.8的元空间与jdk1.8之前的永久代。

  • 可以这样说 在JDK1.7及之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;
  • 而在JDK1.8中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中连续的,但物理上是不连续的,也叫非堆。

【重点】元空间和永久代的本质区别是:元空间并不在JVM的内存中,而是使用本地内存。

  • 之所以要使用元空间代替永久代是因为:

永久代的对象被垃圾回收的概率相对较小,用元空间将永久代与堆彻底分开,可以减少很多扫描永久代空间对象带来的时间开销。
类及方法的信息等比较难确定其大小,本地内存相比于JVM内存更大,没那么容易OOM,放入系统内存更为合适。

【元空间的大小设置】-XX:MaxMetaspaceSize=8M

JDK1.8的方法区到底存储了什么
  • JDK1.8中:元空间,类信息,运行时常量池。
  • JDK1.7中:永久代,类信息,运行时常量池。
  • JDK1.6中:永久代,类信息,运行时常量池,字符串常量池,静态变量(静态区)。

上述变化:在JDK1.7中将字符串常量池和静态变量(静态区)移动到了堆中。而JDK1.8中将永久代移动到了本地内存中,并取名元空间。值得一提的元空间是其它语言之前已经有的名词。

  • 字符串常量池和静态变量为什么移动到堆中
    字符串存在永久代中,但字符串常量创建之后可能用不了几次,只有full GC才会扫描清理到永久代,因而容易出现性能问题和内存溢出。频繁回收最好的地方在堆中。
    另一方面还没想到。

【补充】类结构信息里包含了常量池信息,加载到JVM之后就变成了运行时常量池。

回归主题,方法进入方法区后下一步就是执行。执行需要通过执行引擎。

执行引擎

执行引擎是Java虚拟机的核心组件之一。它的任务是将字节码指令解释/编译为对应平台上的本地机器指令。也就是说它可以将高级语言‘翻译’为机器语言。这里不做详述。
执行引擎执行时以main方法为入口开始执行。任何方法被调用时都需要入栈,在栈中开辟一片内存空间。每个线程都有自己独立的栈。

Java栈

Java栈由一个个栈帧组成,每个栈帧内都是一个方法,如下图
在这里插入图片描述
一个栈帧中包含局部变量表、操作栈、动态链接、返回地址等。

栈中存储了什么

【局部变量】

  • 基本数据类型是直接保存值
  • 引用类型保存指向其他对象的引用(地址)

【注意】变量名和值是两个不同的概念,两者都需要存储。局部变量的值必须手动初始化。

【补充一】为什么局部变量在栈中?

  • 这也很好理解,方法执行时,被分配的内存就在栈中,所以作为方法的变量的局部变量就在栈中。

【补充二】为什么局部变量不能够static修饰?

  • 由于static变量和局部变量存储的位置不一样。

回归主题,main方法入栈后,首先要new一个对象,这个new的对象在堆中。

Java堆

【new出来对象】

  • 凡是通过new生成的对象都存放在堆中。创建对象的时候,成员变量和成员方法参考方法区的类信息创建。成员变量随即初始化,成员方法则保存方法名以及方法区中方法的地址值。再将对象名和堆中所创建对象的地址值保存在栈中。

再回到栈中,有了对象之后我们要为对象的成员变量赋值。这时可以根据栈中对象的地址值找到堆中的对象的成员变量,并将初始值代替为所赋值。

再回到栈中,我们继续调用对象的方法。我们根据栈中的对象的地址值找到堆中的对象,再根据堆中的方法的地址值找到方法区中的方法信息。进而将方法入栈。

栈中调用完成一个方法后随即出栈,方法的基本数据类型或者对象的引用立即被释放;

至此,便完了一个.java源文件的执行。下面再说一下JVM内存中另外两个部分。

堆中存储了什么

【成员变量】

  • 基本数据类型是直接保存值
  • 引用类型保存指向对象的引用(地址)

【静态区】

  • 类变量(静态成员变量)

【字符串常量池】

  • 字符串常量池中保存的是字符串的引用。字符串在堆中以字节数组的形式存储。
本地方法栈

与Java栈类似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native
Method)服务的。

PC寄存器
  • 其本质是一个程序计数器,是一块很小的内存空间,几乎可以忽略不计。
  • 它存储了当前线程要执行的下一条指令地址。每个线程都有自己独立的PC寄存器。用于线程切换后能恢复到正确的执行位置。
  • 但如果正在执行的是native方法,则是未指定值(undefined),因为程序计数器不负责本地方法栈。

JVM的内容还有很多,详见

参考博文:
简单理解jdk1.8中的方法区
JAVA 方法区是在堆里面吗
Java内存图以及堆、栈、常量区、静态区、方法区的区别
JAVA String对象和字符串常量的关系解析

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

万码无虫

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

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

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

打赏作者

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

抵扣说明:

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

余额充值