因为CPU需要不停的切换各个线程,切换回来的时候,就得知道从哪开始继续执行,所以为了实现这个需求(需要记录下一次执行的指令地址)才有了PC来存放。但是,PC寄存器是线程私有的,为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
虚拟机的运行,类似于这样的循环:
while(not end){
取PC中的位置,找到对应的位置指令;
执行指令
PC++
}
2. JVM stacks(栈)
- stacks里面存的是多个frame(栈帧),每个方法会对应一个栈帧。
- ⚠️注意: StackOverFlowError 表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。而 OutOfMemoryError 是指当线程申请栈时发现栈已经满了,而且内存也全都用光了
2-1 *** 作数栈本地方法栈 当调用的是原生native方法的时候,需要寄存到本地方法栈当中
虚拟机栈 专门为调用jvm内部方法所提供的一个栈但是在主流的Hotspot虚拟机中本地虚拟栈和虚拟机栈已经被融合成了一体,所以并没有过多的区别。
- 在方法的执行过程中,各种字节码指令会往 *** 作数栈中写入和提取内容(如上图),也就是出栈/入栈 *** 作, *** 作数栈的深度都不会超过在code属性中的maxstacks数据项中设定的最大值,如果当前线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 记录变量
2-3 动态链接
- 局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,一个Slot可以存放一个32位以内的数据类型,每个槽都应该能存放一个boolean、byte、char、short,int,float、reference或returnAddress类型的数据。
- 对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的引Slot空间。Java语言中明确的(reference类型则可能是32位也可能是64位),64位的数据类型只有long和double两种。
- 为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。
- 将符号引用解析为直接引用
2-4 返回值地址符号引用即用用字符串符号的形式来表示引用,其实被引用的类、方法或者变量还没有被加载到内存中。而直接引用则是有具体引用地址的指针,被引用的类、方法或者变量已经被加载到内存中。以变量举个例子:
//符号引用 String str = "abc"; System.out.println("str=" + str); // 直接引用 System.out.println("str=" + "abc");
- 就是当方法执行完成后,需要返回到调用方的地址,以保证程序的继续往下运行。
- invokeStatic:调用静态方法
- invokeVirtual:调用虚方法,运行期动态查找的过程,多数调用方法,自带多态
- invokeInterface:抵用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的哪个对象的特定方法
- inovkeSpecial:调用构造方法,private方法,可以直接定位不需要多态的方法
- invokeDynamic:lambda表达式或者反射或者其他动态语言(scala,kotlin)或者GCLib
所有的对象实例以及数组都应当在运行时分配在堆上
几乎所有的对象实例都在这里分配内存,但是少数情况下,堆可以额外开辟一个空间用于给线程存储一些属于它们专有的buffer。这种技术叫做TLAB,属于栈上分配技术。
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
-
Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
Young Generation Space : 新生区 Young/New
又被划分为Eden区和Survivor区
Tenure generation space: 养老区 Old/Tenure
Permanent Space:永久区 Perm -
Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
Young Generation Space:新生区 Young/New
又被划分为Eden区和Survivor区
Tenure generation space :养老区 Old/Tenure
Meta Space :元空间 Meta
Java堆区用于存储Java对象实例,堆的大小在JVM启动时就已经设定好了,可以通过选项-Xmx
和-Xms
来进行设置。
-Xms
:用于表示堆区的起始内存,等价于-xx:InitialHeapSize- -
Xmx
:则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
tips
- 方法区是堆的一个逻辑部分,它与 Java 堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾回收。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。内存回收效率低,回收一遍内存之后可能只有少量信息无效。
- 1.8之间也可以称作PermSpace,在1.8之后称作MetaSpace。
- 方法区的大小
决定了系统可以保存多少个类
,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误
:ava.lang.OutofMemoryError:PermGen space
或者java.lang.OutOfMemoryError:Metaspace
-
perm space < 1.8
字符串常量位于PermSpace
FGC不会清理
-
meta space >= 1.8
字符串常量位于堆
会触发FGC清理
5. 直接内存(Direct Memory)运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
在JDK4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 方法库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行 *** 作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。相当于jvm可以直接访问的内核空间的内存(OS管理的内存),NIO提高效率0拷贝
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)