提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一.java内存模型
主要关注运行时数据区,可以分为线程私有区域和共享区域。
线程私有区域: 虚拟机栈,本地方法栈,程序计数器
线程共享区域: 堆,方法区,运行时常量池
1.虚拟机栈
-
每个方法被执行的时候,都会在虚拟机栈中创建一个栈帧用于存储局部变量表、操作数栈、动态连 接、方法出口等信息。 -
局部变量表存放了编译期可知的各种基本数据类型,对象引用。 -
局部变量表中的存储空间以局部变量槽(Slot)来表示, 其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。 -
此区域常见的异常: 如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设置栈容量),将会抛出StackOverFlowError异常。 递归调用没有边界限制就会出现这个异常。
2.本地方法栈
- 本地方法栈与虚拟机栈的作用完全一样,区别是本地方法栈为虚拟机使用的Native方法服务。
- 有些java虚拟机(如Hot-Spot)中,本地方法栈与虚拟机栈是同一块内存区域。
3.程序计数器
4.堆
- JVM 所管理的最大内存区域,所有的对象实例以及数组都要在堆上分配"。
- Java堆是垃圾收集器管理的内存区域 。
- 如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展 时,Java虚拟机将会抛出OutOfMemoryError异常。
5.方法区
-
它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 -
在JDK8以前的HotSpot虚拟机中,方法区也被称为"永久代"(JDK8已经被元空间取代)。 此区域的内存回收主要是针对常量池的回收以及对类型的卸载。 -
JVM规范规定:当方法区无法满足内存分配需求时,将抛出OOM异常。
6.运行时常量池
- Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信 息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面 量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池 中。
字面量 : 字符串(JDK1.7后移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
二. 垃圾回收
1. 怎么找到垃圾?
-
引用计数算法: 给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器 为0的对象就是不能再被使用的,即对象已"死"。 引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采用引用计数 法进行内存管理。 存在对象之间相互循环引用的时候计数器不为0无法GC的问题 -
可达性分析算法 : 核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,也就是从GC Roots到这个对象不可达时,证明此对象是不可用的。
? 这也是Java选用的算法。
哪些可以被当成GC Roots?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中Native方法引用的对象
- 所有被同步锁(synchronized关键字)持有的对象。
总之就是一些程序运行期间一直存在的对象。
可达性算法补充(了解):
即使在可达性分析算法中判定为不可达的对象,也不是“非死不 可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死 亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有 与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛 选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有 覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟 机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为确有必要执行finalize()方法,那么该对象将 会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自 动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。 这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一 定会等待它运行结束。这样做的原因是,如果某个对象的finalize()方法 执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中 的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。 finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对FQueue中的对象进行第二次小规模的标记,如果对象要在finalize()中成 功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬 如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在 第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃 脱,那基本上它就真的要被回收了。
2.回收方法区
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
回收变量的条件:已经没有任何字符串对象引用常量池中的这个常量,且虚拟机中也没有其他地方引用这个字面量 ,就可以回收该变量。
回收无用类的条件:
- 该类所有实例都已经被回收(即在Java堆中不存在任何该类的实例)
- 加载该类的ClassLoader已经被回收
- 该类对应的Class对象没有在任何其他地方被引用,无法在任何地方通过反射访问该类的方法 。
3.垃圾回收算法
? 垃圾回收器都用了分代算法的思想,将GC堆分成两个部分,老年代和新生代
? 新生代:又可以分为Eden区和两个 Survivor 区。新生代的垃圾回收被称为 YGC( Young GC ) ,一般非常频繁,回收速度也比较快。
? 老年代: 老年代垃圾回收又称为OGC(old GC)或者 Major GC 。 频率不高,速度也很慢。
3.1 标记-清除算法 (Mark-Sweep)
- 用于老年代的回收算法
- 算法实现:首先标记处所有需要回收的对象,再统一回收被标记的对象。
- 问题如下图:会导致内存空间变得零散,难以存储大对象。
3.2 标记-整理算法(Mark-Compact)
3.3 标记-复制算法 ( Copying )
**也叫半区复制 Semispace Copying ,因为他将内存按容量划分为大小相等的两块,一半用于存储,一半用于复制和回收 **
- 算法思路:将所有存活的对象复制到另一半区域,然后将这一半全部清理掉。
- 速度快,复制的是存活对象,所以适用于新生代,因为新生代垃圾回收频繁,而且对象往往朝生夕灭。
- 为了效率将可用的内存缩减了一半。
3.4 优化的复制算法
现在的很多虚拟机报货 HotSpot都是采用这种收集算法来回收新生代
将新生代内存划分为 一个较大的Eden(伊甸园)区和两个较小的Survivor(幸存者)区,也叫作Survivor From : Survivor To
HotSpot默认 : Eden:Survivor From : Survivor To = 8:1:1 。
算法实现流程:
- 当Eden区满了,触发第一次YGC,将存活对象复制到 Survivor From区 ,并将Eden区清空。
- 后续再次触发YGC,会扫描Eden区和Survivor From区,将存活对象复制到Survivor To区。
- 再触发就扫描Eden区和Survivor To区,同上。
- 一直存活的对象就会在To和From区复制来复制去, 当次数超过MaxTenuringThreshold(JVM参数参数 默认是15), 就会进入老年代。
4.垃圾回收器
HotSpot的这些垃圾回收器,各有特点,没有最好,只有更适合。
并行GC:表示所有GC线程一起执行
并发GC:表示GC线程和用户线程一起执行
4.1 Serial收集器 (新生代收集器,串行GC)
4.2 ParNew收集器 (新生代收集器,并行GC)
- Serial收集器的多线程版本, 其他和Serial收集器没有区别。
- 搭配CMS收集器,在用户体验优先的程序中使用:
4.3 Parallel Scavenge收集器 (新生代收集器,并行GC)
- 复制算法, 多线程
- Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。 不同于CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,
4.4 Serial Old收集器 (老年代收集器,串行GC)
4.5 Parallel Old收集器(老年代收集器,并行GC)
- 多线程
- 标记-整理算法
- Parallel Scavenge老年代版,也是吞吐量优先。
4.6 CMS收集器(老年代收集器,并发GC)
重新标记:当GC线程标记到B时,线程调度为用户线程执行,用户将B->C关系链去掉,加上了A->C关系链。这时候在GC线程看来,A已经是标记完了的,而从B又不再能找到C,导致C被误认为是垃圾。所以需要回头来 “Stop The World”,然后标记上C这个对象。
浮动垃圾:当GC线程标记到B时,线程调度为用户线程执行,用户将B->C关系链去掉,这时候C就是新制造的垃圾对象,但这一轮GC就处理不了这个对象,只能留到下一次GC,这个垃圾对象就叫浮动垃圾。
4.7 G1收集器(全区域的垃圾回收器)
? 停顿时间模型:能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃 圾收集上的时间大概率不超过N毫秒这样的目标,这几乎已经是实时Java(RTSJ)的中软实时垃圾收集器特征了, G1就做到了停顿时间模型。
将Java堆划分为多个大小相等的独立区域 (Region),每一个Region都可以根据需要,扮演新生代的Eden空间、 Survivor空间,或者老年代空间 。收集器能够对扮演不同角色的Region 采用不同的策略去处理。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。 G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。 每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范 围为1MB~32MB,且应为2的N次幂。 而对于那些超过了整个Region容 量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1 的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
Region作为单次回收的最小单元 , 垃圾收集的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使 用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理 回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。
年轻代的垃圾收集: 依然STW, 使用复制算法, 把Eden区和Survivor区的对象复制到新的 Survivor区域 。
TAMS(Top at Mark Start): 由于GC和用户线程是并发的。 所以在Region中的一部分空间被划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。
SATB(原始快照 snapshot at the beginning): 当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之 后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
G1收集器的运作过程 :
-
初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行YGC的时候同步完成的,所以 G1收集器在这个阶段实际并没有额外的停顿。 -
并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段 耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。 -
最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。 -
筛选回收(Live Data Counting and Evacuation):负责更新Region 的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收 集,然后把决定回收的那一部分Region的存活对象复制到空的Region 中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移 动,是必须暂停用户线程,由多条收集器线程并行完成的。
5.垃圾回收的时机
5.1 Minor GC触发条件:
5.2 Majar GC触发的条件:
- 当对象需要放到老年代,但老年代空间不足时。分别有如下情况:
5.内存分配回收策略
|