美文网首页
JVM内存管理

JVM内存管理

作者: GoLearning轻松学 | 来源:发表于2022-01-17 13:18 被阅读0次
    JVM内部机构图
    12c60649f7ef5071991588221ff5275.jpg

    Java堆(Heap)是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例。
    方法去(Method Area),是用于存储已被虚拟机加载的类信息、常量静态常量,即编译后的代码数据。
    程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。
    JVM栈(JVM Stacks)虚拟机栈描述的是Java方法执行的内存模型,每个方法被执行的时候都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
    本地方法栈(Native Method Stacks)是与虚拟机栈所发挥的作用非常相似的,其区别不过是虚拟机执行Java方法,而本地方法栈则是为虚拟机使用到的Native方法服务。

    JVM Stack
    c21e02abfcd049d0405aa0eb66a523c.jpg

    对应关系:一个线程对应一个JVM Stack JVM Stack中一组Stack Frame线程每调用一个方法就对应这JVM Stack中JVM Frame的入栈,方法执行完毕或者异常终止对应这出栈(销毁)。
    当JVM调用一次Java方法时,它从对应类的类型信息中得到此方法的局部变量去和操作数栈的大小,并据此分配栈帧内存,然后压入JVM栈中。
    在活动线程中,只有位于栈顶的栈才是有效的,成为当前栈帧,与这个栈相关联的方法称当前方法。

    类的生命周期
    0c907512dee04b77f0dd7d19a01d76a.jpg

    加载:查找并加载Class文件。
    链接:是包括验证、准备、解析。
    验证:是确保被导入的类型的正确性。
    准备:是为类的静态字段分配字段,并用默认值初始化这些字段。
    初始化:将类变量初始化为正确的初始值。
    加站可以知道具体内容,反射获取.class文件中的类信息,Class.forName根据类相关的内容进行一个反射,获取类的相关信息,然后进入第二部链接部分,链接分为三部,第一步是验证,看看我们导入class文件类型是不是一个合法类型,你的字节码是不是正确的,如果是正确的就可以通行,接着下一步,准备操作,准备操作就是为了我们的静态变量以及我们静态字段分配内存空间,为我们的默认值初始化字段,然后就去解析,解析就是将我们的常量符号池里面的一个引用变成直接引用,最后的是初始化,为我们初始化变量赋予正确的处置。最后到达我们的使用,使用完之后,我们的相关类处理完之后就是卸载,协助就是将加载到方法去的数据进行清除,也就是说这个类已经没用了,被卸载掉了,从内存空间里面去掉了。

    对象的创建

    判断对象对应的类是否加载,链接和初始化

    为对象分配内存

    Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,分为两种分配方式:1.指针碰撞 2.空闲列表

    处理并发安全问题

    创建对象时一个非常频繁的操作,所以需要解决并发问题,由两种解决方式:1.CAS算法+失败重试 2.本地线程分配缓冲区

    初始化分配到的内存空间

    将分配到的内存,除了对象头外都初始化为零值。

    设置对象的对象头

    将对象的所属类、对象的HashCode和对象的GC分代数据存储在对象的对象头中。

    执行init方法进行初始化。

    执行init方法初始化对象的成员变量、调用类的构造方法,只有一个对象就被创建出来了。

    在类加载检查通过后,接下来虚拟机将为新生的对象分配内存,对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空i西安的内存都放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把哪个指针向空闲空间那边挪动一段与对象大小相等的距离,这种方式成为“指针碰撞”(Bump thePointer),如果Java堆中的内存并不是整的,已使用的内存和空闲的内存相互交错,那就没办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可以用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式,称为“空闲列表”(FreeList),选择哪一种分配方式是由Java堆是否规整决定的,而Java堆是否规整又由所采用的垃圾收集器上是否带有压缩整理功能决定的,因此,使用Serial、ParNew等待Compact过程的收集器时,系统采用分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

    并发安全问题:我们进行内存分配的时候,可能时由多个线程进行创建对象,页就是说可能存在对象A,也可能存在对象B,对象A在线程1,对象B在线程,线程1我们通过指针碰撞的方式进行分配内存,线程2也通过指针碰撞的方式进行内存分配,但是存在并发的情况,两方都找到了一块叫“xxx”的空间,他们都想拿到这块空间,那怎么办呢?处理这种并发问题,我们由两种处理方式,一种是通过CAS算法+失败重试,第二种是本地线程分配缓冲区。

    CAS算法:(AtomicInteger里使用到)
    CAS:Compare-And-Swap 即比较并交换
    CAS包含了三个操作的数据:
    主内存中的变量值:V
    预估值(可以理解为原来旧的值):A
    更新值(操作后,要更新的值):B
    CAS的特点:当且仅当预估值A=内存值V的时候,才会将V的值更新为B,分则也不操作。
    V==B;V=B;
    使用CAS算法多线程操作的时候有且仅有一个线程操作成功,其他线程都会操作失败,操作失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试,失败的线程采用自旋进行尝试的。
    以AtomicInteger来看看CAS算法,具体的算法代码实现在Natice代码里面,我们来模拟一下

    if(;;){
      int current = get();
      int next = current + 1;
      if(compareAndSwap(current,next)){
       return current;
    }
    }
    

    CAS算法的效率为什么比Synchronized的效率高?
    Synchronized时阻塞算法的,而CAS是非阻塞的,采用的是乐观锁技术,因为阻塞算法是CPU切换的,而CAS是CPU指令操作的,CPU切换时间相对于CPU指令操作来说时间更长。所以使用CAS算法的线程比使用Synchronized的效率高。

    对象在JVM的生命周期

    创建阶段(Create)

    分配内存空间,构造对象,对static成员初始化,递归调用超类的构造方法,调用子类的构造方法,

    应用阶段(In Use)

    当对象被创建,并分配给变量赋值、状态到了应用阶段

    不可见阶段(Invisible)

    在程序中找不到对象的任何强引用

    不可达阶段(Unreachable)

    在程序中找不到对象的任何强引用,并垃圾收集器已经准备好要对该对象的内存空间重新进行分配,这个时候如果该对象重写了finalize方法,则会调用该方法。

    终结阶段(Finalized)

    该对象进入终结阶段,并等到垃圾收集回收该对象空间。

    回收算法

    可达性算法


    可达性算法
    标记清除算法
    标记清除算法

    标记清除算法(mark-and-sweep),就是标记存活的对象,清除死去的对象,如图所示,我们对存活的对象进行标记,标记完毕后,再扫描整个空间,将未被标记的对象进行回收。
    标记整理算法


    标记整理算法

    标记整理算法(mark-and-compact),就是标记存活的对象,整理回收后的空间,如图所示,我们对存活的对象进行标记,标记完毕后,再扫描整个空间,中未被标记的对象进行回收,最后进行整理。

    缺点:1.整理需要时间,进行内存的整理需要挂起线程,才进行内存区域的整理,整理完之后调整重新修改指针,改完之后再解除挂起。2.交换位置需要用到额外的内存空间,进行两次赋值,占用了内存空间也占用了时间。
    总结:虽然标记整理算法解决了之前的碎片化问题,但是如果整理过程非常的多而且内存占用大,效率会比较低,数量多会造成画面卡顿问题。

    Coping算法

    Coping算法

    该算法的提出是为了解决对碎片的垃圾回收,如图所示,它刚开始时把对分成对象空间(from space)和空闲空间(to space),程序再对象空间为实例对象分配空间,对象空间满了,就将每个存活的对象复制到空闲空间,变成了对象空间,原来的对象空间变成了空闲空间。
    coping算法好比:拿到一块空间,比如大小为,然后且一刀,一边分50,使用的一边叫对象空间,没用的叫空闲空间,然后就对对象空间进行操作,分配内存,扫描内存,回收的时候标记,将存活的对象移动到空闲空间,再将对象清空。
    优点:效率高,只进行一次扫描,直接将东西移动就行,而且分配方式使用指针碰撞,不需要访问空闲列表,然后全部删除就好。
    缺点:空间利用率不高,只有50%每次只有50%在操作,另外如果存货数量比较多的话,移动效率会低。

    算法对比

    效率:复制算法 > 标记整理 > 标记清除
    内存整齐度:复制算法 = 标记整理 > 标记清除
    内存使用率:标记整理 = 标记清除 > 复制算法

    分代垃圾回收机制

    为什么要分代?
    分代垃圾回收策略,是基于不同的对象的生命周期不一样,因此生命周期对象可采用不同的回收方法,以便于提高回收效率。
    分类情况:
    年轻代:年轻代用来存放新创建的对象,它们死亡非常快,存在朝生夕死的情况。
    有些对象,创建比较快,回收也比较快,也就是说用了一下就仍了,像局部变量的一些对象,像onDraw()里的,循环里面创建的东西,用完就直接没用了。
    老年代:老年代存放的对象是存活了很久的,如果年轻代对象经历了N次垃圾回收仍然存活,就会被放到老年代中。
    存在非常就的,像Android里面的Application,存活非常久。

    年轻代 -->复制算法
    老年代-->标记清除(如果不需要就使用到空闲列表的方式标记清除算法)、标记整理(如果需要一个大的内存的话,我们要使用到标记整理算法)

    相关文章

      网友评论

          本文标题:JVM内存管理

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