天天看点

JVM内存结构以及javaGC回收机制常用算法

引言:转眼春去秋又来,日复一日年复一年的过,又到面试的好时节,本人小白一个偶尔看看书,javaGC回收机制,

还有就是我们GCRoots 可达式算法和引用计数器算法我就不说了,四大引用强软弱虚如有需要

1.JVM内存结构

JVM内存结构以及javaGC回收机制常用算法

由上图可以清楚的看到JVM的内存空间分为3大部分:

1.堆内存

2.方法区

3.栈内存

栈内存(Task)可以再细分为java虚拟机栈和本地方法栈,

堆内存可以划分为新生代和老年代,,新生代中还可以再次划分为Eden区、From Survivor区和To Survivor区。

一部分是线程共享的,包括 Java 堆和方法区;

另一部分是线程私有的,包括虚拟机栈和本地方法栈,以及程序计数器这一小部分内存。

堆内存(Heap)java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。堆是被所有线程共享的区域,实在虚拟机启动时创建的。堆里面存放的都是对象的实例(new 出来的对象都存在堆中)。

此内存区域的唯一目的就是存放对象实例(new的对象),几乎所有的对象实例都在这里分配内存。

堆内存分为两个部分:年轻代和老年代。我们平常所说的垃圾回收,主要回收的就是堆区。更细一点划分新生代又可划分为Eden区和2个Survivor区(From Survivor和To Survivor)。

下图中的Perm代表的是永久代,但是注意永久代并不属于堆内存中的一部分,同时jdk1.8之后永久代已经被移除。

JVM内存结构以及javaGC回收机制常用算法

新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )

默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

方法区(Method Area)方法区也称”永久代“,它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。

在JDK8之前的HotSpot JVM,存放这些”永久的”的区域叫做“永久代(permanent generation)”。永久代是一片连续的堆空间,在JVM启动之前通过在命令行设置参数-XX:MaxPermSize来设定永久代最大可分配的内存空间,默认大小是64M(64位JVM默认是85M)。

随着JDK8的到来,JVM不再有 永久代(PermGen)。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)。

方法区或永生代相关设置

  • -XX:PermSize=64MB 最小尺寸,初始分配
  • -XX:MaxPermSize=256MB 最大允许分配尺寸,按需分配
  • XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled 设置垃圾不回收
  • 默认大小
  • -server选项下默认MaxPermSize为64m
  • -client选项下默认MaxPermSize为32m

虚拟机栈(JVM Stack)

java虚拟机栈是线程私有,生命周期与线程相同。创建线程的时候就会创建一个java虚拟机栈。

虚拟机执行java程序的时候,每个方法都会创建一个栈帧,栈帧存放在java虚拟机栈中,通过压栈出栈的方式进行方法调用。

栈帧又分为一下几个区域:局部变量表、操作数栈、动态连接、方法出口等。

平时我们所说的变量存在栈中,这句话说的不太严谨,应该说局部变量存放在java虚拟机栈的局部变量表中。

java的8中基本类型的局部变量的值存放在虚拟机栈的局部变量表中,如果是引用型的变量,则只存储对象的引用地址。

本地方法栈(Native Stack)

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

程序计数器(PC Register)

程序计数器就是记录当前线程执行程序的位置,改变计数器的值来确定执行的下一条指令,比如循环、分支、方法跳转、异常处理,线程恢复都是依赖程序计数器来完成。

Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。

2.垃圾回收算法

1.标记清除

标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。

在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

JVM内存结构以及javaGC回收机制常用算法

适用场合:

1,存活对象较多的情况下比较高效

2,适用于年老代(即旧生代)

缺点:

1,容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收

2,扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)

2.复制算法

从根集合节点进行扫描,2标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉

JVM内存结构以及javaGC回收机制常用算法

现在的商业虚拟机都采用这种收集算法来回收新生代。

适用场合:

1,存活对象较少的情况下比较高效

2,扫描了整个空间一次(标记存活对象并复制移动)

3,适用于年轻代(即新生代):基本上98%的对象是”朝生夕死”的,存活下来的会很少

缺点:

1,需要一块儿空的内存空间

2,需要复制移动对象

3.标记整理

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。

这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。

JVM内存结构以及javaGC回收机制常用算法

标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。

首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

4.分代收集算法

分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。

在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率搞,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。

JVM内存结构以及javaGC回收机制常用算法

3.垃圾回收机制

年轻代分为Eden区和survivor区(两块儿:from和to),且Eden:from:to==8:1:1。

JVM内存结构以及javaGC回收机制常用算法

jvm内存结构

1)新产生的对象优先分配在Eden区(除非配置了-XX:PretenureSizeThreshold,大于该值的对象会直接进入年老代);

2)当Eden区满了或放不下了,这时候其中存活的对象会复制到from区。

这里,需要注意的是,如果存活下来的对象from区都放不下,则这些存活下来的对象全部进入年老代。之后Eden区的内存全部回收掉。

3)之后产生的对象继续分配在Eden区,当Eden区又满了或放不下了,这时候将会把Eden区和from区存活下来的对象复制到to区(同理,如果存活下来的对象to区都放不下,则这些存活下来的对象全部进入年老代),之后回收掉Eden区和from区的所有内存。

4)如上这样,会有很多对象会被复制很多次(每复制一次,对象的年龄就+1),默认情况下,当对象被复制了15次(这个次数可以通过:-XX:MaxTenuringThreshold来配置),就会进入年老代了。

5)当年老代满了或者存放不下将要进入年老代的存活对象的时候,就会发生一次Full GC(这个是我们最需要减少的,因为耗时很严重)。

4.垃圾回收有两种类型:Minor GC 和 Full GC。

1.Minor GC

对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。

2.Full GC

也叫Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor

GC要慢,因此应该尽可能减少Full GC的次数,导致Full

GC的原因包括:老年代被写满、永久代(Perm)被写满和System.gc()被显式调用等。

结尾:不求大富大贵,只求衣食无忧.