文章目录
java 内存结构
java 堆
- 被所有线程共享
- 所有的对象实例以及数组都要在堆上分配
- 细分: 新生代、老年代
- 再细分: Eden、From Survivor、To Survivor
- 配置参数
- -Xmx
- -Xms
- 异常:堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
Java虚拟机栈
- 线程私有
- 生命周期和线程相同
- 描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧
- 存储局部变量表、操作数栈、动态链接、方法出口
- 局部变量表存放了编译期可知的各种基本数据类型(8个基本数据类型)、对象引用(地址指针)、returnAddress类型。
- 异常: 如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,在扩展是无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈
- 本地方法栈与虚拟机栈所发挥作用非常相似
- 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,,而本地方法栈则为虚拟机使用到的native方法服务。本地方法栈也是抛出两个异常。
- 异常:StackOverflowError,OutOfMemoryError
方法区
- 线程共享
- 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 它有个别命叫Non-Heap(非堆)
- 异常: 当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。
运行时常量池
- 存放编译期生成的各种字面量和符号引用
- 具备动态性:Java语言并不要求常量只有在编译期才能产生,运行期间也能把新的常量放到方法区运行常量池。
- 例如:string类的intern方法:将共享池中的字符串与外部字符串进行比较,若池中有与之相等的字符串,则返回池中的字符串,否则将string对象添加到池中,并返回string的对象引用
程序计数器
- 一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
- 虚拟机中唯一没有规定OutOfMemoryError情况的区域。
直接内存
- 不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域
- JDK1.4中新加入的NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
对象的创建过程
垃圾回收算法
程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用System.gc
方法来"建议"执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。
程序计数器、虚拟机栈、本地方法栈 随线程生,随线程灭。
finalize方法作用
垃圾收集器删除对象之前对这个对象调用的。在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。
新生代与老年代
堆的内存模型大致为:
- 新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )
- 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 .
- 绝大多数情况下,对象首先分配在eden区,在新生代回收后,如果对象还存活,则进入s0或s1区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄后,则进入老年代。
判断对象已死
引用计数法
- 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1.
- 很难解决对象之间相互循环引用的问题。
可达性分析法
-
通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
-
GCRoots对象
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
-
引用:
- 强引用:new 对象,只要强引用还存在,垃圾回收器永远不会回收被引用的对象
- 软引用:还有用但非必须的对象。在系统发内存溢出对象之前把软引用对象回收。
- 弱引用:非必须的对象:只能生存到下一次垃圾回收之前。
- 虚引用:无法通过虚引用取得对象实例,存在的目的是在被回收的时候收到一个系统通知。
垃圾回收算法
标记-清除算法
- 概念:该算法有两个阶段。
- 标记阶段:找到所有可访问的对象,做个标记
- 清除阶段:遍历堆,把未被标记的对象回收
- 应用场景
- 老年代,因为老年代的对象生命周期比较长。
- 优缺点
- 是可以解决循环引用的问题
- 必要时才回收(内存不足时)
- 缺点
- 回收时,应用需要挂起,也就是stop the world。
- 标记和清除的效率不高,尤其是要扫描的对象比较多的时候
- 会造成内存碎片
复制算法
- 概念: 一开始就会将可用内存分为两块,from域和to域, 每次只是使用from域,to域则空闲着。当from域内存不够了,开始执行GC操作,这个时候,会把from域存活的对象拷贝到to域,然后直接把from域进行内存清理。
- 应用场景
- 新生代中,因为新生代中每次垃圾回收都有大量对象死去,只有少量存活,对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
- 万一存活对象数量比较多,那么To域的内存可能不够存放,这个时候会借助老年代的空间
标记-整理算法
- 概念:所有存活的对象向一端移动,然后清理掉边界以外的内存。
- 优点:解决内存碎片问题
- 缺点:由于移动了可用对象,需要去更新引用。
分代收集算法
- 新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
- 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清理” 或 “标记-整理” 算法进行回收。
HotSpot算法实现
枚举根节点
当系统停顿下来以后,虚拟机使用 OopMap的数据结构得知哪些地方存放对象引用。这个结构是在类加载完成的时候把对象内什么偏移量上是什么类型计算出来。
安全点
HotSpot没有为每个指令生成OopMap,只在安全点记录这些信息。程序并非在所有地方都能停下来GC,只有到达安全点才能暂停。
安全点的选取策略:能否让程序长时间执行。
如何在GC发生时让所有的线程跑到安全点?
- 抢断式中断: 弃用。首先所有线程中断,发现未到安全点,恢复线程跑到安全点
- 主动式中断: 不对线程操作,设置一个标志,各个线程执行时主动轮询这个标志,发现标志位真,跑到安全点。把自己挂起。
安全区域
引入原因: 对于没有分配CPU时间的线程(sleep或者block)状态,线程无法响应中断请求,无法响应JVM中断请求,走到安全地方挂起。
安全区域:在一段代码中引用关系不会发生变化,在这个区域任何一个地方GC都是安全的。
线程进入安全区域,首先标志自己进入安全区域,GC时,系统不会管这些区域的线程,线程离开安全区域,首先判断系统是否完成根节点枚举,如果完成,继续执行,否则等待。
垃圾收集器
serial收集器
最古老,最稳定以及效率高(没有线程交互开销)的收集器,可能会产生较长的停顿,只使用一个线程去回收。
新生代——串行 复制算法
老年代——串行 标记整理算法
通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
ParNew收集器
Serial收集器的多线程版本。
新生代——串行 复制算法
老年代——并行 标记整理算法
是运行在server模式的虚拟机首选的新生代收集器
除了serial 外唯一能与CMS收集器配合工作
通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
Parallel Scavenge收集器
新生代收集器
新生代收集器,采用复制算法
目标: 达到一个可以控制的吞吐量: 用户运行代码时间/CPU总耗时
参数:
最大垃圾收集停顿时间: -XX:MaxGCPauseMillis
直接设置吞吐量大小: -XX :GCTimeRatio
自动设置 新生代大小,Eden和survivor比例,晋升老年代对象的大小(-XX:PretenurSizeThreshold)等参数,动态调整: -XX:+UseAdaptiveSizePolicy
设置时注意: GC停顿时间缩短是以牺牲吞吐量和新生代空间换来的,如时间设置太短,会导致频繁GC,导致吞吐量下降
Serial Old 收集器
Serial收集器的老年代版本。串行标记整理算法
用途
- JDK1.5以及之前与 Parallel Scavenge 搭配使用(Parallel Scavenge 无法与 CMS配合),但是会拖累 Parallel Scavenge 性能
- CMS收集器的备用方案,在并发收集发生 Concurrent Mode Failure时使用
Parallel Old
Parallel Scavenge收集器 的老年代版本,多线程 标记-整理算法
JDK1.6以后出现,和Parallel Scavenge组合。适用于 注重吞吐量以及CPU资源敏感场合。
CMS收集器
以获取最短回收停顿时间为目标
应用场景:大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
步骤
- 初始标记
- stop the world
- 标记GC Roots能关联的对象
- 并发标记
- 对GC Roots 进行跟踪
- 耗时长
- 可以和用户线程一起工作
- 重新标记
- stop the world
- 修正并发标记期间因用户程序继续运行而导致标记对象发生变动的对象。
- 停顿时间小于初始标记,大于并发标记
- 并发清除
- 耗时长
- 可以和用户线程一起工作
缺点:
- 对CUP资源敏感,并发期间占用一部分线程导致应用程序变慢
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure 导致另一次GC产生。
- 并发清理阶段用户线程还在运行,新的垃圾只好咋下一次GC过程中清理
- 垃圾回收阶段用户线程还在运行,需要预留足够的内存给用户线程。因此不能像其他收集器等老年代满了以后触发回收,JDK1.6以后,老年代占比92% 触发CMS回收,如果8%内存无法满足用户线程,出现Concurrent Mode Failure ,使用Serial Old 收集器,停顿时间很长。
- 阈值设置: -XX:CMSInitiatingOccupancyFraction ,配置过高会产生上述现象。
- CMS是基于标记清除算法实现,产生碎片,给大对象分配带来麻烦,触发FullGC
- -XX:+UseCMSCompactAtFullCollection 默认开启,在CMS顶不住触发FullGC时内存碎片合并整理,无法并发,停顿时间会边长
- -XX:CMSFullGCsBforeCompaction:执行多少次不压缩的FullGC 跟随来一次 带压缩的。默认0,每次FullGC都会压缩。
JVM参数 -XX:+UseConcMarkSweepGC设置
G1 收集器
优势
- 并行与并发
- 在主线程暂停的情况下,使用并行收集
- 在主线程运行的情况下,使用并发收集
- 分代收集(和其他收集器一样),虽然可以不和其他收集器配合就能独立管理整个GC堆。
- 空间整合,整体来看 基于标记-整理,不会产生空间碎片。
- 可预测的停顿,使用者明确指定在 M毫秒的时间片段内,垃圾手机时间不得超过N秒
- 保留新生代和老年代,但不是物理隔离,都是 多个大小相等的独立区域 Region
- 避免在整个Java堆中进行回收,跟踪各个Region藜麦垃圾回收价值(回收空间/所需时间)的大小,后台维护优先列表,每次根据允许收集时间,优先回收价值最大的Region。
- 问题: 不同Region的对象可以互相引用,还是不能避免全堆扫描
- Region对象间的引用和新生代老年代的引用通过 Remebered Set 避免全堆扫描。虚拟机发现 程序对 引用类型的数据进行写操作,产生一个Write Barrier中断写操作,检查引用的兑现是否在不同Region,如果是则记录在 Set,当垃圾回收时,GC根节点的枚举范围加入 Remebered Set 保证不全堆扫描也不会遗漏。
步骤
- 初始标记
- 并发标记
- 最终标记
- 需要停顿线程,但可并行执行
- 筛选回收
- 排序,和和用户线程并发执行。
JVM参数配置
-XX:+PrintGC 每次触发GC的时候打印相关日志
-XX:+UseSerialGC 串行回收
-XX:+PrintGCDetails 更详细的GC日志
-Xms 堆初始值
-Xmx 堆最大可用值
-Xmn 新生代堆最大可用值
-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.
-XX:NewRatio 配置新生代与老年代占比 1:2
含以-XX:SurvivorRatio=eden/from=den/to
总结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,
这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。
-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.