1.内存(Memory)
内存的概念:是计算机中的临时存储设备,在程序执行时,用来存放程序和程序处理的数据。
1.1存储器层次结构
在计算机系统中的存储设备都被组织成了一个存储器层次结构,这个层次中,从上至下,设备的访问速度越来越慢,存储量越来越大。而内存的作为本地磁盘和CPU之间的高速缓存,来缓冲CPU读取本地磁盘中数据的速度。
1.2虚拟内存:
虚拟内存是一个抽象概念,他为进程提供一个假象,即每个进程都在独占的使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。基本思想是把进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。 —《深入理解计算机系统》第一章1.7.3
我们知道在计算机中,内存的作用就是临时存放计算机中运行的程序需要处理的数据。
1.3Q&A
- 为什么需要内存来临时存储数据? 因为计算机的存储设备由于各设备自身的存储量大小和访问速度,从而组织成立一个层级结构“存储器层次结构”。这个层次结构的中心思想就是:“上一层的存储器作为下一层存储器的高速缓存”。这个结构的特点就是,层级越高访问速度越快,存储量越小,计算机CPU的寄存器处于层次结构的顶端,而寄存器想要处理存储在层次底端的本地磁盘上的数据,就需要处于层次结构中间的计算机存储器来缓冲字节的访问速度。
- 对于虚拟内存的理解? 虚拟内存是计算机管理内存的一种技术。它使得在内存中运行的程序认为他拥有连续的内存空间(一个完整连续的地址空间)。就如当JVM运行时,JVM相对于计算机系统来说,是一个进程,通过虚拟内存的概念,会为JVM进程开辟一片内存。这片内存也就是JVM的运行时内存。
2.JVM内存结构
2.1不同版本JDK与JVM内存结构
JVM的内存结构根据JDK的版本不同也有不同的结构,如:
- 在JDK1.7的版本中JVM内存划分为5个区域:虚拟机栈、本地方法栈、程序计数器、堆、方法区(也被称为永久代)
- 在JDK1.8的版本中由全空间替代了1.7中的方法区(取消永久代)。java永久代去哪儿了?
2.2详解各内存区域
2.2.1Java虚拟机栈
2.2.1.1概述JVM虚拟机
虚拟机栈描述的是Java方法执行的内存模型。是线程私有的,所以它的生命周期和线程相同。 每个方法在执行的同时都会创建一 个栈帧(Stack Frame) 用于存放局部变量表、操作数栈、动态链接、方法出口等信息。 每一个方法从调用直至执行结束的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
2.2.1.2JVM虚拟机栈中的局部变量表
局部变量表是存储变量值的内存空间,用来存放方法参数和方法内部定义的局部变量。局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAdress类型。 在编译期间(编译为Class文件),方法的Code属性的max_loacls的值就是该方法所需要分配的最大的局部变量表的容量。 局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法所需要在帧中分配多大的局部变量空间时完全确定的,所以在方法运行期间不会改变局部变量表的大小。 局部变量不像成员变量在定义的时候赋初始值。
- 基本数据类型:boolean byte int short long double float char
- 对象引用:reference类型,他不等同于对象本身,可能是指向对象起始地址的指针、也可能是指向一个代表对象的句柄或其他与对象相关的位置
- returnAdress类型:指向了一条字节码指令的地址
2.2.1.2.1局部变量表中的基本数据类型
在局部变量表中64位长度的long和double类型的数据会占用两个局部变量空间(Slot),其余的的数据类型只占用一个。 局部变量槽(variable slot):是局部变量表的最小单位,可复用。
2.2.1.2.2对象引用
- 建立对象之后该如何使用对象呢? JVM栈中除了8个基本的数据类型,还有reference类型来专门操作堆上的具体对象。
- reference类型是如何访问堆中的对象呢? 关于reference类型JVM虚拟机规范中只规定了它是一个指向对象的引用,而没有定义这个引用应该用何种方式定位、访问堆中的对象的具体位置,所以访问方式也是取决于虚拟机的具体实现。目前主流的访问方式有句柄和直接指针。
关于对象 对象实例数据:存储在堆中,对象中各个实例字段的数据 对象类型数据:存储在方法区中,对象的类型、父类、实现的接口、方法等 静态区:存储在方法区中,用来存储静态变量,静态块
句柄访问
graph TD
A[Java栈本地变量表中的reference] -->|指向Java堆中的句柄池| C(句柄包含两个指针分别指向实例池和元数据)
C -->|指向Java堆中的实例池| D[对象的实例数据]
C -->|指向方法区&元空间| E[对象类型数据]
如果使用句柄访问的话,存储对象的Java堆就会分出一块内存作为句柄池存放句柄,句柄包含了对象实例数据和对象类型数据各自的具体地址信息。
直接指针
graph TD
A[Java栈本地变量表中的reference] -->|指向堆中的对象| C(对象的实例数据包含到对象类型数据的指针)
C -->|指向方法区&元空间| D[对象的类型数据]
使用直接指针方式时,reference中存储的直接就是对象地址,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息。
直接指针和句柄这两种访问对象方式的优劣势
两种方式各有优势,使用句柄最大的好处就是reference中存储的是稳定的句柄地址,如果队中的对象地址发生改变的话(因为垃圾收集,导致对象的地址发生改变的情况时常发生),只需要改变句柄中的指向对象实例数据指针,而不需要改变reference存储的地址。 相对于句柄,使用直接指针的好处就是速度更快,因为它比句柄节省了一次访问对象实例数据的时间开销。频繁的访问对象,这些访问对象的开销会积累一个非常可观时间成本。 就Hotspot虚拟机而言,使用的是直接指针的方式来访问堆中的对象。
2.2.1.3 操作数栈
也被称为操作栈,是一个后入先出的栈。它的最大深度在编译期间被写入到Code属性的max_stacks数据项中。操作数栈中的元素可以是任意类型。 当方法刚开始执行时,操作栈是空的,方法执行过程中,通过字节码指令对操作数栈中写入和提取操作。
2.2.1.4 Java虚拟机栈会遇到的异常情况
Java虚拟机规范中规定Java虚拟机栈两种异常情况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError
/*
VM Args: -Xss128k 设置栈允许深度的参数
*/
public class JvmStackSOF {
private int stackLength = 1;
public void stackLeak(){
stackLength ++;
stackLeak();
}
public static void main(String[] args) Throwable{
JvmStackSOF sof = new JvmStackSOF();
try{
sof.stackLeak();
}catch(Throwable e){
System.out.println("stack length:" + sof.stackLength);
throw e;
}
}
}
// 运行结果:
Exception in thread "main"stack length:985
java.lang.StackOverflowError
...
在这段demo中,使用了-Xss参数来减少栈的内存容量。定义大量本地变量,不断进栈而不出栈,增加此方法帧中局部变量表的长度。最终会导致虚拟机的内存空间耗尽。 2. 虚拟机栈动态扩展时,如果无法申请到足够的内存,就会抛出OutOfMemoryError异常。
/*
VM Args: -Xss2M设置栈允许深度的参数
*/
public class MyTest6 {
private void dontStop(){
while (true){
}
}
public void stackLeakByThread(){
while (true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
MyTest6 oom = new MyTest6();
oom.stackLeakByThread();
}}
//运行结果:
Java的线程是映射到操作系统的内核线程上的,demo执行时有较大风险,可能会导致操作系统假死
不同于SOF,OOM指的是当整个虚拟机内存耗尽,并且无法申请到新的内存时抛出的异常。 通过建立过多线程导致的内存溢出,在不能焦山线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程
2.2.2本地方法栈
- 概述一下本地方法栈 本地方法栈和虚拟机栈发挥的作用类似,也是线程私有的,也能抛出StackOverflowError和OutOfMemoryError异常(Hotspot虚拟机中,因为hotspot虚拟机选择合并了本地方法栈和虚拟机栈)。 在JVM虚拟机规范中并未对本地方法栈的具体实现做出强只要求,因此具体的虚拟机可以自由实现它。
- 本地方法栈和虚拟机栈的不同点? 虚拟机栈是为Java代码(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 当线程调用Java方法时,虚拟机会创建一个栈帧并压入Java虚拟机栈。然而当调用的是native方法时,虚拟机会保持虚拟机栈不变,也不会向Java虚拟机栈中压入新的栈帧,虚拟机只是简单的动态连接并直接调用指定的native方法。
- 简述一下Native方法? Native方法是Java通过JNI直接调用本地C/C++库,可以认为是Native方法相当于C/C++暴露给Java的一个接口,Java通过调用这个接口从而调用到C/C++方法。
2.2.3程序计数器
- 概述一下程序计数器 程序计数器是一块很小的内存空间,和Java虚拟机栈、本地方法栈一样,都是线程私有的,也是唯一一个在Java虚拟机规范中没有规定任何OOM异常情况的区域 他可以看作时当前线程的所执行的字节码的行号指示器,在虚拟机的概念模型里,字节码解释器工作时就是通过这个计数器的值来选择吓一跳需要执行的字节码指令。
- ”线程私有“? JVM虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,在任何一个确定的时刻,一个处理器都会执行一条线程的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互相不影响,独立存储,我们称这类内存区域为”线程私有“的内存。
2.2.4堆
2.2.4.1堆的概述
Java堆空间是虚拟机内存管理中最大的一块,这区域被所有线程共享。目的就是存放对象的实例,几乎所有的对象都在堆中分配内存所以也是垃圾收集器管理的主要区域。 随着JIT编译器额发展与逃逸分析技术的成熟,栈上分配内存、标量替换优化技术将会导致一些微妙的变化发生,所有对象分配在堆上也渐渐变得不那么绝对了。 Java虚拟机规范规定:Java堆可以处于物理上不连续的空间中,只要逻辑上连续即可。在实现时既可以固定大小的,也可以是可扩展的,当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms参数控制)
2.2.4.2堆与垃圾收集器
因为现在的垃圾收集器都采用分代收集算法,所以从内存回收的角度来看,Java堆还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间和To Survivor空间等。
2.2.4.3堆的内存异常
如果在堆中没有内存完成实例分配,并且堆也无法在扩展时,将会抛出OutOfMemoryError异常。 思考:因为堆可以是不连续的空间,如果有很多碎片空间空闲,且有一个较大的对象需要分配,并且无法分配在碎片空间中,会出现什么情况呢?(待补充) 代码实战:
/*
VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM{
static class OOMObject{
}
public static void main(String[] args){
List<OOMObject> list = new ArrayList<>();
// 不断向list里添加对象在堆中拓展内存,直至堆空间无法分配。
// 通过HeapDumpOnOutOfMemoryError参数可以在虚拟机出现异常时dump出当前的内存转储快照文件以便分析。
while(true){
list.add(new OOMObject());
}
}
// 运行结果:
java.lang.OutOfMemoryError: Java heap spae:
Dumping heap to java_pid3402.hprof ...
Heap dump file created ...
2.2.4.4数据结构-堆(补充)
堆(Heap)又被称为优先队列(Priority Queue),是计算机科学中一类特殊的数据结构的统称。堆通常可以被看作一棵树的数组对象。在队列中,调度程序反复提取队列中的第一个作业并运行,因而实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样具有优先权。堆即为解决此问题设计的一种数据结构。
2.2.5方法区
2.2.5.1 方法区概述
和堆一样,方法区也是所有线程共享的一块内存区域。它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2.2.5.2 方法区与永久代
使用HotSpot虚拟机的用户更愿意把方法区称为“永久代“,但是本质上两者并不等价。仅仅是因为Hotspot虚拟机的设计团队选择把GC分代收集至方法区,或者说用永久代来实现方法区而已。这样HotSpot的垃圾收集器可以像管理Java堆一样管理方法区这部分的内存,能省去专门为方法区编写内存管理代码的工作。
2.2.5.3 方法区存放的内容
2.2.5.3.1 类信息
- 类型全限定名
- 类型的直接超类的全限定名
- 类型是类还是接口
- 类型的访问修饰符
- 任何直接超接口的全限定名的有序列表
- 类型的常量池:存放该类型所用到的常量的有序集合,包括直接常量(字符串、整数、浮点数)和对其他类型、字段、方法的符号引用。
- 字段信息(该类声明的所有字段):字段的访问修饰符、字段的类型、字段名称
- 方法信息(方法信息中包含类的所有方法):方法的访问修饰符、方法的返回类型、方法名、[方法参数个数、类型、顺序等]、方法字节码、操作数栈和该方法在栈帧中的局部变量区大小、异常表。
- 除了常量意外的所有类变量(包括静态变量)
- 到该类的类加载器的引用
- 到Class类的引用
2.2.5.3.2 运行时常量池(Runntme Constant Pool)
运行时常量池也是方法区的一部分。class文件中除了有类信息外,还有一项常量池(Constant Pool Table)用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。 [这里简单说一下class文件的结构,关于class文件的解读留个坑,以后单独补充]
2.2.5.4 方法区异常【demo待补充】
1.根据虚拟机规范规定,当方法区无法满足内存分配需求时,将跑出OutOfMemoryError异常。 2.当常量池无法再申请到内存时会抛出OutOfMemory异常。
2.2.6元空间
2.2.6.1 源空间概述
Meta Space是JDK1.8引入的,替代了1.8之前的方法区,永久代。元空间存储的是元信息,使用的是操作系统的本地内存(与之前的永久代相比内存结构改变),可以是不连续的,由元空间虚拟机进行管理。
2.2.6.2 Metaspace的组成
Metaspace由两大部分组成:Klass Metaspace和NoKlass MetaSpace。
2.2.6.2.1 Klass Metaspace
- Kalss Matespace就是用来存储klass的,就是class文件在jvm里运行时数据结构
- 这部分默认放在Compressed Class Pointer Space中,是一块连续的内存区域,紧接着Heap,和之前的永久代一样。通过-XX:CompressedClassSpaceSize来控制这块内存的大小。
2.2.6.2.2 NoKlass Matespace
- NoKlass Matespace专门来存Klass相关的其他内容,比如method,constant等,可以由多块不连续的内存构成。
- 这块内容是必须存在的,在本地内存中分配。
2.2.6.3 元空间的容量
默认情况下,类元数据分配受到可用的本机内存容量的限制,一个新的参数(MaxMetaspaceSiza)可以使用。允许你来限制用于类元数据的本地内存。如果没有特别指定,元空间将会根据应用程序在运行时的需求动态设置大小。
2.2.6.4 元空间的垃圾回收
如果类元数据的空间占用达到参数"MaxMatespaceSize”设置的值,将会触发对死亡对象和类加载器的垃圾回收。 [还是觉得从永久代到元空间改变这一点,应单独整理一篇笔记(待完成)]
2.2.7直接内存
2.2.7.1 直接内存概述
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。 在JDK1.4中新加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,他可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存进行操作。这样能在一些场景中显著提高性能,因为避免了在java堆和Native堆中来回复制数据。
2.2.7.2 异常情况
本机直接内存的分配不会受到Java堆大小的限制,但是会受到本机总内存大小以及处理器寻址空间的限制。在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。
2.2.8结尾
了解了JVM运行时的内存区域后,感觉对JVM的运行及对象的存储,思路更加清晰了。每一块内存区域都有着自己的职责,有着支持方法运行的JVM虚拟机栈、程序计数器和本地方法栈这一大块的线程私有区域,也有存放对象的"GC堆"和储存matadata的元空间这一大块的线程共享区域,还有存储在本机直接内存上的堆外内存。这些空间相互配合维持着JavaDemo在计算机里有效运行。 这些笔记大多来源于周志明老师的《深入理解Java虚拟机第二版》。好记性不如烂笔头,作者作为一名初学者,想将书里涵盖的知识点,以笔记的形式记录下来。