标签:tsp 压缩 调用 这一 计数 递归调用 记录 优先 局部变量
最近想整理一下GC相关的知识和经验,在整理之前下决心先整理一下jvm的内存结构,后续会持续更新。
jvm内存结构重要由两部分组成:线程共享区域与线程私有区域,如下图所示:
其中方法区和堆为线程共享区域,栈与程序计数器为线程私有区域。与操作系统定义的堆栈类似,栈用来存储方法调用时产生的临时变量以及寄存器值,函数的调用伴随着栈帧的开辟及销毁。而堆则是一块较大的内存区域由各线程共享,像对象、常量等jvm进程拥有的资源在堆中由各线程共享。
方法区
方法区也是线程共享区,用于存储虚拟机加载的类信息(instanceOopKlass,即类在jvm中的数据结构),常量,静态变量,即时编译器编译后的代码等数据。
在逻辑上方法区其实属于堆的一部分,但是为了与堆进行区分,方法区也叫“非堆”。
HotSpot虚拟机使用永久代来实现方法区,使得HotSpot虚拟机的垃圾收集器可以像管理堆内存一样来管理这部分内存,能省去专门为方法区编写内存管理代码工作。所以开发者喜欢将方法区称为永久代,本质上两者并不等价,对于其他虚拟机来说不存在永久代的概念。jdk1.8之后,HotSpot虚拟机放弃了永久代的概念,转而使用元空间来代替永久代。下面我们会解释一下方法区与永久代的关系,以及元空间的概念。
方法区可选择不实现垃圾收集,一般来说,这个区域对内存回收的条件较为苛刻。因为类一旦被加载,会有多种被使用的方式,判断该类是否还在被使用并不是那么容易,不合理的垃圾回收策略很容易导致运行时异常。但是这部分区域的回收确实是必要的,不进行回收的代价便是会时常出现内存溢出问题。
当方法区无法满足内存分配需求时,将会抛OutOfMemoryError异常。
方法区与永久代的关系
在Java虚拟机规范中,方法区在虚拟机启动的时候创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择不在方法区实现垃圾回收与压缩。这个版本的虚拟机规范也不限定实现方法区的内存位置和编译代码的管理策略。所以不同的JVM厂商,针对自己的JVM可能有不同的方法区实现方式。
在HotSpot中,设计者将方法区纳入GC分代收集。HotSpot虚拟机堆内存被分为新生代和老年代,对堆内存进行分代管理,所以HotSpot虚拟机使用者更愿意将方法区称为老年代。
方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。我们知道在HotSpot虚拟机中存在三种垃圾回收现象,minor GC、major GC和full GC。对新生代进行垃圾回收叫做minor GC,对老年代进行垃圾回收叫做major GC,同时对新生代、老年代和永久代进行垃圾回收叫做full GC。许多major GC是由minor GC触发的,所以很难将这两种垃圾回收区分开。major GC和full GC通常是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是major GC。
上面说过,HotSpot虚拟机在1.8之后已经取消了永久代,改为元空间,类的元信息被存储在元空间中。元空间没有使用堆内存,而是与堆不相连的本地内存区域,即堆外内存。所以,理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。
这项改造也是有必要的,永久代的调优是很困难的,虽然可以设置永久代的大小,但是很难确定一个合适的大小,因为其中的影响因素很多,比如类数量的多少、常量数量的多少等。为了减少内存碎片,永久代中的元数据的位置也会随着一次full GC发生移动,比较消耗虚拟机性能。同时,HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理永久代中的元数据。将元数据从永久代剥离出来,不仅实现了对元空间的无缝管理,还可以简化Full GC以及对以后的并发隔离类元数据等方面进行优化。
运行时常量池
运行时常量池是方法区的一部分。jdk1.8之前常量池是使用永久代来实现的,相比类的多少,常量池的大小更容易引起内存溢出异常,好在元空间的引入替代了永久代,HotSpot 虚拟机在1.8版本之后使用了堆外内存也就是元空间来实现常量池,大大缩减了该类风险。
class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后加入方法区的运行时常量池中存放。
运行时常量池相于class文件中的常量池所不同的是其具备了动态性。class文件中常量池中的常量在编译期间就已经定义好了,而运行时常量池在程序运行期间也可以将常量放入该常量池中,最常见的做法就是调用String类的intern()方法。
堆
堆是JVM管理的最大的一块内存区域,存放着对象的实例,由该jvm进程中的所有线程共享。同时堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”。
JAVA堆的分类:
从内存回收的角度上看,可分为新生代(Eden空间,From Survivor空间、To Survivor空间)及老年代(Tenured Gen)。
从内存分配的角度上看,为了解决分配内存时的线程安全性问题,线程共享的JAVA堆中可能划分出多个线程私有的分配缓冲区(TLAB)。
JAVA堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
可通过参数 -Xmx -Xms 来指定运行时堆内存的大小,堆内存空间不足也会抛OutOfMemoryError异常。值得注意的是(个人经验,未必正确),适当情况下,应当在jvm启动前充分测试确定合理的堆的大小,将堆的初始化大小与最大大小设为一致,避免jvm在运行时动态的扩容堆的大小,对性能造成影响。
程序计数器
程序计数器是一块较小的空间,它可以看作是当前线程所执行的字节码的行号指示器。
如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是native方法,这个计数器的值为undefined。
JVM的多线程是通过线程轮流切换并分配CPU执行时间片的方式来实现的,任何一个时刻,一个CPU都只会执行一条线程中的指令。为了保证线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程间的程序计数器独立存储,互不影响。
此区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域,因为程序计数器是由虚拟机内部维护的,不需要开发者进行操作。
虚拟机栈
每个线程有一个私有的栈,随着线程的创建而创建,生命周期与线程相同。栈中以栈帧为单位进行分配和释放,每调用一个方法,便为方法开辟一个新的栈帧。栈帧中存放了该次方法调用的局部变量表、操作数栈、动态链接、方法出口等信息。
对于递归调用,每次调用时便会为该次调用开辟新的栈帧,这也是递归函数容易引起爆栈的原因。许多语言对尾递归进行了优化,尾递归是指递归调用在函数的最后一行且递归调用的返回值不会被用来计算的递归函数。因为调用在最底层,递归调用时需要的临时变量会在调用时传入新的调用,且返回值不参与计算,所以当函数执行到递归调用时已经没有临时变量需要存储。许多语言便针对此特性进行了优化,进行尾递归调用时不开辟新的栈帧而直接清空并复用之前一次调用的栈帧,这样无论调用多少层,只需要一个栈帧就够了,这就避免了爆栈的风险。
但遗憾的是java并未对尾递归进行优化,按官方文档的说明是目前看来不需要优化,之后的版本如有需要会对这一特性进行补充。
这样我们必须对递归函数进行改造,比如用递推模拟回归过程(如动态规划),或借助栈结构进行递推(如二叉树的深度优先遍历)。
本地方法栈
和虚拟机栈类似,两者的区别就是虚拟机栈是为虚拟机执行java方法服务,本地方法栈为虚拟机执行native方法服务 。
HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的。
与虚拟机栈一样,本地方法栈也会抛StackOverflowError和OutOfMemoryError异常。
对象的创建
下面我们看一下程序员脱单的基本操作,new一个对象的过程。
标签:tsp 压缩 调用 这一 计数 递归调用 记录 优先 局部变量
原文地址:https://www.cnblogs.com/niuyourou/p/11978024.html