JVM与操作系统的关系
JVM也是一个软件,将上层的字节码解释成机器可识别的语言,因此不管在任何操作平台上,只要搭载了虚拟机,就可以运行程序,因此JVM拥有跨平台的性质。另一方面,不管是Java、Kotlin或者其它语言,只要编译成java字节码,都能被虚拟机所执行。因此JVM也拥有跨语言的性质。
JVM、JRE、JDK之间的关系
JVM整体运行过程
HelloWorld.java经过编译(javac)生成HelloWorld.class。JVM通过类加载器(classLoader)将class文件加载进运行时数据区,通过执行引擎进行解释执行,与操作系统完成交互
运行时数据区
Java虚拟机在执行Java程序的过程中会把他所管理的内存分为若干个不同的数据区域, 其中 程序计数器、虚拟机栈、本地方法栈 属于线程隔离数据区,方法区、堆 属于线程共享区域
程序计数器(PC)
指向当前线程正在执行的字节码指令的地址
作用:当线程进行调度时,当前线程失去CPU的执行权时,需要保存现场,即记录当前程序执行到哪一步了,当线程再次获得时间片可以继续执行时,便可以根据PC上所记录的指令地址,从当前位置继续往下执行。由于PC存的只是指令的地址,所以不会出现内存溢出的情况。
虚拟机栈
存储当前线程运行方法所需的数据、指令、返回地址
虚拟机栈中存放的是栈帧,每个方法在被执行都会作为一个栈帧被压入虚拟机栈,方法结束后出栈。遵守后进先出的规则
栈帧 内部被分为4个区域 :局部变量表(存放方法内定义的变量、参数),操作数栈(供数据进行操作的区域),动态连接,完成出口
方法区:
类信息
常量
静态变量
即时编译期编译后的代码
方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、静态变量、常量、运行时常量池、字符串常量池。
JVM 在执行某个类的时候,必须先加载。在加载类(加载、验证、准备、解析、初始化)的时候,JVM 会先加载 class 文件,而在 class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用。
字面量包括字符串(String a=“b”)、基本类型的常量(final 修饰的变量),符号引用则包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、字段的名称和描述符以及方法的名称和描述符。(参考:https://www.jianshu.com/p/6e63c505af2f)
例如,类中的一个字符串常量在 class 文件中时,存放在 class 文件常量池中的;在 JVM 加载完类之后,JVM 会将这个字符串常量放到运行时常量池中,并在解析阶段,指定该字符串对象的索引值。运行时常量池是全局共享的,多个类共用一个运行时常量池,class 文件中常量池多个相同的字符串在运行时常量池只会存在一份。
方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入 JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。在 HotSpot 虚拟机、Java7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而 Java8 版本已经将方法区中实现的永久代去掉了,并用元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地
Java堆
对象实例(几乎所有)
数组
堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。
那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置。
Java 的对象可以分为基本数据类型和普通对象。
对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。
对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。
从底层理解运行
当我们通过 Java 运行以上代码时,JVM 的整个处理过程如下:
JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间。
JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。
完成上一个步骤后, JVM 首先会执行构造器,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,静态变量和常量放入方法区
执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 Teacher 对象,对象引用 student 就存放在栈中。
执行其他方法时,具体的操作:栈帧执行对内存区域的影响。[栈帧执行对内存区域的影响]
深入辨析堆和栈
功能
以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;
线程独享还是共享
栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
空间大小
栈的内存要远远小于堆内存