& JVM1-虚拟机的前世今生和JVM内存区域
JVM学习总结1
1.1 JVM从编译到执行
1.1.1 Java程序的执行过程
一个Java程序,首先经过javac编译成.class文件,然后JVM将其加载到方法区,执行引擎将会执行这些字节码. 执行时,会翻译成操作系统相关的函数, JVM作为.class文件的翻译存在, 输入字节码,调用操作系统函数
过程如下: Java文件->编译器->字节码->JVM->机器码
JVM全称: Java Virtual Machine , 也就是我们耳熟能详的Java虚拟机,他能识别.class后缀的文件, 并且能够解析他的指令,最终调用操作系统上的函数,完成我们想要的操作!
1.1.2 JVM JRE JDK的关系
JVM是虚拟化的操作系统, 也就是我们通常说的java虚拟机, JVM会把class文件翻译成机器识别的代码, 但是需要注意的是, JVM不会自己生成代码, 需要大家编写代码, 同时需要很多依赖库,这时候就要用到JRE
JRE除了包括JVM之外, 还提供了很多类库(也就是我们常说的jar包,他可以提供一些即插即用的功能,比如读取或操作文件,连接网络等等之类的),
这些东西就是JRE提供的基础类库. JVM标准加上JRE基础类库, 就组成了Java的运行时环境, 也就是我们常说的JRE(Java Runtime Environment)
但对于程序员 来说, JRE是不够的. 我们写完代码要编译代码, 还需要调试代码, 还需要打包代码、有时候还需要反编译代码。所以我们会使用JDK,因为JDK还提供了一些非常好用的工具,比如javac(编译代码)、java(启动JVM)、jar (打包代码)、javap(反编译)等。这个就是JDK
1.2 从跨平台到跨语言
跨平台: 我们写一个类,在不同的操作系统上(linux、windows、macos)执行,效果是一样的,这就是JVM的跨平台性。
为了实现跨平台性,不同操作系统有对应的JDK版本
https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html
跨语言(语言无关性):JVM只识别字节码,所以JVM其实跟语言是解耦的,也就是没有关联,JVM运行不是翻译Java 文件,而是识别class文件,这个一般被称之为字节码。还有像Groovy、Kotiln、Scala等语言,他们其实也是编译成字节码,所以他们也可以在JVM上面跑,这个就是JVM的跨语言特征。Java的跨语言性一定程度上奠定了非常强大的java语言生态圈。
1.3 JVM 的发展(非重点)
常见的 JVM 实现
Hotspot:目前使用的最多的 Java 虚拟机。在命令行 java –version。它会输出你现在使用的虚拟机的名字、版本等信息、执行模式。
Jrocket:原来属于BEA 公司,曾号称世界上最快的 JVM,后被 Oracle 公司收购,合并于 Hotspot
J9: IBM 有自己的 java 虚拟机实现,它的名字叫做 J9. 主要是用在 IBM 产品(IBM WebSphere 和 IBM 的 AIX 平台上)
TaobaoVM: 只有一定体量、一定规模的厂商才会开发自己的虚拟机,比如淘宝有自己的 VM,它实际上是 Hotspot 的定制版,专门为淘宝准备的,阿里、天
猫都是用的这款虚拟机。
LiquidVM: 它是一个针对硬件的虚拟机,它下面是没有操作系统的(不是 Linux 也不是 windows),下面直接就是硬件,运行效率比较高。
zing: 它属于 zual 这家公司,非常牛,是一个商业产品,很贵!它的垃圾回收速度非常快(1 毫秒之内),是业界标杆。它的一个垃圾回收的算法后来被
Hotspot 吸收才有了现在的 ZGC。
2. JVM 整体知识模块
JVM 能涉及非常庞大的一块知识体系,比如内存结构、垃圾回收、类加载、性能调优、JVM 自身优化技术、执行引擎、类文件结构、监控工具等。
但是在所有的知识体系中,都或多或少跟内存结构有一定的关系:
比如垃圾回收回收的就是内存、类加载加载到的地方也是内存、性能优化也涉及到内存优化、执行引擎与内存密不可分、类文件结构与内存的设计有关
系,监控工具也会监控内存。所以内存结构处于 JVM 中核心位置。也是属于我们入门 JVM 学习的最好的选择。
同时 JVM 是一个虚拟化的操作系统,所以除了要虚拟指令之外,最重要的一个事情就是需要虚拟化内存,这个虚拟化内存就是我们马上要讲到的 JVM 的内存区域。
2.1 JVM 的内存区域
2.1.1 运行时数据区域
运行时数据区的定义:Java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
Java引以为豪的就是它的自动内存管理机制。相比于C++的手动管理内存、复杂难以理解的指针等,Java程序写起来就比较方便。
所以要深入理解JVM必须理解内存虚拟化的概念。
在JVM中,JVM内存主要分为堆、方法区、虚拟机栈和本地方法栈等。
同时按照与线程的关系也可以这样划分:
线程私有区域:一个线程拥有单独一份的区域
线程共享区域:被所有线程共享,且只有一份
这里还有一个直接内存:这个虽然不是运行时数据区的一部分,但是会被频繁使用。你可以理解成没有被虚拟化的操作系统上的其他内存(比如操作系统上有8G内存,被JVM虚拟化了3G,那么还剩下5G,JVM是借助一些工具使用这5G的内存的,这个内存部分称之为直接内存)
2.2 JAVA方法的运行与虚拟机栈
虚拟机栈是线程运行JAVA方法所需的数据、指令、返回地址。其实在我们实际的代码中,一个线程是可以运行多个方法的。比如:
先起一个main方法,
在main方法运行中调用A方法,
A方法中调用B方法,
B方法中调用C方法。
假设线程1来运行这段代码,线程1跑起来,就会有一个对应的虚拟机栈,同时在执行每个方法的时候都会打包成一个栈帧。
比如main开始运行,打包一个栈帧送入到虚拟机栈。
直到C方法运行完了,C方法出栈,
接着B方法运行完了,B方法出栈,
接着A方法运行完了,A方法出栈,
最后main运行完了,main出栈了。
这个就是JAVA方法运行对虚拟机栈的一个影响。虚拟机栈就是用来存储线程运行方法中的数据的。而每一个方法对应一个栈帧。
2.2.1 虚拟机栈
栈的数据结构:先进后出(FILO)的数据结构,
虚拟机栈的作用:在JVM运行过程中存储当前线程运行方法所需的数据,指令、返回地址。
虚拟机栈是基于线程的:哪怕你只有一个main()方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁的入栈和出栈,栈的生命周期和线程是一样的!
虚拟机栈的大小缺省值为1M,可用参数-Xss调整大小,列如-Xss256k。
参数官方文档(JDK1.8):https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
英
中
虚拟机栈里面存储的是栈帧
虚拟机栈里面存储的是栈帧
虚拟机栈里面存储的是栈帧
栈帧:在每个Java方法被调用的时候,都会创建一个栈帧,并入栈。一旦方法完成相应的调用,则出栈。
栈帧大体都包含四个区域:(局部变量表,操作数栈,动态连接,返回地址)
- 局部变量表
顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量)。首先它是一个32位的长度,主要存放我们的Java的八大基础数据类型,一般32位就可以存放下,如果64位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。 - 操作数栈
存放Java方法执行的操作数的,他就是一个栈,先进后出的栈结构,
操作数栈,就是用来操作的,操作的元素可以是任意的java数据类型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的。
操作数栈本质上是JVM执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不执行,操作数栈就是空的
↓↓↓↓↓操作数栈本质上是JVM执行引擎的一个工作区,这句话如何理解呢?↓↓↓↓↓
站在 电脑操作系统层面来看
操作系统包括了
CPU + 缓存 + 主内存;
JVM就是模拟板的操作系统,包括了
JVM执行引擎 + 操作数栈 + 堆、栈
操作数栈就是CPU缓存!!!!!!
操作数栈就是CPU缓存!!!!!!
操作数栈就是CPU缓存!!!!!!
CPU缓存是什么?↓↓↓↓↓
在计算机系统中,CPU高速缓存(英语:CPU Cache,在本文中简称缓存)是用于减少处理器访问内存所需平均时间的部件。在金字塔式存储体系中它位于自顶向下的第二层,仅次于CPU寄存器。其容量远小于内存,但速度却可以接近处理器的频率。
当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。 - 动态连接
JAVA语言特性多态 - 返回地址
正常返回:(调用程序计数器中的地址作为返回)
异常的话:(通过异常处理器表<非栈帧中的>来决定)
同时,这个虚拟机栈这个内存大小并不是无限大的,他有大小限制,默认情况下是1M
如果我们不断的往虚拟机栈中入栈帧,但是就是不出栈的话,那么这个虚拟机栈就会爆掉
2.2.2 程序计数器
较小的内存空间,当前线程执行的字节码的行号指示器;各个线程之间独立存储,互不影响。
程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,列如:分支、循环、跳转、异常、线程恢复等都依赖于计数器。
由于Java是多线程语言,当前执行的线程数量超过CPU核数时,线程之间会根据时间片轮询争夺CPU资源,如果一个线程的时间片用完了,或者是其他原因导致这个线程的CPU资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。
因为JVM是虚拟机,内部有完整的指令与执行的一套流程,所以在运行Java方法的时候需要使用程序计数器(记录字节码执行的地址或行号),如果是遇到本地方法(native方法),这个方法不是JVM来执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器,这个会记录本地代码的执行的地址,所以在执行native方法时,JVM中程序计数器的值为空(Undefined)
另外程序计数器也是JVM中唯一一个不会OOM(OutOfMemory)的内存区域。
2.2.3 为什么要有程序计数器?
对于操作系统来说,操作系统里面也有程序计数器。JVM中的程序计数器与操作系统中的程序计数器属于映射关系。
CPU时间片及CPU时间片轮转机制:1秒钟CPU可以干很多事情,CPU会将这个1秒钟进行切片,假设每一片对应的是1ms,切成了1000片,假设现在有很多线程在泡,其中一个线程,CPU分配了1个时间片代表1ms,这个线程执行到一半,时间片执行完了,CPU发现当前时间没有可用的时间片,这个线程必须要挂起或者阻塞,等下一个可用的时间片,此时CPU会把这个线程执行到哪一步给记录到程序计数器中,以便下一个时间片继续执行。(CPU时间片轮转机制)以确保CPU时间片轮转机制中程序的正常执行
同理 JVM中也需要程序计数器,确保java多线程 时间片轮转机制程序的正常执行
2.2.4 栈帧执行对内存区域的影响
对 class 进行反汇编 javap –c XXXX.class
字节码助记码解释地址:https://cloud.tencent.com/developer/article/1333540
在 JVM 中,基于解释执行的这种方式是基于栈的引擎,这个说的栈,就是操作数栈。
2.2.5 字节码的执行细节(重要)
示例:
package com.cemao.payment;
public class Person {
public int work() throws Exception{
int x = 1;
int y = 2;
int z = (x+y)*10;
return z;
}
public static void main(String[] args) throws Exception {
Person person = new Person();
person.work();
}
}
javac Person.java
javap -c Person.class
public int work() throws java.lang.Exception;
Code:
0: iconst_1 //将常量1压入操作数栈(第一行字节码执行完成)
1: istore_1 //将数值1从操作数栈存储到局部变量表
2: iconst_2 //将常量q压入操作数栈
3: istore_2 //将数值2从操作数栈存储到局部变量表
4: iload_1 //将下标是1的局部变量加载到操作数栈(此处不是数值为1,而是下标为1)
5: iload_2 //将下标是2的局部变量加载到操作数栈(此处不是数值为2,而是下标为2)
6: iadd //将操作数栈 栈顶的两个数值出栈(一般是栈顶的两个数值),放到JVM执行引擎(可以理解为CPU)中相加运算后,再自动入栈(重新加载到操作数栈中)
7: bipush 10 //将常量10加载到操作数栈
9: imul //将操作数栈 栈顶的两个数值出栈(一般是栈顶的两个数值),放到JVM执行引擎(可以理解为CPU)中相乘运算后,再自动入栈(重新加载到操作数栈中),此时数值为30
10: istore_3 //将30出栈,存储到局部变量表中
11: iload_3 //将局部变量表中下标为3的数值加载到操作数栈
12: ireturn //返回指令 只要是指令都是需要执行引擎(CPU)来执行,但是执行引擎的数据都是从操作数栈(缓存)里获取。所以上面iload_3的意义就是将下标为3的数值加载到操作数栈
此处 第4步
数值1 对应的局部变量下标为1
数值2 对应的局部变量下标为2
不要混淆
字节码位置/行号
左边的数字代表着,字节码的位置,也可以理解为行号。
为什么没有8?
这些行号只是简单的近似位,java底层本质是C C++来实现的,C C++使用内存会有一个概念叫做:偏移量
这些行号都是一些近似位,不会很准确
2.2.6 运行时数据区及 JVM 的整体内存结构
本地方法栈
本地方法栈跟Java虚拟机栈的功能类似,Java虚拟机栈用于管理Java函数调用,而本地方法栈则用于管理本地方法的调用,但本地方法并不是用Java实现的,而是由C语言实现的(比如Object.hashcode法)
本地方法是和虚拟机栈非常相似的一个区域,它服务的对象是native对象。你甚至可以认为虚拟机栈和本地方法栈是同一个区域。
虚拟机规范无强制规定,各个版本虚拟机自由实现,HotSpot直接把本地方法栈和虚拟机栈合二为一
方法区
方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、静态变量、常量、运行时常量池、字符串常量池等。
方法区是JVM对内存的“逻辑划分”。
在JDK1.7及之前很多开发者都习惯将方法区称为"永久代",是因为在HotSpot虚拟机栈中,设计人员使用了永久代来实现JVM规范的方法区。在JDK1.8及以后使用了元空间来实现方法区。
JVM在执行某个类的时候,必须先加载。在加载类(加载、验证、准备、解析、初始化)的时候,JVM会先加载class文件,而在class文件中除了有类的版本、字段、方法、和接口等描述信息外,还有一项是常量池(Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用。
字面量包括字符串(String a = "b")、基本类型的常量(final修饰的变量)。
符号引用则包括类和方法的全限定名(列如String这个类,它的全限定名就是java/lang/String)、字段的名称和描述以及方法的名称和描述符。
元空间
方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类的信息,而这个类还没有装入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
Java8 为什么使用元空间替代永久代,这样做有什么好处呢?
官方给出的解释是:
移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。
永久代内存经常不够用或发生内存溢出,抛出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。
符号引用
一个Java类(假设为People类)被编译成一个class文件,如果People类引用了Tool类,但是在编译时People类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。
而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,及直接引用地址。
即在编译时用符号引用来替代引用类,在加载时再通过虚拟机获取该引用类的实际地址。
以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局是无关的,引用的目标不一定已经加载到内存中。
常量池与运行时常量池
而当类加载到内存中后,JVM就会将class文件常量池中的内容存放到运行时的常量池中;在解析阶段,JVM会把符号引用替换为直接引用(对象的索引值)。
列如,类中的一个字符串常量在class文件中时,存放在class文件常量池中的;在JVM加载完类之后,JVM会将这个字符串常量放到运行时常量池中,并在解析阶段,指定该字符串对象的索引值。运行时常量池是全局共享的,多个类共用一个运行时常量池,class文件中常量池多个相同的字符串在运行时常量池只会存在一份。
javap -v People.class
静态常量池
堆
堆是JVM上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。堆一般设置成可伸缩的。
随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在Java中,就叫作GC(Garbage Collection)。
那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在Java类中存在的位置。
Java的对象可以分为基本数据类型和普通对象。
对于普通对象来说,JVM会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。
对于基本数据类型来说(byte\short\int\long\float\double\char),有两种情况。
当你在方法体内声明了基本数据类型的对象,他就会在栈上直接分配,其他情况,都是在堆上分配。
堆大小参数:
-Xms:堆的最小值。
-Xmx:堆的最大值。
-Xmn:新生代的大小。
-XX:NewSize:新生代的最小值。
-XX:MaxNewSize:新生代的最大值。
例如:
-Xmx256m
直接内存(堆外内存)
直接内存有一种更加科学的叫法,堆外内存。
JVM 在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的
内存也就是堆外内存。
它不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义的内存区域;如果使用了 NIO,这块区域会被频繁使用,在 java 堆内可以用
directByteBuffer 对象直接引用并操作;
这块内存不受 java 堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异
常。
小结:
1、直接内存主要是通过 DirectByteBuffer 申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小。
2、其他堆外内存,主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。
堆外内存的泄漏是非常严重的,它的排查难度高、影响大,甚至会造成主机的死亡。后续章节会详细讲。
同时,要注意 Oracle 之前 计划在 Java 9 中去掉 sun.misc.Unsafe API 。这里删除 sun.misc.Unsafe 的原因之一是使 Java 更加安全,并且有替代方案。
目前我们主要针对的 JDK1.8 ,JDK1.9 暂时不放入讨论范围中,我们大致知道 java 的发展即可。