目录git
程序计数器(Program Counter Register)是一块较小的内存空间,它能够看做是当前线程所执行的字节码的行号指示器。字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计算器来完成。github
因为 Java 虚拟机的多线程是经过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个肯定的时刻,一个处理器(对于多核处理器来讲是一个内核)都只会执行一条线程中的指令。故为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,因此这块内存区域是"线程私有"的区域。算法
Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型,每一个方法在执行的同时都会建立一个栈帧(Stack Frame)用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。数组
本地方法栈(Native Method Stack)与虚拟机栈所发挥的做用是很是类似的,它们之间的区别不过是虚拟机栈为虚拟机栈执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。多线程
对于大多数应用来讲,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被全部线程共享的一块内存区域,在虚拟机启动是建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配内存。这一点在 Java 虚拟机规范中的描述是:全部对象的实例以及数组都要在堆上分配,可是随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会致使一些微妙的变化发生,全部对象都分配在堆上也渐渐变得没那么"绝对"了。优化
方法区(Method Area)与 Java 堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把 Java 方法区描述为堆的一个逻辑部分,可是它却有一个别名叫作 Non Heap(非堆),目的应该是与 Java 堆区分开来。线程
方法区也被开发者成为"永久代"(Permanent Generation)。3d
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(Constant Pool Table),用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池中存放。指针
如同名字同样,算法分为"标记"和"清除"两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象,它的标记过程其实在前一节讲述对象标记断定时已经介绍过了。之因此说它是最基础的算法,是由于后续的收集算法都是基于这种思路并对其不足进行改进而获得的。cdn
它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另外一个是空间问题,标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能会致使之后在程序运行过程当中须要分配较大对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。
为了解决效率问题,复制(Copying)算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。这样就是每次只对其中一块内存进行回收,内存分配时也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。
这种算法的代价是将内存缩小为了原来的一半,代价很大。这种算法也在特殊场景中会有很大用处,好比回收新生代的时候,IBM 公司的专门研究代表,新生代的对象 98% 是"朝生夕灭"的,因此不须要按照 1:1 的比例来划份内存区域,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor 空间。当回收时,将 Eden 空间和 Survivor 空间中还存活着的对象一次性地复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才使用过的 Survivor 空间。这里确定有一个具体空间分配比例,HotSpot 虚拟机默认 Eden:Survivor 为 8:1,也就是每次新生代中可用内存为整个新生代的 90%(80%+10%),只有 10% 的内存会被"浪费"。固然,98% 的对象可回收只是通常场景下的数据,JVM 没有办法保证每次回收都只有很少于 10% 的对象存活,当 Survivor 空间不够用时,须要依赖其它内存(这里指老年代)进行分配担保(Handle Promotion)。
内存的分配担保就比如咱们如今使用支付宝里面的花呗,若是咱们信誉很好,在 98% 的状况下都能按时偿还,因而支付宝会默认咱们会在下一月也能按时按量的偿还咱们的预支,只须要有一个担保人能保证若是我下次不能还款时,能够帮助你还钱,那支付宝就认为咱们预支花呗是没有风险的。内存的分配担保也同样,若是另一块 Survivor 空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接经过分配担保机制进入老年代。具体怎么分配担保会在后续分析。
复制收集算法在对象存活率较高时就要进行较多的复制操做,效率将会变低。更关键的时,若是不想浪费 50% 的空间,就须要有额外的空间进行分配担保,以应对使用的内存中全部对象都 100% 存活的极端状况,因此在老年代通常不能直接选用复制算法。
根据老年代存活时间较长的特色,有人提出了另外一种"标记-整理"(Mark-Compact)的算法,标记过程仍然与"标记-清除"算法同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存活对象想一端移动,而后直接清理掉边界之外的内存。
这种算法没有什么新的思想,只是根据对象存活周期的不一样将内存划分为几块。通常把 Java 堆分为新生代和老年代,这样就能够根据各个年代的特色采用最适当的算法。
新生代:复制算法
由于在新生代中,每次垃圾回收时都发现有大批对象死去,只有少许存活,那就选用复制算法,只需付出少许存活对象的复制成本就能够完成收集。
老年代:标记-清理/标记-整理
由于老年代对象存活率高、没有额外空间对它进行分配担保。
更多精彩原创内容请关注:JavaInterview,欢迎 star,支持鼓励如下做者,万分感谢。