(内容归纳于网络,不妥之处可共同商讨)
文章目录
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的Jrockit和IBM的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对象和字符串常量的关系解析