你点赞了吗?你关注了吗?每天分享干货好文。
高并发解决方案与架构设计。
海量数据存储和性能优化。
通用框架/组件设计与封装。
如何设计合适的技术架构?
如何成功转型架构设计与技术管理?
在竞争激烈的大环境下,只有不断提升核心竞争力才能立于不败之地。
留言【我要晋级】,一对一指导,带你晋级。
一、JVM 内存结构总览
JVM 内存结构是 Java 程序运行的基石,定义了数据在虚拟机中的存储与访问规则。其核心分为 线程私有区域(线程隔离) 和 线程共享区域,具体划分如下图
线程私有区域(线程隔离) :程序计数器、虚拟机栈、本地方法栈
线程共享区域:堆、方法区
从类型上分,也可以大致分为:堆、栈。堆是存储单元,主要作用是存储数据的。栈是运行单元,主要作用是解决程序如何运行。一个为线程执行服务,一个为全局数据存储。
二、程序计数器(Program Counter Register)
程序计数器是线程私有的内存区域,用于存储当前线程执行的字节码指令地址。如果线程正在执行本地方法,程序计数器的值为空(Undefined)。
程序计数器是一块较小的内存空间,也是运行速度最快的存储区域。它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
说人话就是:用于存储当前线程执行的字节码指令地址。
由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说就是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类的内存区域为“线程私有”的内存。
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行本地方法(Native),这个计数器值则为空(Undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有任何 OutOfMemoryError 情况的区域。
生命周期:程序计数器的生命周期与线程的生命周期一致。
三、虚拟机栈(VM Stack)
虚拟机栈是线程私有的内存区域,由多个栈帧(Stack Frame)构成,在当前线程中,每调用一个方法,都会有一个对应的栈帧进行入栈,用于存储方法调用的局部变量、操作数栈、动态链接、方法出口等信息。当方法结束时,对应的方法的栈帧就会进行出栈。每个线程在创建时都会分配一个栈。
虚拟机栈包含以下几个部分:
局部变量表:用于存储方法参数和局部变量。
操作数栈:用于存储方法执行过程中的操作数。
动态链接:指向运行时常量池中该方法的引用。
方法出口:记录方法返回的地址。
一些附加信息
生命周期:栈的生命周期与线程的生命周期一致。线程启动时创建栈,线程结束时销毁栈。
设置大小:Java 虚拟机规范允许 虚拟机栈的大小是动态的或者是固定的。可以通过参数-Xss
来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
当栈达到最大栈空间限制时,又要进行新的方法调用(入栈操作)时,就会抛出 StackOverflowError 异常。
当线程创建的时候,没有足够的空间穿件对应的虚拟机栈时,就会抛出 OutOfMemoryError 异常。
四、本地方法栈(Native Method Stack)
本地方法栈与栈类似,但它是为本地方法(Native Method)服务的。本地方法是用其他语言(如 C、C++)编写的方法。
生命周期:本地方法栈的生命周期与线程的生命周期一致。
常见问题也与虚拟机栈类似。
五、堆(Heap)
堆是 JVM 中最大的一块内存区域,是存储 Java 程序中绝大多数的对象实例。是所有线程共享堆内存。Java 虚拟机规范中,把堆进行了划分,主要分为:新生代、老年代、元空间(JDK 8 之前是永久代)。
生命周期:堆的生命周期与 JVM 的生命周期一致。JVM 启动时创建堆,JVM 关闭时销毁堆。
设置大小:堆的大小可以通过-Xmx
(最大堆)、-Xms
(初始堆)控制大小。
新生代(Young Generation)
新创建的对象一般首先分配在新生代中,除非对象过大直接进入老年代。新生代又分为 Eden 区和两个 Survivor 区(通常称为 S0 和 S1)。默认占比是 8 : 1 : 1。正常情况下两个 Survivor 区总有一个是空的。后面聊垃圾回收的时候,我们可以细聊。
当 Eden 区满了,会执行 Minor GC,将 Eden 区和当前使用的 Survivor 区的幸存对象移动到空的 Survivor 区中。每经历过一次 Minor GC,幸存的对象其年龄计数器 +1,达到阈值(默认15)后会晋升至老年代。
老年代(Old Generation)
老年代通过是内存满时执行垃圾回收,如果回收后依旧没有空出空间,新对象无位置存储,则会抛出 OutOfMemoryError 异常。
元空间(Metaspace)
存储类信息、常量池等。
元空间可以通过-XX:MetaspaceSize
(初始大小)、-XX:MaxMetaspaceSize
(最大限制)控制大小。
不论是 JDK 8 之前的永久代,还是 JDK 8 之后的元空间,其实都可以理解为虚拟机规范中方法区的具体实现。
虽然虚拟机规范把方法区描述为堆的一部分,但是它其实有一个别名 Non-Heap(非堆)。具体的我们详见方法区部分。
六、方法区(Method Area)
方法区是虚拟机规范中定义的一个概念,是所有线程共享的内存区域,主要用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据。然后虚拟机规范中并没有规定如何去实现它,不同的虚拟机厂商有不同的实现。
永久代(PermGen)则是 Hotspot 虚拟机特有的一个概念,在 JDK 8 中,又被改为元空间。
其实永久代合元空间都可以理解为是方法区的具体实现。
生命周期:方法区的生命周期与 JVM 的生命周期一致。当 JVM 启动时,方法区被创建;当 JVM 关闭时,方法区被销毁。
方法区主要包含以下几个部分:
类信息:包括类的名称、访问修饰符、字段描述、方法描述等。
运行时常量池:用于存储编译期生成的各种字面量和符号引用。
静态变量:类级别的静态变量。
即时编译器编译后的代码:JIT(Just-In-Time)编译器将热点代码编译为本地机器代码后存储在此。
设置大小:JDK 8 之后,可以使用参数 -XX:MetaspaceSize
和 -XX:MaxMetaspaceSize
指定。
方法区的大小决定了JVM 可以加载多少个 Class,如果加载的 Class 过多,则会抛出 OutOfMemoryError
异常。如果不指定大小的情况下,默认虚拟机会耗尽系统所有的可用内存。
-XX:MetaspaceSize
:设置初始的元空间大小。64 位 JVM默认的 -XX:MetaspaceSize=2075MB
,这是初始大小,当达到这个大小后,将会触发 Full GC(Stop the world),卸载无用的 Class(Class 对应的ClassLoader 不再存活),并重置元空间大小(不超过 -XX:MaxMetaspaceSize
),重置的值取决于 Full GC 后释放了多少的元空间。
如果初始化-XX:MetaspaceSize
过低,元空间的大小会发生多次调整,为了避免因为元空间初始大小过小导致的频繁 Full GC,建议将 -XX:MetaspaceSize
设置为一个相对较高的值。
架构设计之道在于在不同的场景采用合适的架构设计,架构设计没有完美,只有合适。
在代码的路上,我们一起砥砺前行。用代码改变世界!
如果有其它问题,欢迎评论区沟通。
感谢观看,如果觉得对您有用,还请动动您那发财的手指头,点赞、转发、在看、收藏。
高并发解决方案与架构设计。
海量数据存储和性能优化。
通用框架/组件设计与封装。
如何设计合适的技术架构?
如何成功转型架构设计与技术管理?
在竞争激烈的大环境下,只有不断提升核心竞争力才能立于不败之地。
留言【我要晋级】,一对一指导,带你晋级。