如何向女朋友解释什么是java?基础篇(一)

一、Java内存结构详解

1 . 内存结构图

内存共享(绿色):方法区(包含运行时常量池)、和堆。

内存隔离(蓝色):虚拟机栈、本地方法栈、程序计数器。

2. 各个区域详细解释

直接内存(非堆内存) 并不是虚拟机运行时数据区的一部分,也不是jvm规范定义的一部分,但是这部分也被频繁使用。直接内存不属于 Java 堆,所以它不受堆大小限制,但是它受物理内存大小的限制。

配置 可以通过 -XX:MaxDirectMemorySize 参数来设置最大可用直接内存,如果启动时未设置则默认为最大堆内存大小,即与 -Xmx 相同。即假如最大堆内存为1G,则默认直接内存也为1G,那么 JVM 最大需要的内存大小为2G多一些。当直接内存达到最大限制时就会触发GC,如果回收失败则会引起OutOfMemoryError。

理论上直接内存的机制访问速度要快一些,但也不能武断地直接说直接内存快,另外,在内存分配操作上直接内存要慢一些。直接内存更适合在内存申请次数较少,但读写操作较频繁的场景。

#程序计数器(线程私有) 代码的运行是有顺序的,因为cpu在多线程间间切换,从A线程切换到B线程,再从B线程切换回来时,cpu该如何知道到哪里开始执行呢?

这个时候,程序计数器就发挥作用了。它是一块较小的内存空间,cpu工作时就是根据每个线程的程序计数器的值来选取下一条该执行那一行字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都要依赖。

特别注意: 如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果线程正在执行的是native方法,计数器值则为空。 程序计数器是jvm规范中唯一没有规定任何OutOfMemoryError情况的区域。

#虚拟机栈(线程私有) 描述的是java方法执行的内存模型,它的生命周期与线程相同。每个java方法在执行的过程中都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、 每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中从虚拟机栈中从入栈到出栈的过程。(平常大多数人说的栈其实就是对应的这里的虚拟机栈,或者是虚拟机栈中的局部变量表)。

局部变量表:存放了编译器可知的各种基本数据类型(boolean、byte、char、short、float、double、long、int),对象引用(对象起始地址的指针)和retuenAddress类型(指向一条字节码指令的地址)。
复制代码

特别注意: 在jvm规范中,有两种异常情况:1、如果线程请求的栈深度大于虚拟机所允许的深度,会抛出StackOverflowError异常。2、如果虚拟机栈可以动态扩展,如果虚拟机栈无法通过动态扩展在jvm中申请到内存,将会抛出OutOfMemoryError异常。

#本地方法栈(线程私有) 该区域作用和虚拟机栈了类似,虚拟机栈是为了执行java方法(字节码)。而本地方法栈则是为了jvm使用到Native方法。

特别注意: 在jvm规范中对本地方法栈中的方法中所用的语言、使用方式和数据结构并没有强制规定。因此这部分区域可以自由的实现它。甚至例如:HotSpot虚拟机,是将虚拟机栈和本地方法栈合二为一的。也会抛出StackOverflowError异常和OutOfMemoryError异常。

#java堆(线程共享) java虚拟机中最大的一块内存区域,在虚拟机启动时被创建。是所以线程共享的一块内存区域,唯一的目的就存放对象实例。几乎所有的对象实例都在这里分配内存。

jvm规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,所有的对象都分配在堆上渐渐的变得不是那么“绝对”了。

特别注意: java堆是gc垃圾回收最主要的管理区域。

  • 从垃圾回收的角度上来看,主流的收集器基本都是采用分代收集算法,所以java堆中可以分为:新生代和老年代。(Eden空间、From Survivor空间、To Survivor空间等);

  • 从内存分配的角度来看,线程共享的java堆中还可以划分出许多个线程私有的分配缓冲区;

无论如何划分,都与存放内容无关,无论哪个区域,存储的都会对象实例。 划分的目的都是为了更好的回收内存或者分配内存。如果堆中的的内存完成实例分配,并且堆也无法再进行扩展,将会抛出OutOfMemoryError异常。

#方法区(线程共享)永久代 这块内存区域主要是用来存储已经被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。虽然jvm规范将方法区描述为堆中的一部分,但实际上它有一个别名(非堆)目的就是为了和堆区分开。

对于习惯在HotSpot虚拟机上开发、部署程序的开发这来说。习惯称呼这一区域为永久代,实际上两者并不等价,仅仅是因为 HotSpot虚拟机的设计团队选择把gc分代手机扩展至方法区。或者说使用永久代来实现方法区。这样就可以在方法区上实现分代的垃圾收集。但是对于其他的虚拟机是没有这个概念的。使用永久代来实现方法区并不是一个好主意,因为这样容易遇到内存溢出问题。(永久代有-XX:MaxPermSize的上限)该方式逐渐会被替代。采用 Native Memory来实现方法区。jdk7之后的版本已经把字符串常量池从永久代移除。

特别注意: jvm规范对方法区限制是比较宽松的。除了和java堆一样可以使用不需要连续的内存空间、可以动态的扩展大小之外,几乎没什么特别的限制。甚至在这一区域可以选择不进行垃圾回收。实际上垃圾回收在这一区域也是比较少出现的,在这一区域主要是为了回收常量池和类型的卸载。虽然很少出现垃圾回收,但是还是很有必要在这一区域进行回收的。因为有可能会导致内存泄漏,引起的严重的bug。(根据jvm规范,当方法区无法满足内存分配需求时将抛出OutOfMemoryError异常)。

#运行时常量池 该区域是属于方法区中的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外。还有一项就是常量池。用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载之后,进入方法区中的运行时常量池。jvm规范虽然对class文件中的每一个部分的存储格式都有严格要求,每一个字节用于存储那种数据,都有严格的要求,但java虚拟机规范没有做任何细节的要求,所以不同的提供商实现的虚拟机可以按照自己的需求来实现这个区域。

特别注意: 运行时常量池相对于class文件常量池有一个重要特征是具备动态性,java语言并不要求常量一定只有编译器才能产生,也就是并非置于class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中。

猜你喜欢

转载自juejin.im/post/5d19f62b6fb9a07ecf723d0f