运行JVM字节码的工作由解释器来完成 (Java解释器是JVM的一部分) 整个解释过程分为
- 代码装入 (Class Loader)
- 代码校验
- 代码执行 Java字节码的执行方法:解释器每次解释并执行一小段代码来完成Java字节码程序的所有操作
- JVM是一种用于计算设备的规范
- JVM包括一套字节码指令集,一组寄存器,一个栈,一个垃圾回收堆和一个存储方法域
- JVM是可运行Java代码的假象计算机
- JVM存在于内存中,根据不同的CPU翻译成不同的语言
运行一个Java程序的同时诞生了一个JVM实例,如果运行三个就会产生三个JVM实例。
- 为实现平台无关性
- 是运行java程序的软件环境(Java解释器就是Java虚拟机的一部分)
- (Java程序通过JVM实现跨平台,JVM本身不跨平台)
就像接口似的,只要根据JVM规格描述将解释器移植到特定的计算机上,就能保证编译过的Java代码能够在该系统上运行。 启动一个Java程序时同时诞生一个虚拟机实例,如果运行是那个Java程序,会得到三个Java虚拟机实例。
寄存器(用于保存机器的运行状态)
- PC Java程序计数器
- optop 指向操作数栈顶端的指针
- frame 指向当前执行方法的执行环境的指针
- vars 指向当前执行方法的局部变量区第一个变量的指针
JVM内部有两种线程 守护线程,如JVM使用过程中的垃圾处理 非守护线程,如main
java命令其实代表着Java SDK的Java虚拟机
java xx parm1,parm2
JVM中的boolean Java源码编译成字节码时,会用int或者byte来表示boolean(false-0, tyre-非0整数) JVM中额外的基本类型 returnAddress 指向一条VM指令的操作码 用于实现finally语句 JVM中的引用类型称为引用 reference 分为 类类型 接口类型 数组类型 null表示无引用 (JVM最基本的数据单位 Word 字)
JVM两种类加载器 启动类加载器 用户自定义加载器
Runtime Data Area可以说是存放各式各样数据的内存区域 JVM中Runtime Dae Area中有如下几个数据结构:
- Stack Java栈
- Heap Java堆
- Method Area 方法区
- PC Register 程序计数器
- Native Method Area 本地方法区
每个JVM实例都有一个方法区以及一个堆,所有线程共享 一个新线程被创建时,将得到比它自己的PC(寄存器)以及一个Java栈
Java栈中存储着该线程中Java方法调用的状态(非本地方法) ————局部变量被调用时传进来的参数,方法的返回值,运算的中间结果。
Java栈的基本元素是Java栈帧,而Java栈帧组成如下:
- 局部变量区
- 操作数栈
- 帧数据区
一个方法的调用至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
Java栈的操作:方法执行时压入栈,方法返回或异常时弹出栈。
-
局部变量区 存放编译期可知的各种基本类型,对象引用和returnAddress类。每个方法使用一个固定大小的局部变量集,32位。方法的参数顺序严格固定,局部变量任意。 局部变量区内数据,如果是double型的变量则占有n,n+1两个位置的空间。 byte,short,char被转换成int型。 数据访问通过索引访问。
-
帧数据区 用以支持常量池解析(比如需要使用常量等非局部变量的数据),用以支持正常的方法返回,异常派发机制。
-
运行环境区 包含的信息用来实现动态链接,正常方法的返回,异常和错误的传播
-
操作数栈区 机器指令从操作数栈中获取操作数 iadd指令取栈顶两指令相加
各线程共享,存储被虚拟机加载的类信息,常量,静态变量。
当VM装载某个类型时,它使用类加载器定位相应的class文件,连接虚拟机提取其中的类型信息,并将这些信息存储到方法区,该类型的类型变量存储在方法区中。方法去区大小不固定,空间不连续,也可被堆垃圾回收。
- 具体存储的类型信息: 1.1 全限定名,超类,超接口的全限定名 1.2 域的类型(引用类型) 1.3 访问修饰符
- 常量池,也称为运行时常量池,存储各种字面量(如字符串"123")和符号引用。常量池也是一种堆结构。
- 字段信息(名,类型,修饰符)
- 方法信息(名,返回类型,参会类型以及顺序,修饰符)
- 类变量(静态变量)
- 指向类加载器的引用
- 指向Class实例的引用
- 方法表
符号引用 一个类在编译期不知道所引用类的内存地址,只能用符号引用来表示
一个字长(32位),通过改变计数器的值来选取下一条要执行指令的字节码。线程私有,一个线程拥有一个PC,如果正在执行的是本地方法,PC值为undefined PC中存储着指向下一条被将被执行的指令,其实指的是线程或者说是具体的某个方法。 存储着当前线程锁执行的字节码指令的地址,字节码解释器通过改变程序计数器的值获取下一条执行字节码的指令。
线程共享,存放对象实例(包括数组)。逻辑上连续。也称GC堆。
JVM虚拟机没有规定Java对象是如何在堆中表示。但是声明了对象应该包含的基本数据,所属类,超类的实例变量,指向方法区的指针。
因此有如下设计 设计一: 把堆分为句柄池,对象池。句柄池中的条目含有分别指向对象池(实例数据)和方法区的指针。 好处,对象的添加删除比较快。
设计二: 堆不分区,直接包含对象实例数据,对象实例数据中包含一个指向方法区的指针。 减少了一次指针定位的开销,对象的访问比较快。但是添加删除相较慢。
数组在堆中的表示,数组也有一个相关联的Class实例,数组的长度,数据存储在堆中。
关于Runtime Data Area 有这么一种理解方式。 这个区域分为两个部分,分别是 1 多线程共享的运行时数据区 和 2 线程私有的运行时数据区
对于多线程共享的运行时数据区,存储着类数据和类实例。类数据便是Heap,而类实例就是Method Area了。从逻辑上讲方法区是堆的一部分。
对于线程私有的运行时数据区,分别被PC和Java Stack占有,Jaca Stack由栈帧组成,表示了方法的执行状态,因此栈帧中存着局部变量,同时内部还有一个操作数栈。
Java命令提供了 -Xms -Xmx 这两个非标准选项来调整堆的初始大小和最大限制。另外 -Xss 设置线程栈的空间大小。
PC的功能
do{
取一个操作符字节;
根据操作符的值执行一个动作;
}while(程序未结束)
字节码(class文件中的)是运行在虚拟机上的机器码,字节码中存放着编码后的JVM指令。意思是说JVM指令编码 -> 字节码。
Java指令: 操作码(1byte) + 操作数(可以为空) 也就是说Java指令最多只有256条
Java指令集有205条指令。
...B2 00 02... 这里B2代表了操作码,00代表了助记符,02代表操作数。 表示的意思是对常量池的第二个常量执行B2操作。
类装载器分为启动类装载器和用户自定义装载器,其中启动类装饰器是JVM实现的一部分,而用户自定义是Java程序的一部分。必须派生自java.lang.ClassLoader
类的装载方式分为隐式装载和显式装载。 隐式装载指的是遇见new方式生成对象时,隐式调用类加载器加载对应对象到JVM中。 显式装载值得是通过class.forname等方法显式加载需要的类。
AppClassLoader extend ExtClassLoader extend BootstrapLoader JVM启动时初始化启动类加载器,而后启动类加载器初始化另外两个加载器。
委托模型机制,全盘负责委托机制,双亲委托模型 加载某个类时先请求父类帮忙载入,如果找不到再自己尝试加载。 除了启动类装载器以外的每一个装载器都有一个"双亲"装载器。
- 父类装载器有其唯一标识的命名空间,一个Java程序可以多次装载具有同一全限定名的多个类。类名前加上装载器名组成全限定名。
分带收集算法 将堆分为三个区,分别是新生代,老生代,以及方法区对应的永生存储区。 而新生代又分为三个部分,伊甸区(Eden),幸存者1区(From Survivor),幸存者2区(To Survior)。 简介:创建的一个新的对象首先出现在伊甸区,一段时间后考虑将一些活跃的对象移至幸存,在经过一定的筛选幸存区的对象最终移到老生代,也就是养老区。而未被移动的对象会被垃圾回收。
GC算法面临一个问题——如何找出程序中不可达的内存区(即内存块不可引用)
如何确定对象存活? 方法一:引用计数法,即记录下内存快被引用的次数,这个方法简单直接,但需要编译器配合——生成新的指令来配合计算。如果存在循环引用,玄幻引用消除不了。
方法二:根搜索算法
- 暂停整个应用程序,从根部扫描整个堆,判断是否有跟引用,才有分收集。
有这么一个定义:根引用(GC roots),什么叫做根引用呢。
- 栈帧中局部变量表中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native)引用的对象
内存不可达——无法通过引用链找到根引用
- 怎样判断是否有老生代的对象引用了新生代的对象。 使用卡片标记策略,将老生代对象分块,划分之后每一块叫卡片,使用卡表维护每一块的状态。 当有老生代对象引用或者释放了新生代对象引用,则将卡片状态标记为脏状态,扫描的时候也扫描脏状态的卡片。
- 如何判断是否有引用指向对象。
引用类型 强引用:正常声明的引用 软引用:如果内存空间不够了,这些鸡肋引用才会被清理。 弱引用:弱引用被GC之后肯定会被回收。
真正宣告一个对象死亡要宣告两次,如果在根搜索时没有发生与GC Roots相连的引用链: 将会被第一次标记并进行一次筛选,而筛选条件是否有必要执行finalize()方法。
没必要的话,说明对应类没有覆盖finalize方法,或者finalize方法已经被JVM执行过了。 如果有必要的话,该对象会被放在F-Queue队列中。 稍后会有一条自动建立低优先级的Finalizer线程去执行这个方法。 随着,GC会对F-Queue队列进行第二次小规模标记,如果对象在finalize方法中成功"复活"(重新连上引用链)自己的话, (例如将自己的this复赋值给某个类变量),那么该引用就移出标记集合,否则就被垃圾回收了。
分代收集策略
1.标记清除收集器(Mark-Compact),停下所有工作,从根部扫描并标记每个活跃的对象,标记结束后清除那些没有被标记的对象。 2.复制收集器(Copying)将内存分为两块大小一样的空间,某时刻只有一个空间存在活跃的状态,当活跃的空间满的时候,GC会将活跃的对象复制到未使用的空间(优点是只要扫描可达到的对象,但增加了额外空间的消耗) 3.标记整理收集器(Mark-Swap)结合二者,首先扫描活跃对象并标记,然后清除未标记的对象,将活跃对象推到底部,大大减少了内存碎片。
老生代:Mark-Swap 新生代:Mark-Sweep
- Serial Collector 只有一个线程进行垃圾收集的GC Serial Copying Collector 用于新生代 Serial Mark-Compat Collector 用于老生代
- Parallel Collector 应对多线程 Parallel Copying Collector 用于老生代 Parallel Mark-Compat Collector 用于老生代 Parallel Scavenging Collector 适合内存比较大,高吞吐率的
- Concurrent Collector 并发GC
方法区也能被垃圾回收
-
永久代
“PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。
前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”。
而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space” ,
由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。 -
元空间
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小: