前面章节
JAVA - 大白话探索 JVM - 类加载器(一) https://www.ccode.live/bertonlee/list/6?from=art
JAVA - 大白话探索 JVM - 类加载过程(二) https://www.ccode.live/bertonlee/list/7?from=art
JVM 运行时内存
通过之前的章节, 我们知道. class 类如何加载到内存中, 如图红框
开始讲讲内存空间
先了解 JVM 的周期
JVM 在 java 程序执行时运行, 结束时停止.
一个 java 程序对应开启一个 JVM 进程
JVM 的线程分为两种: 守护线程和普通线程
守护线程属于 JVM 自己使用的线程, 如 GC
普通线程是 java 程序的线程
线程私有数据区
Java 栈(VM Stack)
本地方法栈(NM Stack)
程序计数器及隐含寄存器(Program Counter Register)
线程共享数据区
方法区(Method Area)
Java 堆(Heap)
执行引擎
本地方法接口
本地方法库
你会发现, 这都是些什么?????...... 呃
不着急, 一步一步来
首先, 就是你了, 方法区(Method Area, 线程共享)
类的结构信息和类静态变量都保存在方法区(这样说会不会很抽象, 举个例, 例如运行时常量池, 成员变量和方法数据, 构造函数和普通函数的字节码内容, 还包括一些在类, 实例, 接口初始化时用到的特殊方法. 开发人员在程序中通过 Class 对象中的 getName,isInstance 等方法获取信息时, 这些数据都来自方法区.)
程序中的所有线程共享一个方法区, 简称全局共享
对于 HotSpot 虚拟机, 方法区对应为永久代 (Permanent Generation), 但本质上, 两者并不等价, 仅仅是因为 HotSpot 虚拟机的设计团队是用永久代来实现方法区而已, 对于其他的虚拟机(JRockit,J9) 来说, 是不存在永久代这一概念的.
使用永久代来实现方法区并不是一个好注意, 由于方法区会存放 Class 的相关信息, 如类名, 访问修饰符, 常量池, 字段描述, 方法描述等, 在某些场景下非常容易出现永久代内存溢出. 如 Spring,Hibernate 等框架在对类进行增强时, 都会使用到 CGLib 这类字节码技术, 增强的类越多, 就需要越大的方法区来保证动态生成的 Class 可以加载入内存. 在 JSP 页面较多的情况下, 也会出现同样的问题.
在 JDK1.8 下并没有出现我们期望的永久代内存溢出错误, 而是 Metaspace 内存溢出错误. 这是因为 Java 团队从 JDK1.7 开始就逐渐移除了永久代, 到 JDK1.8 时, 永久代已经被 Metaspace 取代, 因此在 JDK1.8 并没有出现我们期望的永久代内存溢出错误. 在 JDK1.8 中, JVM 参数 - XX:PermSize 和 - XX:MaxPermSize 已经失效, 取而代之的是 - XX:MetaspaceSize 和 XX:MaxMetaspaceSize. 注意: Metaspace 已经不再使用堆空间, 转而使用 Native Memory
还有一点需要说明的是, 在 JDK1.6 中, 方法区虽然被称为永久代, 但并不意味着这些对象真的能够永久存在了, JVM 的内存回收机制, 仍然会对这一块区域进行扫描, 即使回收这部分内存的条件相当苛刻.
呃....... 有点多, 慢慢吸收, 这方法区也需要好好琢磨琢磨, 一不小心溢出就麻烦了.
其次, Java 堆(Heap, 线程共享)
Java 堆是 JVM 所管理的最大一块内存, 所有线程共享这块内存区域, 几乎所有的对象实例都在这里分配内存, 因此, 它也是垃圾收集器管理的主要区域.
从内存回收的角度来看, 由于现在的收集器基本都采用分代收集算法, 所以 Java 堆又可以细分成: 新生代和老年代, 新生代里面有分为: Eden 空间, From Survivor 空间, To Survivor 空间.
有一点需要注意: Java 堆空间只是在逻辑上是连续的, 在物理上并不一定是连续的内存空间.
默认情况下, 新生代中 Eden 空间与 Survivor 空间的比例是 8:1, 可以使用参数 - XX:SurvivorRatio 对其进行配置. 大多数情况下, 新生对象在新生代 Eden 区中分配, 当 Eden 区没有足够的空间进行分配时, 则触发一次 Minor GC, 将对象 Copy 到 Survivor 区, 如果 Survivor 区没有足够的空间来容纳, 则会通过分配担保机制提前转移到老年代去.
何为分配担保机制? 在发送 Minor GC 前, JVM 会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间, 如果是, 那么可以确保 Minor GC 是安全的, 如果不是, 那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小, 如果小于, 直接进行 Full GC, 如果大于, 将尝试着进行一次 Minor GC,Minor GC 失败才会触发 Full GC. 注: 不同版本的 JDK, 流程略有不同.
Survivor 区作为 Eden 区和老年代的缓冲区域, 常规情况下, 在 Survivor 区的对象经过若干次垃圾回收仍然存活的话, 才会被转移到老年代. JVM 通过这种方式, 将大部分命短的对象放在一起, 将少数命长的对象放在一起, 分别采取不同的回收策略.
Java 栈(Stack, 线程私有), 本地方法栈
Java 栈
java 栈中只保存基础数据类型 (四类八种) 和自定义对象引用
存取类型: 先进后出
栈内数据在超出其作用域将自动释放
每个栈是线程私有, 它们的生命周期与线程相同.
每个线程建立一个操作栈, 每个栈又包含若干个栈帧, 每个栈帧对应每个方法调用
栈帧:
局部变量区(方法内基本类型变量, 变量对象指针)
操作数栈区(存放方法执行过程中产生的结果)
运行环境区(动态链接, 方法返回相关信息, 异常捕捉)
本地方法栈
与 JAVA 栈类似
本地方法栈是在程序调用或 JVM 调用本地方法接口 (Native) 时候启用
本地方法非 java 语言编写, 不受 JVM 管理
HotSpot VM 将本地方法栈和 JVM 栈合并了.
程序计数器(线程私有)
概念: 在 JVM 概念模型里, 字节码解释器工作时就说通过改变这个计算器的值来选取下一条需要执行的字节码指令. 分支, 循环, 跳转, 异常处理, 线程恢复等基础功能都需要依赖这个计数器来完成.
Java 虚拟机可以支持多条线程同时执行, 多线程是通过线程轮流切换来获得 CPU 执行时间的, 每条线程都会有独立的程序计数器
如果执行 java 方法, 程序计数器记录 JVM 字节码指令的地址, 如果执行 native, 计数器为空(Underfined)
程序计数器这个内存区域在 JVM 规范中是唯一没有规定任何 OutOfMemoryError 的区域
运行时常量池(Runtime Constant Pool)
方法区的一部分, 用于存放编译期间生成的各种字面量 (int,short 等等) 和符号引用(对象符号引用 Integer,String)
除了编译产生能存入, 运行期间也能将新的常量放入池中(String.intern())
节省内存空间: 常量池中如果有对应的字符串, 那么则返回该对象的引用, 从而不必再次创建一个新对象.
节省运行时间: 比较字符串时,== 比 equals()快. 对于两个引用变量,== 判断引用是否相等, 也就可以判断实际值是否相等
Byte,Short,Integer,Long,Character 这 5 种包装类都默认创建了数值 [-128 , 127] 的缓存数据. 当对这 5 个类型的数据不在这个区间内的时候, 将会去创建新的对象, 并且不会将这些新的对象放入常量池中.
Oracle 对 Java 7 中的常量池做了一个非常重要的改变 - 常量池被重新定位到堆中. 这意味着你不再受限于单独的固定大小内存区域. 所有字符串现在都位于堆中, 与大多数其他普通对象一样, 这使你可以在调整应用程序时仅管理堆大小.
完了......
个人博客文章链接 : https://www.ccode.live/bertonlee/list/12?from=art
先暂时这么多吧, 以上是我个人针对 JVM 的总结, 也方便大家快速理解跟巩固, 有错误的地方望告知下, 谢谢.
来源: https://juejin.im/post/5c5015fff265da616d548e2c