JVM

作者: Goooooooooooal | 来源:发表于2018-10-19 10:20 被阅读0次

    JVM内存结构

    1. 程序计数器:程序执行字节码的行号指示器,线程私有,不会出现内存溢出[OOM]的区域
    2. JVM栈:线程私有,代表程序的执行过程。程序执行时为当前执行的方法创建栈帧,栈帧包括局部变量表,操作数栈,动态链接等
      [
      JVM规范中,规定JVM栈有2种异常
      (1)线程请求的栈深度>JVM运行的深度,StackOverflowError
      (2)JVM动态扩展栈时,无法申请到足够的内存空间,OutOfMemoryError
      当栈空间无法分配时,到底是分配内存太小,还是已使用的栈空间太大,本质上是对同一件事的不同描述;不管是分配内存太小,还是已使用的栈空间太大,单线程环境下都抛出StackOverflowError,多线程环境下都抛出OutOfMemoryError
      ]
      (1)局部变量表: 存放编译期可知的基本数据类型和对象引用,[局部变量表所需的内存空间,在编译期就可以确定,当Java程序被编译成.class二进制文件时,就确定了局部变量表的容量,运行时,不会改变局部变量表的大小]
      (2)操作数栈: 栈的深度在编译期可知,[1个方法刚开始执行时,操作数栈是空的。随着程序的执行,会有各种字节码指令(如加操作,赋值操作)从操作数栈读或写数据,读或写操作对应着操作数栈的入栈和出栈]
    3. 堆:线程共享,几乎所有的对象和数组都在堆上分配内存空间,代表程序的存储结构,GC活动最频繁的区域。从分代回收的角度看,分为新生代和老年代,新生代又分为Eden区,From Suivivor区和To Suivivor区
    4. 方法区:堆逻辑上的1部分,为了和堆区分,又称为非堆/永久代,线程共享。用来存放类信息,static静态变量,常量[和JIT即时编译后的代码]
    5. 运行时常量池:方法区的一部分,[.class文件有1项信息,叫常量池,用来存放编译器生成的字面量和符号引用,这部分信息在类加载后,会存放在运行时常量池;运行时常量池具有动态性,Java并不要求常量必须在编译期产生,运行时也有新的常量放入常量池]

    [
    永久代在JDK1.8及以后,被干掉了,取而代之的是元空间MetaSpace;元空间在本质上还是和永久代类似,都是对JVM规范中方法区的实现;永久代和元空间最大的区别是,永久代使用堆内存,元空间使用本机物理内存,受本机物理内存大小的限制

    有2个参数MetaspaceSize和MaxMetaspaceSize

    MetaspaceSize是初始大小,达到该值会触发GC,JVM会动态调整MetaspaceSize的值,如果释放了大量内存空间,则会调低MetaspaceSize,否则会调高,但不会超过MaxMetaspaceSize
    MaxMetaspaceSize是元空间的最大大小,默认无限制

    ]

    1. 本地方法栈,[与JVM栈的作用类似,只不过JVM栈用来执行Java方法,本地方法栈用来执行native方法]

    引用类型

    1. 强引用,一般声明对象时使用的引用,例如Object obj = new Object(),在GC时, 如果强引用指向的对象和GC Roots引用链上的任何1个对象有关联,它就永远不会被回收
    2. 软引用,在GC时进行判定,若内存空间足够,则不会回收软引用;若不够,对软引用进行回收。一般用作缓存,若程序发生OOM内存溢出,则肯定不会存在软引用

    Object obj = new Object();
    SoftReference<Object> sf = new SoftReference<Object>(obj);

    1. 弱引用,GC时,不管内存空间是否足够,都会对弱引用进行回收。一般用在缓存,生命周期为1次GC

    Object obj = new Object();
    WeakReference<Object> wf = new WeakReference<Object>(obj);

    1. 虚引用,幽灵引用,无法通过引用获得对象,存在的意义是在GC时,收到通知

    GC策略

    分代回收,新生代使用"复制算法",老年代使用"标记-整理算法"

    GC回收的算法包括,"标记-清除"算法、"复制"算法和"标记-整理"算法,"标记-清除"算法是最基本的回收算法,其它两个都基于它

    "标记-清除"算法,分为标记和清除两个阶段,标记阶段使用"可达性"算法对需要GC的对象进行标记,清除阶段将标记阶段标记的对象进行清除。该算法的缺点是:会使GC后,堆中出现大量不连续的内存,如果再分配占用空间特别大的对象,会因为没有连续内存空间,而不得不提前触发GC

    新生代使用"复制"算法,新生代的特点是,对象朝生夕灭,存活率低,所以新生代GC(也叫Minor GC)发生的非常频繁,针对这个特点,新生代GC使用"复制"算法。将新生代分为3部分,Eden区,From Survivor区和To Survivor区,默认Eden区是Survivor区的8倍,即将新生代分为10份,Eden区占8份,2个Survivor区各占1份;为对象分配内存空间时,只使用Eden区和1块Survivor区;GC时将使用的Eden区和Survivor区上仍然存活的对象,复制到另1块Survivor区上。"复制"算法解决了"标记-清除"算法会出现大量不连续内存的问题,但是浪费了10%的新生代内存

    老年代使用"标记-整理"算法,老年代中的对象存活率高,并不像新生代那样频繁的触发GC,如果使用"复制"算法,需要复制的对象会非常多,因此使用"标记-整理"算法,标记阶段使用可达性算法对需要GC的对象进行标记,整理阶段将存活的对象向一端移动,将边界外的对象进行删除,同样解决了"标记-清除"算法会出现大量不连续内存的问题

    [方法区,也就是HotSpot虚拟机中的永久代也会发生GC,主要回收废弃常量和无用的类;但是性价比低于回收堆。在新生代,1次GC回收70%-95%的内存空间,永久代远低于此]

    可达性算法:
    (1)选取GC Roots对象,它们是栈帧局部变量表中引用的对象、方法区中的static静态变量引用的对象,常量引用的对象
    (2)从GC Roots对象开始,向下搜索,形成一条引用链,如果1个对象与引用链上的对象没有任何联系,则将其标记为不可达;被标记为不可达的对象并不意外着,它会被GC,1个对象要经历2次标记,1次筛选才会被回收
    (3)标记之后会进行筛选,筛选的条件是:对象的类是否重写了Object类的finalize(),对象的finalize()是否被JVM执行过
    (4)如果未重写Object类的finalize(),或对象的finalize()已经被JVM执行过,则对象立刻被回收;否则,对象会被放入F-Queue队列, 该队列会被1个优先级比较低的Finalizer线程执行,执行是指触发F-Queue中对象的finalize(),但是不保证执行完毕,因为对象的finalize()可能执行缓慢,也可能死循环,如果永久等待finalize()执行完毕,会导致GC系统崩溃
    (5)finalize()是对象的最后1次机会,在方法中,只有对象与GC Roots引用链上的任何1个对象建立关联,就不会被回收,否则会被GC回收

    对象分配策略

    1. 对象优先在Eden区分配内存空间,当Eden区空间不足时,触发1次Minor GC
    2. 大对象直接进入老年代,大对象是指大数组,或很长的String对象
    3. 长期存活的对象进入老年代,对于每个对象,JVM都定义了1个age年龄计数器,处于新生代的对象每经过1次Minor GC,age+1,当age值达到15时,对象进入老年代
    4. 并不是所有对象的age达到15时,才进入老年代,JVM还会基于动态年龄判定,决定对象是否进入老年代;当Survivor区中相同age的对象大小 > Survivor区大小的一半时,>=age的对象进入老年代

    空间分配担保

    新生代GC称为Minor GC,老年代GC称为Full GC,一般Full GC至少伴随着1次Minor GC,Full GC比Minor GC慢10倍

    最极端的情况下,若Minor GC之后,所有的对象仍然存活,空闲的Survivor区无法容纳Eden区和1块Survivor区中的对象,这时就需要老年代的空间分配担保,把无法容纳的对象放入老年代

    在Minor GC之前,JVM首先检查老年代的最大可用连续内存空间是否>新生代的对象所占内存空间之和;若>,执行Minor GC,因为Minor GC肯定是安全的;若不>,再检查JVM是否允许空间担保失败,若不允许,执行Full GC,若允许,检查之前历次从新生代晋升到老年代对象的平均大小,若老年代最大可用连续内存空间>此值,尝试执行Minor GC,但是是有风险的,否则,执行Full GC

    相关文章

      网友评论

          本文标题:JVM

          本文链接:https://www.haomeiwen.com/subject/qiljzftx.html