zoukankan      html  css  js  c++  java
  • JVM内存模型

    一、    内存分区

    java运行时的数据区域分为以下几个部分:程序计数器、方法区、虚拟机栈、本地方法栈、堆五部分。

    程序计数器:属于线程私有的内存,是内存中一块较小的区域,记录当前线程所执行字节码的行号。主要为字节码解释器选取下一条需要执行的字节码执行、分支、循环、跳转、异常处理、线程恢复等基础功能。

    Java虚拟机栈:属于线程私有内存,描述的是Java方法执行的内存模型,每个方法被执行的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。其中局部变量表指编译期可知的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress。局部变量表所需空间是在编译期完成分配的,方法在运行期间不会改变局部变量表的大小。

    参数:-Xss

    内存溢出类型StackOverflowError,造成的可能原因递归次数太多导致发生栈溢出。

    参考链接:https://www.cnblogs.com/minisculestep/articles/4934947.html

    本地方法栈:与虚拟机栈作用比较类似,其是为虚拟机执行Native方法服务,在Sun HotSpot虚拟机中两者直接合为一体。

    Java堆:属于线程共享内存,是java虚拟机管理的最大的一块,虚拟机启动时创建,存放对象实例。这里就涉及到堆的分代以及垃圾回收。

    参数:-Xms20M –Xmx40M,最小堆内存为20M,最大堆内存为40M

    内存溢出类型OutOfMemoryError,造成内存溢出的常见原因是由于某些对象生命周期过长,持有状态时间过长。

    方法区(永久代):属于线程共享内存,存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码数据。在Java1.8以后,永久代已经被移除,取而代之的是元数据区metaspace

    参数:-XX:PermSize和-XX:MaxPermSize限制方法区的大小。

    内存溢出类型OutOfMemoryError中带有提示信息”PermGen space”。

    运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。同时,也可存储运行期间产生的常量,如String类的intern()方法。

    直接内存:并不是虚拟机运行时的数据区的一部分,也不是Java虚拟机规范中定义的内存区域,是在JDK1.4中引入了NIO(New Input/Output)类,引入了一种基于通道与缓冲区的IO方式,可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存引用进行操作。

    参数:-XX:maxDirectMemorySize

    二、    对象访问

    对象访问在Java中无处不在,是最普通的程序行为,即使最简单的访问,也会涉及Java栈、Java堆、方法区这三个重要的内存区域。举例说明:

    Object obj = new Object();

    其中:

    ”Object obj”这部分语义将反应在Java栈的本地变量表中。作为一个reference类型数据出现。

    “new Object()“ 这部分语义将反映到java堆中,形成一块存储了Object类型所有势力数据值的结构化内存。在java堆中还必须包含能查找到此对象数据类型(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。

    Reference类型在java虚拟机规范中只规定了一个指向对象的引用,主流的访问方式有两种:使用句柄和直接指针。

    •  句柄访问方式,Java堆中会画出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

    • 直接指针访问方式,java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址。

     

           这种两方式各有优势,使用句柄访问方式的好处是,存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,reference本身不需要被修改。而使用直接访问指针的访问方式的最大好处就是速度快,节省了一次指针定位的时间。当前Sun HopSpot使用的是第二种访问方式。

    三、    垃圾收集器

    1.     判断对象存活算法

    引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就增加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。

    存在的问题是:难以解决对象之间相互循环引用的问题。

    如:

    public class Test

    {

        Object instance = null;

       

        public static void main(String[] args)

        {

            Test test1 = new Test();

            Test test2 = new Test();

            test1.instance = test2;

            test2.instance = test1;

            //及时test1、test2已经置为null了,但是由于两者的instance的引用并没有消除,所以计数器的值仍然是1,所以不会对其回收。

            test1 = null;

            test2 = null;

        }

    }

    下图是个人的理解的结构图:

     

    根搜索算法(GC Roots Traceing):Java使用该算法来判定对象是否存活的。算法基本思路通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

    GC Roots的对象包括以下几种:

    •   虚拟机栈(栈帧中的本地变量表)中引用的对象
    •   方法区中的类静态属性引用的对象
    •   方法区中的常量应用的对象
    •  本地方法栈中JNI(即一般说的native方法)的引用的对象

     

           在使用跟搜索算法中不可达的对象,并非是一次判断就宣告死亡的。其至少经历两次标记过程,才会被真正的进行垃圾回收。逃脱的办法是在finalize方法中将自己赋值给一个引用链上的任一对象。但是仅能逃脱一次,因为finalize方法最多只能被执行一次。

    2.     收集算法

    标记-清除算法:先标记再清理,造成空间中存在过多的碎片。

    复制算法:两块内存空间,将其中一块中存活的对象,复制到另一块中,然后将原来已经使用过的空间全部清理掉。实现简单,运行高效,但是会造成空间的浪费。现在商业虚拟机新生代采用的此种收集算法,但是,并不是按照1:1的关系划分的。在新生代会划分出三块内存,分别为eden和两个survivor空间,这样仅会浪费10%的空间。比例为8:1:1

    Eden

    Survivor0

    survivor1

    标记-整理算法:将所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,一般应用在老年代。

    分代算法:根据存活周期,将内存划分为几块,在java中堆可分为新生代和老年代就是根据各个年代的特点采用合适的算法进行垃圾回收。

    3.     垃圾回收器

    垃圾回收器当前主要有以下几个serial 、parallel new、parallel scavenge、cms、serial old 、 parallel old 、 G1几种收集器。

    其中serial 、parallel new、parallelscavenge是年轻的的垃圾回器,根据年轻代的特点,大部分对象存活时间并没有那么久,因此,采用的均是复制算法。其不同点是,serial收集器是单线程的,在单核的环境下会有良好的表现,个人理解,现在不太适合当前多核的环境了。Parallel new是serial的多线程版本,其实现大部分与serial没有太多差异,parallel new存在的另一个原因是,cms收集器仅能与serial收集器同时使用,因此,增加了parallel new收集器,提升年轻代的收集效率。Parallel scavenge收集器,是注重效率优先的收集器。怎么理解效率优先呢?也就是最大利用CPU来提升业务代码执行的时间,而非是尽量缩短GC停顿的时间(从知乎上看到了其不能与CMS同时使用的原因是开发框架的不一致,导致了其两者的不兼容)。Parallel scavenge收集器适用于与用户交互较少的服务端,来提升其吞吐量。

    另外的cms、serial old、parallel old收集器均是老年代的垃圾回收器,serial old采用的是标记-整理的算法,简单的看,其使用的是单线程手机。Parallel old是为了与parallel scavenge配合使用而诞生的多线程垃圾回收器,其使用的是标记-整理算法。接下来是CMS收集器,先谈CMS收集器,其与其他收集器不同的是,它使用的是标记-清楚的算法实现。主要包括以下几个步骤:

    • l  初始标记(CMS initial mark)
    • l  并发标记(CMS concurrent mark)
    • l  重新标记(CMS remark)
    • l  并发清除(CMS concurrent sweep)

    其中,初始标记,仅是标记与GC roots直接关联的对象,速度很快,并发标记则进行GC Roots Tracing的过程,是比较耗时的,重新标记则是修正在并发标记阶段,由于用户程序的运行导致标记变动的那部分对象,之后是并发清除的过程。在整个过程当中,并发标记与并发清除是可以与用户线程一起工作的。

    接下来说下CMS收集器的几个缺点。

    • l  CMS收集器对CPU资源非常敏感。CMS默认的启动的垃圾回收线程数是(CPU数量+3)/4,也就是当CPU数量在4个以上时,最多占用资源不超过25%,但是,如果低于4个的话,则会造成很大的负荷。
    • l  CMS收集器无法处理浮动垃圾。怎么理解浮动垃圾呢?就是由于CMS是并发收集的,也就是在收集过程中,用户的程序还在不断产生垃圾,因此,会产生一些,在本次垃圾回收过程中,无法进行收集的垃圾,这些就被成为“浮动垃圾”。同时,由于是并行运行,所以,需要预留一部分空间给用户的程序运行,可以通过-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数获取更好的性能。但是如果预留的过少,无法满足程序需求,则会出现“Concurrent Mode Failure”,这时临时启动Serial Old这样就会停顿时间更长了。
    • l  产生大量碎片,可以通过提供的参数-XX:+UseCompactAtFullCollection开关在Full GC之后整理一次碎片,但是整理过程又是耗时的,所以,可以通过-XX:CMSFullGCsBeforeCompaction参数来设定多少次不压缩Full GC后,进行异常大压缩的Full GC。
  • 相关阅读:
    Django render与redirect的区别
    前段(一)html基础标签
    mysql数据库多表查询及函数运算符(二)
    前段(四) JS部分
    Cookie、Session、Token、JWT总结
    前段(三)css完整部分
    django杂项 (二)
    Python框架Tornado基础(一)
    Django基础(一)
    博客园美化css/JS代码
  • 原文地址:https://www.cnblogs.com/woniu4/p/8832056.html
Copyright © 2011-2022 走看看