Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。具体区域如下图,其中黄色的是线程私有,其他为线程共享区域。
内存空间较小,当前线程执行指令的指示器。通过计数器来选取下一条执行的指令,分支、循环、跳转、异常指针、线程恢复等基础功能都需要依赖计数器完成。
Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个时刻一个处理器或一个内核都只会执行一条线程中的指令。
是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量、操作数栈、动态连接、方法出口等信息。每个方法被调用完成对应一个栈帧在虚拟机中从入栈到出栈的过程。
和Java虚拟机栈发挥的作用是非常相似的,其区别是虚拟机栈为虚拟机执行Java方法,而本地方法栈则是为虚拟机使用到本地方法服务。
是虚拟机管理内存中最大的一块。存放对象实例,几乎所有的对象实例都在这里分配。
是垃圾收集器管理的内存区域,为每个线程分配一定量的线程缓存区(Thread Local Allocation Buffer, TLAB)以提高对象分配的效率。
可以通过-Xms和–Xmx来设定内存空间的大小
存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码缓存等数据。
在JDK7中HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到JDK8终于废弃永久代的概念,采用本地内存
中实现的元空间
来代替,把JDK7剩余的内容都移动到元空间中。
方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
并不是虚拟机运行时数据区域的一部分,但这部分内存会被频繁使用,而且也能导致OOM异常。
JDK1.4新加入了NIO(New Input/Output)类,引入了一种基于通道与缓冲区的I/O方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。避免了在Java堆和Native堆来回复制数据。
Java通过可达性分析算法
来判定对象是否存活,通过一系列“GC Roots”的跟对象作为起点集,根据引用关系向下搜索,搜索过程所走过的路径为“引用链”。如果某个对象到GC Roots间没有引用链或是GC Roots到这个对象不可达时,证明此对象不可能再被使用。
固定作为GC Roots
的对象包含以下几种:
对象引用分为如下4中
强引用
:有用且必须,垃圾收集器无论如何都回收不掉的对象软引用
:有用非必须,在系统发生内存溢出之前,会把这些对象列进回收范围之中进行二次回收,如果内存还不够则抛出内存溢出。弱引用
:非必须对象,只能生存到下次垃圾收集器发生为止。无论内存是否充足。虚引用
:无法通过虚引用来获取对象实例,唯一目的是对象被回收时收到一个系统通知。 回收策略同弱引用Java一般把堆分为新生代与老年代,在新生代中每次垃圾收集时都会发现大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
跨代引用假说:存在相互引用的对象应该倾向于同时生存或是同时消亡。很少存在跨代引用对象。在跨带引用只需在新生代建立一个全局的数据结构,把老年代划分成若干小块,标识出老年代的哪一快内存存在跨代。此后当发生Minor GC时,只要包含了跨代引用的小块内存里面的对象才会被加入到GC Roots进行扫描。
部分收集(Partial GC):
新生代收集
(Minor GC/Young GC): 发生在新生代的垃圾收集,对象大部分朝生夕死。老年代收集
(Major GC/Old GC): 发生在老年代的垃圾收集,只有少量对象可回收。目前只有CMS收集器会单独收集
老年代的行为。混合收集
(Mixed GC): 收集真个新生代以及部分老年代的垃圾收集器,目前只有G1支持这种行为整堆收集
(Full GC):收集整个Java堆和方法区的垃圾收集。算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成之后统一回收所有被标记的对象。
缺点:第一执行效率不稳定,如果堆中存在大量对象,且大部分需要回收,则需要进行大量的标记和清除动作。第二回收后内存不连续,如果分配大对象时无法找到连续的空间而触发另一次垃圾收集动作。
标记-复制算法也叫半区复杂,将内存划分为两个内存相等的区域,每次只使用其中一块,当这一块用完就讲存活的对象复制到另外一块上面,然后把已用过的内存空间一次性清空。
解决了标记-清除算法面对大量可回收对象执行效率低的问题。
优点:实现简单、运行高效、内存连续
缺点:有一半的空间浪费,且如果存活对象过多,会导致效率低下。
HotSpot虚拟机执行半区复制策略:把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配只是用Eden和一块Survivior。当发生垃圾收集时,将Eden和Survivor中仍然存活的对象复制到另一块Survivor空间上,然后直接清空Eden和已用过的那块Survivor空间。当另一块Survivor空间不足,部分对象会被分配到老年代。默认Eden和Survivor比例:8:1
标记-整理和标记-清除算法一样,但后续步骤不是直接对可回收的对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界外的内存。
标记-清除与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。
HotSpot虚拟机垃圾收集器的搭配使用如下图
虚拟机列表如下:
名称 | 回收区域 | 算法 | 是否多线程 | 是否暂停服务 | 戳发 |
---|---|---|---|---|---|
Serial | 年轻代 | 标记-复制 | 否 | 是 | 新生代收集 |
ParNew | 年轻代 | 标记-复制 | 是 | 是 | 新生代收集 |
Parallel Scavenge | 年轻代 | 标记-复制 | 是 | 是 | 新生代收集 |
Serial Old | 老轻代 | 标记-整理 | 否 | 是 | 整堆收集 |
Parallel Old | 老轻代 | 标记-整理 | 是 | 是 | 整堆收集 |
CMS | 老轻代 | 标记-清除 | 是 | 否 | 老年代收集 |
G1 | 局部 | 标记-复制 | 是 | 否 | 混合收集 |
收集器单线程工作,且在收集期间暂停其他所有工作线程,直到它收集结束。
优势:
是Serial收集器的多线程并行版本,除了同时使用多线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致。
是新生代收集器,基于标记-复制算法实现的收集器,并行收集的多线程收集器。
其他收集器关注目标是尽可能地缩短垃圾收集时用户线程的停顿时间
,而Parallel Scavenge收集器的目标是达到一个可控的吞吐量(Throughput)。 吞吐量是处理器用于运行用户代码的时间与处理器总消耗时间的比值。
控制吞吐量参数:设置吞吐量大小参数 -XX:GCTimeRatio,控制最大垃圾收集停顿时间参数:-XX:MaxGCPauseMillis
Serial收集器的老年代版本,单线程、标记-整理算法。
是Parallel Scavenge收集器的老年代版本,多线程、标记-整理算法实现。
CMS(Concurrent Mark Sweep)收集器是一种已获取最短回收停顿时间为目标的收集器,多线程、标记-清除算法实现。
标记-清除运行过程:
优点:并发收集、低停顿。
缺点:对处理器资源敏感,在并发阶段虽然不会导致用户线程停顿,但会占用一部分线程而导致程序变慢,降低吞吐量。
CMS在并发标记和并发清除阶段,用户线程是继续运行的
,程序在运行自然还会伴随有新的垃圾不断产生,但这部分垃圾是在标记过程之后产生
的,所以CMS无法在当次手机中处理掉他们,只好留在下次垃圾收集时再清理,这部分垃圾就称为“浮动垃圾”
。
同时因用户线程是继续运行的,所以就必须预留足够的呢次提供给用户线程使用,因此不能等老年代几乎完全填满在收集。这导致预留空间过大容易导致收集器被激活
,而预留空间过小容易内存不足无法分配新对象导致并发失败
。
CMS收集器无法处理”浮动垃圾“(Floating Garbage)
而导致内存不足,有可能出现"并发失败(Concurrent Mode Failure
)"是不进而导致另一次完全"Stop The World”的Full GC
的产生,临时启用Serial Old收集器
来重新进行老年代的垃圾收集。
G1将堆划分多个Region,每个Region可以根据需要,扮演新生代的Eden、Survivor空进,或是老年代空间。收集器能够根据不同角色的Region采用不同的策略去处理。
G1收集器之所以能够建立可预测的停断模型,是因为将Region作为最小回收单位,每次回收都是Region大小的整数倍。避免了Java堆中全区域收集。让G1收集器跟踪各个Region里面垃圾堆积的“价值”大小,价值即回收所获得空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停断时间,优先处理回收价值收益最大的那些Region。
G1收集器运作过程
停顿线程
,但耗时段,复制
到空的Region中,在清理掉真个旧Region的全部空间。暂停服务,多线程并发
。