Java内存结构:

Java内存结构

在这里插入图片描述
运行时数据区
内存是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM 内存布局规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行。不同的 JVM 对于内存的划分方式和管理机制存在着部分差异。
下图是 JVM 整体架构,中间部分就是 Java 虚拟机定义的各种运行时数据区域。
**在这里插入图片描述**

  • 线程私有:程序计数器、虚拟机栈、本地方法区
  • 线程共享:堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、代码缓存)

一、程序计数器

通过下面两个问题,理解下PC计数器

  1. 使用PC寄存器存储字节码指令地址有什么用呢?为什么使用PC寄存器记录当前线程的执行地址呢?
    因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

  2. PC寄存器为什么会被设定为线程私有的?
    多线程在一个特定的时间段内只会执行其中某一个线程方法,CPU会不停的做任务切换,这样必然会导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以为每个线程都分配了一个PC寄存器,每个线程都独立计算,不会互相影响。

程序计数器的特点

1.线程隔离性,每个线程工作时都有属于自己的独立计数器。
2.执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址
3.执行native本地方法时,程序计数器的值为空(Undefined)。因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。
在这里插入图片描述
小结:

· 它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
· 在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期一致
· 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。如果当前线程正在执行的是 Java 方法,程序计数器记录的是 JVM 字节码指令地址,如果是执行 native 方法,则是未指定值(undefined)
· 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
· 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
· 它是唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域

二、虚拟机栈

2.1:概述:

Java 虚拟机栈(Java Virtual Machine Stacks),早期也叫 Java 栈。每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。

作用:主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
特点:

  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
  • JVM 直接对虚拟机栈的操作只有两个:每个方法执行,伴随着入栈(进栈/压栈),方法执行结束出栈
栈不存在垃圾回收问题

Java 虚拟机规范允许 Java虚拟机栈的大小是动态的或者是固定不变的

  • 如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常
  • 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个OutOfMemoryError异常

2.2 栈的存储单元

栈中存储什么?

  • 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在
  • 在这个线程上正在执行的每个方法都各自有对应的一个栈帧
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息

2.3 栈运行原理

JVM 直接对 Java 栈的操作只有两个,对栈帧的压栈和出栈,遵循“先进后出/后进先出”原则

2.4 栈帧的内部结构

每个栈帧(Stack Frame)中存储着:

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)(或称为表达式栈)
  • 动态链接(Dynamic Linking):指向运行时常量池的方法引用
  • 方法返回地址(Return Address):方法正常退出或异常退出的地址
  • 一些附加信息

在这里插入图片描述

2.5栈溢出;

package com.baidu.JVM;

public class StackBenefit {

    public static void main(String[] args) {
        StackBenefit stackBenefit = new StackBenefit();
        stackBenefit.A();
    }

    public void A(){
        B();
    }
    public void B(){
        A();
    }
}

在这里插入图片描述

三、本地方法栈

3.1 本地方法接口(JNI)

JNI 百度百科:https://baike.baidu.com/item/JNI/9412164?fr=aladdin

3.2 本地方法栈(Native Method Stack)

https://blog.csdn.net/qq_37692087/article/details/84674170

四、堆

在这里插入图片描述

4.1 内存划分

  1. JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)
  2. 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
  3. 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
  4. 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。

4.2 分区的概念

新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。
老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。

  • Minor GC : 清理年轻代
  • Major GC : 清理老年代
  • Full GC : 清理整个堆空间,包括年轻代和永久代

所有GC都会停止应用所有线程。

OOM:(内存溢出)
若老年代执行了 Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常在这里插入图片描述
元空间:
不管是 JDK8 之前的永久代,还是 JDK8 及以后的元空间,都可以看作是 Java 虚拟机规范中方法区的实现。
虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分开。

4.3 GC 垃圾回收简介

https://blog.csdn.net/lingbo229/article/details/82586822
垃圾回收算法(GC,Garbage Collection)
红色是标记的非活动对象,绿色是活动对象。

- 标记-清除(Mark-Sweep)
GC分为两个阶段,标记和清除。首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。同时会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC。

在这里插入图片描述

  • 复制(Copy)
    将内存按容量划分为两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。缺点需要两倍的内存空间。
    在这里插入图片描述
  • 标记-整理(Mark-Compact)
    也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。此方法避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题。
    一般年轻代中执行GC后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外过多内存空间分配,就需要使用标记-清理或者标记-整理算法来进行回收。

在这里插入图片描述

4.4 为什么会堆溢出?

在年轻代中经过GC后还存活的对象会被复制到老年代中。当老年代空间不足时,JVM会对老年代进行完全的垃圾回收(Full GC)。如果GC后,还是无法存放从Survivor区复制过来的对象,就会出现OOM(Out of Memory)。
OOM(Out of Memory)异常常见有以下几个原因:

  • 老年代内存不足:java.lang.OutOfMemoryError:Javaheapspace
  • 永久代内存不足:java.lang.OutOfMemoryError:PermGenspace
  • 代码bug,占用内存无法及时回收。

OOM在这几个内存区都有可能出现,实际遇到OOM时,能根据异常信息定位到哪个区的内存溢出。
可以通过添加个参数-XX:+HeapDumpOnOutMemoryError,让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。

熟悉了JAVA内存管理机制及配置参数,下面是对JAVA应用启动选项调优配置:

JAVA_OPTS="-server -Xms512m -Xmx2g -XX:+UseG1GC -XX:SurvivorRatio=6 -XX:MaxGCPauseMillis=400 -XX:G1ReservePercent=15 -XX:ParallelGCThreads=4 -XX:
ConcGCThreads=1 -XX:InitiatingHeapOccupancyPercent=40 -XX:+PrintGCDetails  -XX:+PrintGCTimeStamps -Xloggc:../logs/gc.log"
  • 设置堆内存最小和最大值,最大值参考历史利用率设置
  • 设置GC垃圾收集器为G1
  • 启用GC日志,方便后期分析
设置堆内存大小
package com.baidu.JVM;

public class Stack {
    public static void main(String[] args) {

        //返回 JVM 堆大小
        long initalMemory = Runtime.getRuntime().totalMemory() / 1024 /1024;
        //返回 JVM 堆的最大内存
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 /1024;

        System.out.println("-Xms : "+initalMemory + "M");
        System.out.println("-Xmx : "+maxMemory + "M");

        System.out.println("系统内存大小:" + initalMemory * 64 / 1024 + "G");
        System.out.println("系统内存大小:" + maxMemory * 4 / 1024 + "G");
    }


}

测试结果:
在这里插入图片描述
小结:

  • 选择高效的GC算法,可有效减少停止应用线程时间。
  • 频繁Full GC会增加暂停时间和CPU使用率,可以加大老年代空间大小降低Full GC,但会增加回收时间,根据业务适当取舍。

五、方法区

5.1:概念

  • 方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域。

  • 运行时常量池(Runtime Constant Pool)是方法区的一部分。

  • 方法区的大小和堆空间一样,可以选择固定大小也可选择可扩展,方法区的大小决定了系统可以放多少个类,如果系统类太多,导致方法区溢出,虚拟机同样会抛出内存溢出错误

  • JVM 关闭后方法区即被释放

    方法区(method area)只是 JVM 规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,并没有规定如何去实现它,不同的厂商有不同的实现。**而永久代(PermGen)是 Hotspot 虚拟机特有的概念, Java8 的时候又被元空间取代了,**永久代和元空间都可以理解为方法区的落地实现。

5.2:方法区的内存结构

方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
类型信息
对每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation),JVM 必须在方法区中存储以下类型信息

域(Field)信息

  • JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
  • 域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient 的某个子集)

方法(Method)信息
JVM 必须保存所有方法的

栈、堆、方法区的交互关系
在这里插入图片描述

5.3:运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分

在加载类和结构到虚拟机后,就会创建对应的运行时常量池

常量池表(Constant Pool Table)是 Class 文件的一部分,用于存储编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中

JVM 为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的

运行时常量池中包含各种不同的常量,包括编译器就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或字段引用。此时不再是常量池中的符号地址了,这里换为真实地址

运行时常量池,相对于 Class 文件常量池的另一个重要特征是:动态性,Java 语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,String 类的 intern() 方法就是这样的

当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则 JVM 会抛出 OutOfMemoryError 异常

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值