最近初读《深入理解java虚拟机》对很多点豁然开朗,建议大家如果时间充裕可以找来看一看,比博客什么的更加深入。
JVM总体架构:
首先,我们需要理解什么是java虚拟机(JVM),JVM是英文Java Virtual Machine的缩写,JVM用于执行经过编译的字节码文件,java的跨平台执行就是依靠JVM的一致性。
代码如何在虚拟机上运行:
xx·Java 源文件—->编译器—->字节码文件(xx.class)—->JVM—->机器码
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够跨平台的原因。
当一个程序从开始运行,这时虚拟机就开始实例化了,所以多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,虚拟机实例之间数据不能共享。
Hotspot JVM(java虚拟机的一种,Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机) 后台运行的系统线程主要有下面几个:
一.JVM内存区域:
内存区域结构思维导图如下:
线程私有:依赖用户线程的启动/结束而创建/销毁(在 HotspotVM 内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的生/死对应)。
线程共享区域:随虚拟机的启动/关闭而创建/销毁。
内存结构如下:
1.程序计数器(线程私有)
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError 情况的区域。
2.虚拟机栈(线程私有)
描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
3.本地方法区(线程私有)
本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native方法服务。
4.堆(Heap-线程共享)-运行时数据区
被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。
被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。
java堆
Java 堆从GC(Garba Collection)的角度还可以细分为: 新生代(Eden区、From Survivor区和To Survivor区)和老年代。
Ⅰ.新生代
a.Eden区 Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
b.ServivorFrom 上一次GC的幸存者,作为这一次GC的被扫描者。
c.ervivorTo 保留了一次 MinorGC过程中的幸存者。
d.MinorGC(复制算法---一种垃圾收集算法)
1: 首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1,年龄达到阈值(默认为15)就移动到老年代中。(如果 ServicorTo空间不足就放到老年区);
2:清空eden、servicorFrom;
3:最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom。(ServicorTo与ServicorFrom循环重复使用)。
Ⅱ.老年代
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC(一种垃圾收集算法) 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC 进行垃圾回收腾出空间。
MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
5.方法区/永久代(线程共享)
常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
HotSpot VM把GC分代收集扩展至方法区, 即使用Java 堆的永久代来实现方法区, 这样HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。
二.垃圾回收与算法详解
思维导图如下
如果你要去回收你会怎么做?我猜你肯定会说,直接丢掉呀!那么你知道哪些是垃圾,哪些是正常数据么?
1.垃圾确定
a.引用计数法
引用和对象是有关联的。如果要操作对象则必须用引用进行。很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关 联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法,如下代码所示a、b。
public static void main(String[] args) {
Test a = new Test();//对象a
Test b = new Test();//对象b
a.instance = b; //a引用b
b.instance = a; //b引用a
a = null;
b = null;
doSomething();
}
b.可达性分析
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。可达的含义是程序运行过程中肯定能够走到该对象,称为可达的,而那种死活都不会执行的对象就是不可达的。
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
1.虚拟机栈中局部变量表中引用的对象
2.本地方法栈中 JNI 中引用的对象
3.方法区中类静态属性引用的对象
4.方法区中的常量引用的对象
备注:感兴趣可以看看四种强度的引用类型,万一呢?(狗头)
2.垃圾回收算法
这一块是确定哪些可以扔之后,确定怎么扔,毕竟电脑不是现实生活,路过垃圾桶就扔了。
(1)标记清除算法(Mark-Sweep)
最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。(哪儿是垃圾就删哪儿)如图:
缺点:
标记和清除过程效率都不高;
会产生大量不连续的内存碎片,导致无法给大对象分配内存。
(2)标记 - 整理
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点: 不会产生内存碎片
缺点: 需要移动大量对象,处理效率比较低。
(3)复制算法(copying)
为了解决Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小 的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图
主要不足是只使用了内存的一半。商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
(4)分代收集算法
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
新生代使用:复制算法
老年代使用:标记-清除或标记-整理算法
3.垃圾收集器
以下是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
(1) Serial 收集器
Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
有点:简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
(2)ParNew收集器
Serial 收集器的多线程版本。
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
(3)Parallel Scavenge 收集器
与 ParNew 一样是多线程收集器。
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
(4)Serial Old 收集器
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
(5)Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
(6)CMS 收集器
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为以下四个流程:
初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
缺点:吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
(7)G1 收集器
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。
通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
G1 收集器的运作大致可划分为以下几个步骤:
初始标记:同上
并发标记:同上
最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
具备如下特点:
空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
JVM内存区域面试基础知识点大概就这么多,细细理解一遍并不难的哈。
如果对您有所帮助就点赞吧!最近找工作,欢迎大家一起交流。还有,收藏≠学会哈!
(文中部分图片源自书本、某大佬博客,侵删)