JVM-随笔

1、结构图

2、解析

一、方法区(Method Area)
Object Class Data(加载类的类定义数据) 是存储在方法区的。
除此之外,常量、静态变量、JIT(即时编译器)编译后的代码也都在方法区。
正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 Non-Heap。
方法区也可以是内存不连续的区域组成的,并且可设置为固定大小,也可以设置为可扩展的,这点与堆一样。
垃圾回收在这个区域会比较少出现,这个区域内存回收的目的主要针对常量池的回收和类的卸载。
*************************************************************************************************************
 二、Java 堆(The Java Heap)
Heap(堆)是JVM的内存数据区。
Heap 的管理很复杂,是被所有线程共享的内存区域,在JVM启动时候创建,专门用来保存对象的实例。
在Heap 中分配一定的内存来保存对象实例,实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,
并不保存对象的方法(以帧栈的形式保存在Stack中),在Heap 中分配一定的内存保存对象实例。
而对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例,是垃圾回收的主要场所。
java堆处于物理不连续的内存空间中,只要逻辑上连续即可。

*************************************************************************************************************
三、Java 栈(The Java Stack)
(0)概述
当一个线程启动时,Java 虚拟机会为他创建一个 Java 栈。
Java 栈用一些离散的frame 类纪录线程的状态。
Java 虚拟机对 Java 栈的操作只有两种:压入和弹出 frames。
线程中正在执行的方法被称为当前方法(current method),当前方法所对应的 frame 被称为当前帧(current frame)。
定义当前方法的类被称为当前类(current class),当前类的常量池被称为当前常量池(current constant pool.)。
当线程执行时,Java 虚拟机会跟踪当前类和当前常量池。当线程操作保存在帧中的数据时,他只操作当前帧的数据。
当线程调用一个方法时,虚拟机会生成一个新的帧,并压入线程的 Java 栈,这个新的帧变成当前帧。
当方法执行时,他使用当前帧保存方法的参数、本地变量、中间结构和其他数据。
方法有两种退出方式:正常退出和异常推出。
无论方法以哪一种方式推出,Java虚拟机都会弹出并丢弃方法的帧,上一个方法的帧变 为当前帧。
所有保存在帧中的数据都只能被拥有它的线程访问,线程不能访问其他线程的堆栈中的数据。
所以,访问方法的本地变量时,不需要考虑多线程同步。
和方法区、堆一样,Java 栈不需要连续的内存空间,它可以被保存在一个分散的内存空间或者堆上。
栈具体的数据和长度都有 Java 虚拟机的实现者自己定义。
一些实现可能提供了执行堆栈最大值和最小值的方法。

(1)栈帧(The Stack Frame)
栈帧包含三部分:本地变量、操作数栈和帧数据。
本地变量和操作数栈的大小都是字节为单位的,他们在编译就已经确定。
帧数据的大小取决于不同的实现。
当程序调用一个方法时,虚拟机从类数据中取得本地变量和操作数栈的大小,创建一个合适大小和帧,然后压入 Java 栈中。

(1.1)本地变量(Local Variables)
本地变量在 Java 栈帧中被组织存放在一个从 0 计数的数组,指令通过提供他们的索引从本地变量区中取得相应的值。
Int,float,reference, returnValue 占一个字节,byte,short,char 被转换成 int 然后存储,long 和 doubel 占两个字节。
指令通过提供两个字节索引中的前一个来取得 long,doubel 的值。
比如一个 long 的值存储在索引 3,4 上,指令就可以通过 3 来取得这个 long 类型的值。
本地变量区中包含了方法的参数和本地变量。编译器将方法的参数以他们申明的顺序放在数组的前面。
但是编译器却可以将本地变量任意排列在本地变量数组中,甚至两个本地变量可以公用一个地址,比如,当两个本地变量在两个不交叠的区域内,就像循环变量 i,j。
虚拟机的实现者可以使用任何结构来描述本地变量区中的数据,虚拟机规范中没有定义如何存储 long 和 doubel。

(1.2)操作数栈(Operand Stack)
跟本地变量一样,操作数栈也被组织为一个以字节为单位的数组。
但是不像本地变量那样通过索引访问,而是通过 push 和 pop 值来实现访问的。
如果一个指令 push 一个值到栈中,那么下一个指令就可以 pop 并且使用这个值。
操作数栈不像程序计数器那样不可以被指令直接访问,指令可以直接访问操作数栈。
Java 虚拟机是一个以栈为基础,而不是以寄存器为基础的,因为它的 指令从栈中取得操作数,而不是同寄存器中。
当然,指令也可以从其他地方取得操作数,比如指令后面的操作码,或者常量池。
但是 Java 虚拟机指令主要是从 操作数栈中取得他们需要的操作数。
Java 虚拟机将操作数堆栈视为工作区,很多指令通过先从操作数堆栈中 pop 值,在处理完以后再将结果 push 回操作数堆栈。
一个 add 的指令执行过程如下图所示:
先执行iload_0 和 iload_1 两条指令将需要相加的两个数,从本地方法区中取出,并 push 到操作数堆栈中;
然后执行 iadd 指令,先 pop 出两个值,相加,并将结果 pusp 进操作数堆栈中;
最后执行 istore_2 指令,pop 出结果,赋值到本地方法区中。

通俗来讲:操作数栈就是保存操作计算时的值,如 3+5 ,就是保存值3和值5。
详情见:https://www.cnblogs.com/chendongfly/p/4189707.html

(1.3)帧数据(Frame Data)

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。
它是虚拟机运行时数据区中的虚拟机栈的栈元素。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了。
因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。

【局部变量表】:
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot)为最小单位,每个变量槽都可以存储32位长度的内存空间,例如boolean、byte、char、short、int、float、reference。
对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写。
在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用。
(在方法中可以通过关键字this来访问到这个隐含的参数)。
其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot。
为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,也就是说当PC计数器的指令指已经超出了某个变量的作用域(执行完毕),那这个变量对应的Slot就可以交给其他变量使用。 
优点 : 节省栈帧空间。 
缺点 : 影响到系统的垃圾收集行为。(如大方法占用较多的Slot,执行完该方法的作用域后没有对Slot赋值或者清空设置null值,垃圾回收器便不能及时的回收该内存。)

reference(对象实例的引用)一般来说,虚拟机都能从引用中直接或者间接的查找到对象的以下两点 : 
a、在Java堆中的数据存放的起始地址索引。 
b、所属数据类型在方法区中的存储的类型数据。


【动态连接】
在Class文件中的常量池中存有大量的符号引用。
字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。
这些符号引用一部分在类的加载阶段(解析)或第一次使用的时候就转化为了直接引用(指向数据所存地址的指针或句柄等),这种转化称为静态链接。
而相反的,另一部分在运行期间转化为直接引用,就称为动态链接。
动态扩展就是在运行期可以动态修改字节码,也就是反射机制与cglib


【方法返回地址】
当一个方法开始执行后,只有2种方式可以退出这个方法 :
---方法返回指令 : 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。
---异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。
无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。

【操作数栈】
操作数栈和局部变量表一样,在编译时期就已经确定了该方法所需要分配的局部变量表的最大容量。
操作数栈的每一个元素可用是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型占用的栈容量为2。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。
例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的。
索然两个栈帧作为虚拟机栈的元素是完全独立的,但是虚拟机会做出相应的优化,令两个栈帧出现一部分重叠。
栈帧的部分操作数栈与上一个栈帧的局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数复制传递。

*************************************************************************************************************
四、程序计数器PC(The Program Counter)
每一个线程开始执行时都会被创建一个程序计数器。
程序计数器只有一个字长(word),所以它能够保存一个本地指针和 returnValue。
当线程执行时,程序计数器中存放了正在执行指令的地址,这个地址可以是一个本地指针,也可以是一个从方法字节码开始的偏移指针。
如果执行本地方法,程序计数器的值没 有被定义。

和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。
但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。
比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。

*************************************************************************************************************
五、本地方法区
本地方法是由其它语言编写的,编译成和处理器相关的机器代码。
简单地讲,一个Native Method就是一个java调用非java代码的接口。
为什么要使用Native Method

java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
与java环境外交互:
有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。
本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。
与操作系统交互:
JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。
然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。
通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
Sun's Java
Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。
jre大部分是用java实现的,它也通过一些本地方法与外界交互。
例如:类java.lang.Thread 的 setPriority()方法是用java实现的,但是它实现调用的是该类里的本地方法setPriority0()。
这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority() API。
这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。

JVM怎样使Native Method跑起来:
我们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会回载一次。
在这个被加载的字节码的入口维持着一个该类所有方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。
如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。
这些实现在一些DLL文件内,但是它们会被操作系统加载到java程序的地址空间。
当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。
这些DLL的加载是通过调用System.loadLibrary(String filename)或System.load(String filename)方法实现的。

最后需要提示的是,使用本地方法是有开销的,它丧失了java的很多好处。如果别无选择,我们可以选择使用本地方法。

六、其他相关

-------------------------------------------------------元空间------------------------------------------------------------------------------------------------------------------------------------------
1.1、为什么移除持久代

--它的大小是在启动时固定好的——很难进行调优。-XX:MaxPermSize,设置成多少好呢?
--HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,同时它对应用不透明,且是非强类型的,难以跟踪调试,还需要存储元数据的元数据信息(meta-metadata)。
--优化Full GC:每一个回收器有专门的元数据迭代器。
--可以在GC不进行暂停的情况下并发地释放类数据。
--使得原来受限于持久代的一些改进未来有可能实现。

1.2、移除持久代后,PermGen空间的状况

--这部分内存空间将全部移除。
--类的元数据信息还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。
--String 类型直接存在堆中。
--JVM的参数:PermSize 和 MaxPermSize 会被忽略并给出警告(如果在启用时设置了这两个参数)。


1.3、元空间的特点

--充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
--每个加载器有专门的存储空间
--只进行线性分配
--不会单独回收某个类
--省掉了GC扫描及压缩的时间
--元空间里的对象的位置是固定的
--如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉

1.4、元空间的内存分配模型

--绝大多数的类元数据的空间都从本地内存中分配
--用来描述类元数据的类(klasses)也被删除了
--分元数据分配了多个虚拟内存空间
--给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些
--归还内存块,释放内存块列表
--一旦元空间的数据被清空了,虚拟内存的空间会被回收掉
--减少碎片的策略

猜你喜欢

转载自www.cnblogs.com/huazai1024/p/11077420.html