• jvm--3.内存管理



    5.JVM内存管理
    JAVA虚拟机在执行java程序的过程中,会把它管理的内存分成若干个不同的数据区域。
    ------------------------------------------------------------------------------------—
    | 运行时数据区 |
    | ----------- -------- ----------------- |
    | | 方法区 | | 栈 | | 本地方法栈 | |
    | | | | | | | |
    | ----------- -------- ----------------- |
    | |
    | --------------------------- ----------------- |
    | | 堆 | | 程序计数器 | |
    | | | | | |
    | --------------------------- ----------------- |
    | ⬇️ ⬆️ ⬇️ ⬆️ |
    | ---------------------------- ------------------ ----------------- |
    | | 执行引擎 | | 本地库接口 | ➡️ | 本地方法库 | |
    | | | | | | | |
    | ---------------------------- ------------------ ----------------— |
    |
    | 其中,堆和方法区,是所有线程共有区;
    | 栈,本地方法栈,程序计数器,是线程私有区。
    |------------------------------------------------------------------------------------
    (1) 内存区域
    a.程序计数器(Program Counter Register),线程私有,不会抛出任何内存异常
    I.可以这么理解,当前线程所执行字节码的行号指示器。字节码解释器,就是通过程序计数器的值,来选取下一条要执行的字节码指令(分支、循环、跳转等基础功能都需要依赖计数器)。
    II.java虚拟机的多线程是通过,各个线程之间轮流切换并分配内存来实现的。在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换回来之后能够
    恢复到正确的执行位置。每条线程都需要一个独立的程序计数器。
    III.如果线程正在执行的是一个java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;
    如果正在执行的是native方法,计数器的值为空。

    b.java虚拟机栈 (Java Virtual Machine Stack) , 线程私有,会有 StackOverFlow 和 OutOfMemoryError异常 ,通过-Xss分配内存大小
    I.每个方法在执行时,都会创建一个栈帧(Stack Frame),用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
    每一个方法从调用到执行完成的过程,就对应一个 栈帧 在虚拟机栈中,入栈到出栈的过程。

    II.栈中的局部变量表,所需的内存空间,在编译器间完成分配。在进入一个方法时,这个方法需要在帧(Stack Frame) 中分配多大的内存空间是确定的,在这个方法运行期间,
    不会 改变 局部变量表 所占用 内存空间 的大小。

    III.当java启动一个线程时,虚拟机会计算出这个线程所需要的栈深度,(比如10),当线程请求的栈深度(每调一个方法压一个栈帧,用掉一个栈深度),大于虚拟机给Stack分配的
    栈深度,会抛出StackOverFlow异常。(用javap javap -verbose Test 查看程序的字节码,Code 属性, stack=2 , 可以查看运行的详细过程,包括栈深度,和每个栈帧需要多少个slot)
    当一个可扩展栈(栈有可扩展的有固定长度的,由使用的JAVA虚拟决定的),动态扩展时,无法请求到足够的内存(比如我需要10M内存,但是JVM只给我5M),会抛出,
    OutOfMemoryError异常。

    c.本地方法栈 (Native Method Stack) ,线程私有,会有 StackOverFlow 和 OutOfMemoryError异常,通过-Xss分配内存大小
    I.和java虚拟机栈基本一样。区别不过是,
    JVM Stack 为 虚拟机 执行java方法 服务;
    Native Method Stack 为 虚拟机 执行本地方法服务
    II.也会抛出StackOverFlow 和 OutOfMemoryError异常

    d.java堆 , 线程共享 , 会抛出OutOfMemoryError异常。,通过-Xms分配内存最小值,-Xmx分配内存最大值
    I.存放 对象实例 和 数组。
    II.是垃圾回收的主要区域。
    III. java堆,可以处于物理上的不连续空间,逻辑上连续即可。可动态扩展,通过-Xms控制大小。
    IV.如果堆中没有完成内存分配,并且堆也无法扩展是,将会抛出OutOfMemoryError异常。

    e.方法区 , 线程共享 , 会抛出OutOfMemoryError异常。
    I.用于存储,已经被虚拟机加载的,类的信息、常量、静态变量、编译后的代码等数据
    II.不需要连续的内存,可以选择固定大小和可扩展
    III.当方法区无法完成内存分配需求时,会抛出OutOfMemoryError异常。
    e-slave. 运行时常量池,会抛出OutOfMemoryError异常。
    是方法区的一部分。
    I.Class文件中,除了有类的 版本、 字段、方法、接口、等描述信息外,还有一项就是常量池(Constant Pool Table),用于存放编译期生成的,
    各种字面变量和符号引用(),这部分将在类加载后进入方法区的常量池。
    II.并非预置在Class文件中常量池中的内容,才能进入方法区;运行期间也可能将新的常量放入池中。

    f.直接内存,并不是java虚拟机内存的一部分,而是机器内存的一部分,会抛出OutOfMemoryError异常
    I.NIO引入了一种类似于 通道(Channel) 和 缓冲区(Buffer) ,可以使用Native函数库直接分配堆外内存。
    然后,通过一个存储在java堆中的,DirectByteBuffer对象作为这块内存的引用进行操作。
    这样避免了在java堆和native堆中来回复制数据,提高了性能。
    II.当直接内存和JVM内存之和大于机器内存时,抛出OutOfMemoryError内存。

    (2)对象创建细节
    a.内存分配方式
    I.指针碰撞, 如果java堆中内存是绝对规整的,为对象分配空间的任务,等同于把一块确定大小的内存从java堆中划分出来。
    如果java堆中内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为临界点,那么分配内存就是,
    把指针向空闲空间那边挪动等同于对象大小的距离,这种分配方式成为 指针碰撞。

    II.空闲列表, 如果内存是不规则的,虚拟机就必须维护一个表,记录那些内存块是可用的,分配的时候找到一块足够大的内存块分给对象实例,
    并更新列表的记录,这种方式称为 空闲列表(Free List)

    b.选择哪种分配方式是java堆是否规整决定的,java堆是否规整,是由采用的垃圾收集器是否带有压缩功能决定的。因此,
    使用Serial、ParNew灯光带有压缩(Compact)过程的收集器时,系统采用的分配算法是指针碰撞。
    使用CMS这种基于 Mark-Sweep(标记-移除) 算法的收集器,系统采用的分配算法是空闲列表。

    c.空闲列表问题,及解决方案
    问题:
    首先,堆是线程共有的,所以,当多线程创建对象是,有这样一个问题,当Thread A分配一块内存完成后,还没更新列表,这时Thread B给
    自己的对象分配了同一块内存,这就造成了冲突。
    方案:
    I.对分配内存的动作,进行同步,这种造成性能下降。
    II.每个线程在堆中,预先分配一小块内存作为缓冲区,称为(Thread Local Allocation Buffer , TLAB) ,哪个线程需要给自己的对象分配
    内存,就在自己的TLAB上分配,只有自己的TLAB上分配完了,才需要同步锁定。通过-XX:+/-UseTLAB参数来设定。(性能调优)

    d.内存分配完成后,虚拟机将分配到的内存空间初始化为零值。这一步操作保证了Java代码中可以不赋初始值就可以使用

    e.接下来,虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、对象的hash-code、对象的GC分代年龄,等信息。存在对象头中。
    这样从JVM的角度来说,对象创建完成。

    f.对象的内存布局,对象在内存中的存储可以分为3块区域:
    I.对象头 (Header) : 包括两部分信息,
    第一部分,存储对象自身运行时数据,如HashCode,GC分代年龄,锁定状态标志,线程持有的锁。
    第二部分,类型指针,即对象指向它的 类 元数据的指针,虚拟机通常用这个指针确定对象属于哪个类。
    还有记录数组长度的信息。
    II.实例数据 (Instance Data) : 程序定义的个字段的内容。包括父类和子类。
    III.对齐填充 (Padding) : 占位符,换句话说,就是保证对象大小必须是 8字节(byte) 的整数倍

    g.对象的访问定位
    java程序需要使用栈上面的reference,引用数据来操作堆上的具体对象。
    I. 句柄
    这种方式,Java堆中会分配一块内存,作为句柄池,reference存储的就是对象句柄池地址。
    句柄中包含了对象实例数据 (在堆上),和类型数据(类数据,在常量池)具体地址信息。
    好处:GC后reference不需要修改
    II.直接指针
    reference存储的就是对象地址。
    好处:速度快,节省了一次指针定位开销。
    Sun HotSpot使用直接指针
    (3)堆溢出,OutOfMemoryError 后面跟 Heap
    -Xms 和 -Xmx 设置堆的最大和最小内存
    堆的最小参数 -Xms 和 最大参数 -Xmx 设置为一样,就可以避免堆扩展。
    a.解决思路:
    I.用内存映像分析工具(如,Eclipse Memory Analyzer) 堆Dump出来的堆转储快照进行分析。
    II.分析的重点是确认内存中的对象是否是必要的,即先确认是否有 内存泄漏(Memory Leak,当创建
    的对象没有使用,又无法被GC回收,就是内存泄漏)
    III.如果是内存泄漏,查看泄漏对象到GC Roots的引用链信息,就能找到泄漏对象是通过怎样的路径与GC Roots相关联,
    并导致GC无法自动回收他们的。通过引用链信息,定位到泄漏代码的位置,review代码。
    IV.如果没有内存泄漏,即,内存中的对象都必须存活。那就看虚拟机堆参数(-Xms和-Mmx)和内存相比,看是否还可以调大。
    从代码上检查是否有,某些对象生命周期过长,持有时间过长的情况,尝试优化这些代码,从而减少运行期的内存消耗。
    (4)栈溢出 StackOverFlow
    -Xss设置栈占用内存大小。默认1024K,也就是1M
    a.虚拟机启动时,有栈大小的默认参数,当所有的栈帧(Stack Frame),内存加起来超过栈内存大小时,就会抛出StackOverFlow异常。
    在栈深度,默认情况下,大多数栈深度达到1000-2000帧没有问题,对于普通递归是够用了(但是栈帧大小是不确定的,所以,只能是大多数情况下。)
    b.建立线程数量过多,导致内存溢出
    I.操作系统,分配给每个进程的内存是有限制的。如果给一个java分配了1G内存,
    虚拟机提供了参数,来控制堆和方法区所占用内存大小,如果没有指定栈占用的内存大小,忽略其它,剩余的内存 1G - 堆内存 - 方法区内存,被本地方法栈和虚拟机栈
    瓜分,栈是线程私有的,栈分配的内存越大,可以建立的线程数就越少,建立新线程时候,容易把剩下的内存耗尽。这种情况,可以减少最大堆,和减少栈容量,换取更多
    的线程,避免内存溢出。
    (5)方法区,内存溢出 。OutOfMemoryError后面跟随PermGen
    -XX:PerSize 和 -XX:MaxPermSize限制方法区大小
    String.intern()是一个native方法,作用:如果字符串常量池中,已经包含一个等于此String 对象的字符串,则返回常量池中,代表此字符串的对象。
    否则,将此String对象添加到常量池中。
    a.Spring Hibernate在对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区,容易导致方法区的内存溢出。
    b.JSP第一次运行时,要编译成java类,大量的jsp也有可能导致方法区内存溢出。

    (6) 本机直接内存溢出 OutOfMemoryError Unsafe.allocateMemory
    DirectMemory 容量可以通过:-XX:MaxDirectMemorySize指定,如果不指定,则默认与java堆最大值 (-Xmx)一样。
    如果内存溢出,在堆的Dump文件很小,或者没有明显的异常,又或者程序中使用了NIO,可以考虑是 本机直接内存溢出。

  • 相关阅读:
    个人项目作业
    第一次博客作业
    我和计算机的恩怨情仇
    OO第四单元总结暨学期总结
    OO第三单元总结
    OO第二单元总结
    OO第一单元总结
    提问回顾与个人总结
    [技术博客]使用pylint实现django项目的代码风格检查
    BUAA软工-结对项目作业
  • 原文地址:https://www.cnblogs.com/fubaizhaizhuren/p/5938480.html
Copyright © 2020-2023  润新知