文章目录
- 一、JVM自动内存管理
-
- 1、java运行时数据区
-
- 1.1、程序计数器
- 1.2、虚拟机栈
- 1.3、本地方法栈
- 1.4、java堆
- 1.5、方法区
- 1.6、直接内存
- 二、对象已死的判定算法
- 三、垃圾收集算法
-
-
- 1.标记-清除算法
- 2.标记-复制算法
- 3.标记-整理算法
- 4.分代收集算法
- 四、垃圾收集器
-
-
- 1.Serial收集器
- 2.ParNew收集器
- 3.Parallel Scavenge收集器
- 4.Serial Old收集器
- 5.Parallel Old收集器
- 6.CMS收集器
- 7.G1收集器
一、JVM自动内存管理
JVM自动内存管理机制包含两部分:内存分配和内存回收
1、java运行时数据区
1.1、程序计数器
程序计数器是一块占用内存较少的空间,可以看成是当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变当前程序计数器的值来选取下一条要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都是依赖程序计数器来完成的。Java虚拟机的多线程是通过线程轮流切换,分配处理器的执行时间来完成的,在任意一个确定的时间内,一个处理器只会执行一个线程的指令,因此,为了线程切换后能恢复到正确的执行位置,所以每个线程都有一个独立的程序计数器,每个线程的计数器互不影响,线程私有。
1.2、虚拟机栈
- 虚拟机栈也是线程私有,生命周期与线程相同。虚拟机栈描述的是java的方法执行的线程模型:每个方法被执行的时候都会同步的创建一个栈帧用于储存局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从被调用到执行结束的过程,也就是栈帧从入栈到出栈的过程。
- 局部变量表储存了可知的java基本数据类型,对象引用类型,可能不等同于对象本身,可能是指向对象原始地址的引用指针等
- java虚拟机栈中存在两种异常类型:StackOverFlowError:线程请求的栈深度大于虚拟机允许的最大深度时
- OutOfMemoryError:如果虚拟机的栈容量可以进行动态扩展,当虚拟机申请不到足够的栈内存时,会抛出异常,OOM
1.3、本地方法栈
本地方法栈和java虚拟机栈作用很相似,java虚拟机栈为Java方法(字节码)服务,而本地方法栈则是为虚拟机使用到的本地方法进行服务。我们常用的hot-spot虚拟机直接把本地方法栈和java虚拟机栈合二为一了,所以在hot-spot虚拟机中当本地方法栈无法申请到内存时,也会抛出StackOverFlowError和OutOfMemoryError这两个异常
1.4、java堆
java堆是jvm管理的最大的一片内存区域,也是GC管理的主要区域。java堆是被所有线程共享的一块内存区域,虚拟机启动时堆内存就被创建,堆内存被创建的唯一原因就是存放内存实例。在Java中几乎所有的内存都是在这里被分配的。
1.5、方法区
- 方法区和java堆是一样的,也是线程共享的一块内存区域,它用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据,
- 运行时常量池:运行时常量池也是方法区的一部分,classe文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池表,用于生成编译期间生成的各种字面量和符号引用的这部分内容会在类加载之后存放到方法区的运行时常量池中,运行时常量池相比较于class文件常量池的另外一个重要特征就是具备动态性。java并不要求常量只有编译时才能产生,运行期间也可以把新的常量放入常量池中。
1.6、直接内存
直接内存并不是java虚拟机运行时数据区的一部分,也不是《java虚拟机规范》中定义的一部分,但是这部分内存也是被频繁的使用,而且也有可能导致OOM的发生
在java1.4中加入的NIO类引入了一种通道和缓冲区的I/O方式,可以直接使用Native函数库直接分配堆外内存,然后通过储存在java堆里的DirectByteBuffer对象来作为这块内存的引用进行操作。这样能在一些场景中显著提高性能因为避免了Java堆和Native堆中来回复制数据
二、对象已死的判定算法
程序计数器,虚拟机,本地方法栈都是线程独享,占用的内存都是随线程生命周期。但是堆和方法区是线程共享,是GC关注的部分。
在堆中几乎存在着所有对象,GC之前需要考虑哪些对象还活着不能回收,那些对象已经死了可以回收。
判定对象是否存活有两种算法:
- 引用计数算法:给对象中添加一个引用计数器,每有一个地方引用了对象,计数器加一,当引用失效,计数器减一,当计数器为0表示该对象已死。可回收,但是此种算法无法解决两个对象直接相互循环引用的情况。
- 可达性分析算法:通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连即对象到GC Roots不可达,则证明对象已死,可回收。java中可以作为GC Roots的对象包括:虚拟机栈中引用的对象,本地方法栈中Native方法引用的对象,方法区静态属性引用的对象,方法区常量引用的对象。
三、垃圾收集算法
1.标记-清除算法
分为标记和清除两个阶段,首先标记所需要回收的对象,在标记完后统一回收所有被标记的对象。
不足:效率问题,标记和清除过程的效率都不高,空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收
2.标记-复制算法
将可用内存按容量划分为大小相等的两块,每次只需要使用其中一块,当一块内存用完,将还存活的对象复制到另一块上边,然后把刚才用完的内存空间一次清理掉。
不足:可用内存缩小为原来一半,内存利用率低
3.标记-整理算法
标记过程通标记清除算法,但是在后续步骤不是直接对对象进行清理,而是让所有存活的对象都像一侧移动,然后直接清理掉端边界以外的内存。解决了内存碎皮问题
4.分代收集算法
当前商业虚拟机的GC都是采用分代收集算法,根据对象的存活周期的不同将堆分为新生代和老年代,方法区称为永久代(新的版本已经将永久代废弃,引入了元空间的概念,永久代使用的是JVM内存而元空间直接使用物理内存)
这样就可以根据各个年代的不同采用不同的收集算法
新生代中的对象朝生夕死,每次GC都会有大量对象死去,少量存活,使用复制算法。新生代又分为Eden区和survivor区(survivor from 和survivor to,大小比例默认为8:1:1
老年代中的对象因为对象存活率高,没有额外空间进行分配担保。就使用标记-清除或者标记-整理算法
- 新产生的对象优先进去Eden区,当Eden区满了之后再使用survivor from,当survivor from也满了就进行Minor GC(新生代GC),将Eden和Survivor from中存活的对象copy进入Survivor to,然后清空Eden和Survivor from,这个时候原来的Survivor from成了新的Survivor to,原来的Survivor to成了新的Survivor from。赋值的时候如果Survivor to无法容纳全部存活的对象,则根据老年代的分配担保将对象copy进去老年代,如果老年代也无法容纳,则进行full GC(老年代GC)。
- 大对象直接进入老年代:JVM中有个参数配置-XX:PretenureSizeThreshold,令大于这个设置值的对象直接进入老年代,目的是避免在Eden和Survivor 区之间发生大量的内存复制
- 长期存活的对象进入老年代:JVM给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过一次Minor GC后仍然存活,并且能被Survivor 容纳,将被移入Survivor并且年龄设定为1。每熬过一次Minor GC,年龄就加一,当年龄到达一定程度(默认15,可以通过XX:MaxTenuringThreshold来设定),就会移入老年代。但是JVM并不是永远要求年龄必须达到最大年代才会晋升老年代,如果Survivor 空间中相同年龄(如年龄为x)所有对象大小的总和大于Survivor打一半,年龄大于x的所有对象直接进入老年代,无需等到最大年龄要求。
四、垃圾收集器
垃圾收集算法是方法论,垃圾收集器是具体实现。
JDK7/8后,HotSpot虚拟机所有收集器及组合:
1.Serial收集器
Serial收集器是最基本,历史最久的收集器。是单线程的,只会使用一个cpu或者一条收集线去完成垃圾收集工作,并且在收集的时候,必须暂停其他所有的工作线程,知道他结束。
优点:简单而高效(没有线程切换的开销)
2.ParNew收集器
ParNew收集器是Serial收集器的多线程版本,除了使用了多线程之外,其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同Serial收集器一样。
是许多运行在Server模式下的JVM中首选的新生代收集器,其中一个很重还要的原因就是除了Serial之外,只有他能和老年代的CMS收集器配合工作。
3.Parallel Scavenge收集器
新生代收集器,并行的多线程收集器。它的目标是达到一个可控的吞吐量(就是CPU运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=行用户代码的时间/[行用户代码的时间+垃圾收集时间]),这样可以高效率的利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
4.Serial Old收集器
Serial 收集器的老年代版本,单线程,“标记整理”算法,主要是给Client模式下的虚拟机使用。
另外还可以在Server模式下:
- JDK 1.5之前的版本中雨Parallel Scavenge 收集器搭配使用
- 可以作为CMS的后背方案,在CMS发生Concurrent Mode Failure是使用
5.Parallel Old收集器
Parallel Scavenge的老年代版本,多线程,“标记整理”算法,JDK 1.6才出现。在此之前Parallel Scavenge只能同Serial Old搭配使用,由于Serial Old的性能较差导致Parallel Scavenge的优势发挥不出来
Parallel Old收集器的出现,使“吞吐量优先”收集器终于有了名副其实的组合。在吞吐量和CPU敏感的场合,都可以使用Parallel Scavenge/Parallel Old组合。组合的工作示意图如下:
6.CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。
基于“标记清除”算法,并发收集、低停顿,运作过程复杂,分4步:
- )初始标记:仅仅标记GC Roots能直接关联到的对象,速度快,但是需要“Stop The World”
- )并发标记:就是进行追踪引用链的过程,可以和用户线程并发执行。
- )重新标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要“Stop The World”
- )并发清除:清除标记为可以回收对象,可以和用户线程并发执行
由于整个过程耗时最长的并发标记和并发清除都可以和用户线程一起工作,所以总体上来看,CMS收集器的内存回收过程和用户线程是并发执行的。
CSM收集器有3个缺点:
- )对CPU资源非常敏感并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
CMS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。
- )无法处理浮动垃圾(在并发清除时,用户线程新产生的垃圾叫浮动垃圾),可能出现"Concurrent Mode Failure"失败。并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;
- )产生大量内存碎片:CMS基于"标记-清除"算法,清除后不进行压缩操作产生大量不连续的内存碎片,这样会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。
7.G1收集器
G1(Garbage-First)是JDK7-u4才正式推出商用的收集器。G1是面向服务端应用的垃圾收集器。它的使命是未来可以替换掉CMS收集器。
特点:
并行与并发:能充分利用多CPU、多核环境的硬件优势,缩短停顿时间;能和用户线程并发执行。分代收集:G1可以不需要其他GC收集器的配合就能独立管理整个堆,采用不同的方式处理新生对象和已经存活一段时间的对象。空间整合:整体上看采用标记整理算法,局部看采用复制算法(两个Region之间),不会有内存碎片,不会因为大对象找不到足够的连续空间而提前触发GC,这点优于CMS收集器。可预测的停顿:除了追求低停顿还能建立可以预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超N毫秒,这点优于CMS收集器。
回收过程可以分为4个步骤(与CMS较为相似):
- )初始标记:仅仅标记GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时能在正确可用的Region中创建新对象,需要“Stop The World”
- )并发标记:从GC Roots开始进行可达性分析,找出存活对象,耗时长,可与用户线程并发执行
- )最终标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录。并发标记时虚拟机将对象变化记录在线程Remember Set Logs里面,最终标记阶段将Remember Set Logs整合到Remember Set中,比初始标记时间长但远比并发标记时间短,需要“Stop The World”
- )筛选回收:首先对各个Region的回收价值和成本进行排序,然后根据用户期望的GC停顿时间来定制回收计划,最后按计划回收一些价值高的Region中垃圾对象。回收时采用复制算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;可以并发进行,降低停顿时间,并增加吞吐量。