内存怎么管?
JVM 内存模型程序计数器`(Program Counter Register)`Java虚拟机栈`(Java Virtual Machine Stack)`本地方法栈`(Native Method Stacks)`Java堆`(Java Heap 或 GC Heap)`方法区`(Method Area)`运行时常量池`(Runtime Constant Pool)`直接内存`(Direct Memory)`
JVM 垃圾回收概述简单回收策略(按照方法和线程的生命周期回收整个区域)Java堆内存回收可达性分析算法`(Reachability Analysis)`可用作GC Roots的对象
分代收集`(Generational Collection)`理论
hotspot JVM 的垃圾回收细节不同版本jdk 中 hotspot JVM 默认的 垃圾收集器组合JVM 中存在的 7(8)种垃圾收集器垃圾收集器之间的关系Serial收集器ParNew收集器Parallel Scavenge收集器Serial Old收集器Parallel Old收集器CMS收集器Garbage First收集器引用&参考
JVM 内存模型
实际的jvm 不会完全照此模型开发,只要实现此功能即可。
-绿色线程: 一种逻辑上的实现。多数由虚拟机实现,比如 Erlang 的进程其实就是绿色线程,相较于本地线程绿色线程更轻量但会导致多个绿色线程使用一个cpu核心的情况-本机线程: 等价于操作系统线程,线程之间调度由操作系统完成,java中的线程是本机线程。-协程、纤程: 相较于绿色线程,协程更开放,如何调度完全取决于程序员而且更轻量。具体又分为有栈协程和无栈协程,JAVA想实现有栈协程,不过Python已经实现的是无栈协程,两者的优劣可以参考: 理解有栈无栈协程
红色框框是线程私有的区域
发布时间jdk版本主要jvm名称
1996-01-23JDK 1.0Classic VM(默认)1999-03-30JDK 1.2Exact VM、Classic VM(默认)、HotSpot VM2000-05-08JDK 1.3HotSpot VM(默认)、Classic VM2002-02-13JDK 1.4HotSpot VM
后面基本就是HotSpot VM 的天下了
程序计数器(Program Counter Register)
是一块较小的内存空间,它可以看作是当前线程所执行的 字节码的行号指示器,用来确认下一条执行的字节码指令不会 OutOfMemoryError线程私有生命周期等同线程
Java虚拟机栈(Java Virtual Machine Stack)
每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧。用于存储局部变量表、操作数栈、动态连接、方法出口等信 息线程私有生命周期等同线程
本地方法栈(Native Method Stacks)
为虚拟机使用到的本地(Native) 方法服务。主流JVM 将 Java虚拟机栈 和 本地方法栈 合二为一线程私有生命周期等同线程
Java堆(Java Heap 或 GC Heap)
最大的一块区域,在虚拟机启动时创建。是垃圾收集器管理的内存区域。存储各种实例。线程共享
方法区(Method Area)
用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器(JIT)编译后的代码缓存等数据不同jvm 对于 方法区的实现是不同的,网上所说 采用永久代的说法是过时的《Java虚拟机规范》认为该区是 Java堆中的一部分,是个逻辑区域线程共享
运行时常量池(Runtime Constant Pool)
方法区的一部分。用于存放编译期生成的各种字面量与符号引用线程共享
直接内存(Direct Memory)
服务于 NIO(New Input/Output) 的一块内存,避免在Java堆和Native堆中来回复制数据线程共享不是虚拟机运行时数据区,也就是说不在内存模型管理范围之内 -XX:MaxDirectMemorySize 分配直接内存大小
JVM 垃圾回收
概述
对于内存模型的不同区域来说都有针对的垃圾回收策略
简单回收策略(按照方法和线程的生命周期回收整个区域)
对于 程序计数器、虚拟机栈、本地方法栈 这三个区域来说当方法或者线程结束区域自然而然回收,而且对于栈帧来讲是固定的数据结构,占用空间相对固定。不会有碎片化问题。
Java堆内存回收
可达性分析算法(Reachability Analysis)
用于判断对象是否存活,相对于引用计数算法规避了循环引用的问题基本思路就是通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过 程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
可用作GC Roots的对象
在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。在本地方法栈中JNI(即通常所说的Native方法)引用的对象。Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。所有被同步锁(synchronized关键字)持有的对象。反映Java虚拟机内部情况的JM XBean、JVM TI中注册的回调、本地代码缓存等。适应各种垃圾回收优化场景而针对性的加入进来的对象,比如分代收集和局部回收的时候需要的GC Roots集合不同(特定情况下部分老年代会加入)
tip: 如何避免GC Roots 集合过大 ? 现代垃圾收集器减小 stop the word 的过程,在现代垃圾收集器中:除了根节点枚举必须冻结用户线程以外所有的垃圾收集过程都是和用户线程交替进行的,也就是说用户只会感觉变慢而不会感觉停顿
分代收集(Generational Collection)理论
用于决定如何回收被判定已死的对象
标记-清除算法。缺点是会产生内存碎片久而久之可能会导致看起来有空间但大对象却无法分配,而且执行效率不稳定时间复杂度随对象数量增加而增加标记-复制算法。将可用内存分为两部分,回收的时候把不回收的对象复制到另一半内存。然后清空之前的一半内存。优点是没有内存碎片产生,缺点是平白浪费一半内存。
大多数虚拟机都是采用 这个算法思想来处理新生代的回收问题。但是做了优化 将内存分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍 然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间该段引用自深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)
标记-整理算法。在标记完成之后清理的时候移动对象到内存的前端。优点是没有内存碎片产生,缺点是移动对象位置对于指针维护的开销太大,大多用于老年代回收
hotspot JVM 的垃圾回收细节
hotspot JVM 内置多种垃圾搜集器。截止目前hotspot的垃圾回收还是以分代回收思想为主,从最古老的Serial收集器再到里程碑意义的G1垃圾收集器都是如此。 不过随着内存分成小块管理的思想兴起,可以预见未来内存的管理可能会参考数据库页的思想将内存分页管理。而这样做的代价仅仅是多消耗一部分内存来存储管理页的卡表。而优势是可控的停顿时间和大吞吐量
不同版本jdk 中 hotspot JVM 默认的 垃圾收集器组合
JDK版本新生代老年代
1.7Parallel ScavengeParallel Old1.8Parallel ScavengeParallel Old9Garbage FirstGarbage First
JVM 中存在的 7(8)种垃圾收集器
垃圾收集器之间的关系
实线代表垃圾收集器之间可以相互协作,虚线 表示 Serial Old 是作为 G1、CMS收集失败的兜底策略, 因为两款垃圾收集器都不可避免的出现浮动垃圾的问题搜集器所处的 代 就是作用于 新生代 还是 老年代 G1是跨代收集器
Serial收集器
是 hotspot 早期版本中唯一的新生代垃圾收集器
线程算法作用区域
单线程标记-复制算法新生代
特点:收集时暂停所有用户线程
ParNew收集器
实质上是Serial收集器的多线程并行版本
线程算法作用区域
多线程线程标记-复制算法新生代
特点:收集时暂停所有用户线程,在CPU核心数低的情况下性能不如Serial收集器
Parallel Scavenge收集器
在继承ParNew收集器的特点之外,尽可能的缩小GC时用户线程的停顿时间
线程算法作用区域
多线程线程标记-复制算法新生代单线程标记-整理算法老年代
特点:可以自动调节停顿时常和吞吐量,但停顿时常的控制是以牺牲新生代大小获取的,也就是期望短的停顿时间可能会导致频繁GC从而使吞吐量下降。该收集器架构中包含了和Serial Old收集器实现相同的老年代收集器PS M arkSweep收集器(表格中第三行标识的收集器),因为二者高度相似很多资料以Serial Old收集器来描述它
Serial Old收集器
Serial收集器的老年代版本
线程算法作用区域
单线程标记-整理算法老年代
特点:可以自动调节停顿时常和吞吐量,但停顿时常的控制是以牺牲新生代大小获取的,也就是期望短的停顿时间可能会导致频繁GC从而使吞吐量下降。作为CMS收集器发生失败时的后备预案
Parallel Old收集器
是Parallel Scavenge收集器的老年代版本
线程算法作用区域
多线程标记-整理算法老年代
特点:弥补了Parallel Scavenge收集器不能和其他优秀老年代收集器合作的尴尬问题。可以代替Parallel Scavenge收集器中性能低下的Serial Old收集器(PS M arkSweep收集器)
CMS收集器
是一种以获取最短回收停顿时间为目标的收集器。
线程算法作用区域
多线程标记-整理算法老年代
特点:适合应用于web服务和类似场景。采用重复标记-并发清除的方式缩短停顿时间。但会产生浮动垃圾的问题,且收集时会降低吞吐量。在JDK9 的时候已经是即将下线的收集器了
Garbage First收集器
里程碑式的成果,它开创了收集 器面向局部收集的设计思路和基于Region的内存布局形式。采用重复标记-筛选回收的方式达到设计目标。
线程算法作用区域
多线程标记-清除 + 标记-整理all(通过回收集来标定回收区域)
特点:新生代和老年代的大小不固定,由更细力度的Region来决定是新生代还是老年代,并且能建立可预测的停顿时间模型每次回收只回收价值最大的Region,从而在吞吐量和停顿时间之间找到平衡点。大对象即老年代,大多数情况下大对象被G1视作老年代,存储在N个连续的名为Humongous Region的特殊Region中相对复杂的卡表导致内存占用较高且占用更多的运算资源来维护卡表相对细粒度的描述请参考:https://hllvm-group.iteye.com/group/topic/44381#post-272188
引用&参考
[1] https://blog.csdn.net/J080624/article/details/85259041 [2] 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) [3]https://www.zhihu.com/question/62277180/answer/196715976 知乎问答中RednaxelaFX 大佬的回答
本文主要参考 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)一书,但书中内容有些地方颇有争议,并且受限于知识储备,一些个人愚见可能有误。欢迎不同意见留言探讨