美文网首页
JVM-内存结构

JVM-内存结构

作者: Audience0 | 来源:发表于2019-03-09 18:44 被阅读0次

    1.Java内存结构
    结构图如下:


    图片.png image.png

    1>方法区:用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,是线程共享的内存区域,也叫做Non-Heap(非堆).---包含各种常量池
    >>异常:当方法区无法满足内存分配需求(-XX:MaxPermSize)时,将会抛出OutOfMemoryError:PermGen space;异常.
    >>JDK1.8 移除了方法区,增加元空间,元空间内存只受本地内存限制,元空间可以在运行时动态调整,可以使用-XX:MaxMatespaceSize 设置本地内存分配给元空间的最大内存,元空间与堆不相连,但是与堆共享物理内存

    -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:-UseCompressedClassPointers -XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m
    第一个参数用于打印GC日志;
    第二个参数用于打印对应的时间戳;
    第三个参数-XX:-UseCompressedClassPointer表示在Metaspace中不要开辟出一块新的空间(Compressed Class Space),如果开辟这块空间的话,该空间默认大小是1G,所以我们关闭该功能,此时再设置Metaspace的大小;


    image.png

    2>堆内存:
    堆是JVM管理的最大的一块内存,是线程共享的,用于存放对象实例以及数组.堆也是垃圾回收器主要管理的区域,也叫做GC堆.
    虚拟机启动时就会创建堆,可通过-Xmx、-Xms调节堆大小


    image.png
    >>异常:如果在堆中没有内存完成实例分配,并且堆也无法进行再扩展(-Xmx,-Xms)了,将会抛出java.lang.OutOfMemoryError: Java heap space异常
    

    堆中对象的创建过程
    当通过 new 创建对象时,首先检查这个new指令的参数是否能在元空间中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,执行相应的类加载;
    类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确定),在堆的空闲内存中划分一块区域(‘指针碰撞《内存规整》’或‘空闲列表《内存交错》’的分配方式);

    1.指针碰撞法
    假设Java堆中内存时完整的,已分配的内存和空闲内存分别在不同的一侧,通过一个指针作为分界点,需要分配内存时,仅仅需要把指针往空闲的一端移动与对象大小相等的距离。使用的GC收集器:Serial、ParNew,适用堆内存规整(即没有内存碎片)的情况下。

    2.空闲列表法
    事实上,Java堆的内存并不是完整的,已分配的内存和空闲内存相互交错,JVM通过维护一个列表,记录可用的内存块信息,当分配操作发生时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录。使用的GC收集器:CMS,适用堆内存不规整的情况下。

    内存分配并发问题
    在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

    CAS: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
    TLAB: 为每一个线程预先分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。

    内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
    执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成;

    对象的内存布局
    在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding)

    对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’;
    第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以;


    image.png

    实例数据(Instance Data):程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的);
    对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍;

    对象的访问
    使用对象时,我们是通过栈上的 reference 引用来操作堆上的具体对象;
    Sun Hotspot虚拟机使用直接指针访问具体对象;


    image.png

    3>Java虚拟机栈:
    是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息.每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈和出栈的过程,该区域是线程私有的,它的生命周期跟线程的生命相同.


    image.png

    局部变量表:是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量;
    操作数栈:也叫操作栈,它是一个先进后出的栈 (FILO),当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是入栈和出栈操作,一个完整的方法执行期间往往包含多个这样入栈/出栈的过程;
    动态链接:一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池,所以需要在运行时动态将这些符号引用转化为直接引用;
    返回地址:方法不管是正常执行结束还是异常退出,需要返回方法被调用的位置;

    >>局部变量表:存放了编译期可知的各种基本类型
    

    (boolean,byte,short,int,long,float,double,char),对象引用(reference类型)
    >>栈一般都不设置大小,栈所占的空间其实很小,可以通过-Xss1M进行设置,如果不设置默认为1M;
    >>该区域不会有GC回收
    >>异常1:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常.
    >>异常2:如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError.

    4>本地方法栈:与虚拟机栈类似,只不过本地方法栈是为虚拟机使用到的native方法服务的
    >>异常:与虚拟机栈一样.
    >>GC不会回收该区域;

    5>程序计数器:是一块较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器.字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖于这个计数器.
    程序计数器是线程私有的,每个线程依赖于每个线程中的程序计数器来确保线程切换后能恢复到正确的执行位置.
    如果,在执行的是Java方法,则这个计数器记录的是正在执行的虚拟机字节码指令的地址.如果是本地native方法,则该计数器记录的为空(Undefined).
    该区域不存在OutOfMemoryError,也不需要GC回收


    相关文章

      网友评论

          本文标题:JVM-内存结构

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