程序计数器(线程私有)
为什么要有程序计数器?
- 方便字节码解释器工作,字节码解释器可以通过改变程序计数器来依次读取指令(让程序计数器保存下一条要执行的字节码指令),从而实现代码的流程控制,顺序执行,选择,循环,异常处理
- 在多线程下,可以程序计数器可以记录线程的执行的位置,可以在线程上下文切换的时候,恢复线程继续执行。
程序计数器的底层结构。
- 程序计数器是一快很小的内存区域,可以看成是线程所执行的字节码指示器。
程序计数器的注意点。
程序计数器是不会出现OOM的错误的内存区域,它的生命周期随线程开始而开始,线程结束而结束。
虚拟机栈(线程私有)
为什么要有虚拟机栈?
- 虚拟机栈描述了,单个线程中java方法的执行流程,每当执行一个线程中的java方法,就会生成一个对应的栈帧,进入到虚拟栈中,方法执行完(return或抛出异常)栈帧弹出。(虚拟机栈描述了线程中的方法的执行状况)
虚拟机栈的内部结构
-
虚拟机栈的基本单位是栈帧
-
栈帧是由:局部变量表,操作数栈,方法返回地址,动态链接,额外的信息组成
局部变量表
-
局部变量中主要存储了方法中用到的基本数据类型(boolean,byte,short,char,int,float,long,double),引用数据类型(reference类型,存储的不是对象本身,而是指向对象的一个引用地址指针,也可能是一个代表对象的句柄或其他与对象相关的位置)
虚拟机栈的有哪些注意点。
操作数栈中逃逸分析和栈上分配?
- 当栈帧中的局部变量表中的引用类变量,没有被return 出去或没有被外面的对象所获取的可能,(就是这个引用变量值属于我们方法本身,没有可能被其他线程所获取,线程方法私有),就可以将此变量分配到虚拟机栈上的局部变量表存储。
虚拟机中常见的可能出现的错误 StackOverFlowError和OutOfMemoryError
- StackOverFlowError:当我们虚拟机栈不能动态扩充容量是,栈帧的深度达到虚拟机栈的最大深度时,出现这个错误
- OutOfMemoryError:当我们虚拟机栈可以动态扩容时,在动态扩容时没有内存空间可以被申请时,出现这个错误
- 但是在hotspot中虚拟机栈是不能动态扩容的。
本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中本地方法栈和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
堆区
为什么要有堆区?
- 堆区时java虚拟机管理的最大的一个内存区域,堆区也是所有线程共享的一个区域。
- 堆区存在的目的是为了存放对象实例,几乎所有的对象实例以及数组被分配到读取。存储了大量的数据信息。
堆区的具体结构?(依据的分代回收算法,更具体的过程参考内存的分配与回收部分)
- 从年代上堆区被划分为:Edenj空间,From survive空间,To Survive空间,以及old空间。(划分这么细的原因是为了更好的回收内存,或是更好的内存分配,依据的是分代垃圾回收算法)
堆区有哪些注意点?
逃逸分析和栈上分配?
- 随着逃逸分析技术的成熟,在jdk1.7开始就默认开启了逃逸分析。
- 逃逸分析:如果某些方法中的对象没有被返回,或则被外面所引用(当我们实例对象存在,没有被其他栈帧所引用的可能性时,满足栈帧封闭,就将这个实例对象分配到虚拟机栈的局部变量表上,而不是分配到堆空间中)
- 所以并不是所有实例对象数组等都在堆空间中。
堆与内存的分配与回收的重点区域
- 几乎所有的对象实例和数组都在堆区,所以堆区时内存的分配与回收的重点区域
堆区中常见的错误类型
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded : 当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值,详见:Default Java 8 max heap size)
方法区
为什么存在方法区?
- 与堆区一样方法区也是主要存储数据的,与堆区不同,方法区主要存储的是:类信息,运行时常量池,即时编译后的代码数据等
- 方法区在java虚拟机规范中作为堆区的逻辑部分,Non-heap(非堆),但为了更有区分性,根据他们的存储内容的不同将其单独区分出来单独提出了方法区的概念.
方法区的注意点
方法区与永久代的关系?
- 方法区与永久代的关系类似于,接口和实现类之间的关系
- 《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
为什么永久代被元空间替换?
- 在一些大型的jvm,比如如jRockit和j9等都实现了元空间的作法,hotspot为了整合JRockit,最终在jdk1.8将元空间替换永久代,可以更好整合hotspot和JRockit.(在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。)
- 元空间所带来的的好处,元空间使用的是直接内存,操作系统的堆内存,空间更大,更少出现OOM
方法区中的运行时常量池
- 运行时常量池是方法区的一部分。类信息的很多部分都放在这里面:类的当前版本,字段,方法,接口等描述信息,还有常量池表。
- 当常量池内存不足也无法申请到内存时抛出OOM
- 区分与字符串常量池:在hotspot中在jdk1.7时字符串常量池和静态变量就从方法区移到了堆区中:1.jdk1.7之前运行时常量池包含字符串常量池;2.jdk1.7时字符串常量池就从方法区移到了堆区中;3.jdk1.8在元空间取代永久代时没有将字符串常量池移回方法区
直接内存
直接内存是什么?
- 直接聂村不属于jvm运行时数据区,也不是虚拟机定义中的内存区域,但是这部分也被频繁使用,而且导致OOM
- 直接内存是jvm可以在操作系统的堆上分配内存,jvm保存这个直接内存的引用,可以像堆内存那样使用。
直接内存在jvm中的用处?
- JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。(减少数据深拷贝次数)