文章目录

  • Java运行时内存
  • 1、线程共享内存区
  • 1.1 Java堆区(Heap)
  • 1.1.1 Heap-新生代和老年代
  • 1.2 方法区(Method Area)
  • 1.2.1 元空间(MetaSpace)与永久代(PermGen)的区别
  • 1.3 运行时常量池
  • 2、线程私有内存区
  • 2.1 PC寄存器(计数器)
  • 2.2 Java栈
  • 2.2.1 本地方法栈
  • 2.2.2 递归为什么会引起java.lang.StackOverflowError异常
  • 2.2.3 虚拟机栈过多会引发java.lang.OutOfMemoryError异常
  • 3、性能监控区
  • 4、自动内存管理
  • 4.1 内存分配原理
  • 4.1.1 Java对象内存分配的位置
  • 4.2 new一个普通的java对象过程
  • 4.3 内存分配-Hash碰撞
  • 4.4 快速分配策略
  • 4.5 内存分配-重试机制
  • 4.6 对象初始化
  • 4.7 更新计数器
  • 4.8 逃逸分析
  • 4.9 OOP-Klass模型


Java运行时内存

java程序运行内存图 java运行时内存_Java

1、线程共享内存区

1.1 Java堆区(Heap)

  Java堆区是在JVM启动时创建的,它在实际的内存空间中可以是不连续的;

  Heap是一块用于存储对象实例的内存区;

  存储在Heap的对象可以分为两类:瞬时对象,生命周期较短;另一类则是生命周期较长;

  对于生命周期不同的JAVA对象,应该采用不同的垃圾收集策略,因此产生了分代垃圾回收策略。

  可以通过在启动时自定义分配Heap的大小,-Xmx 和 -Xms,-Xmx:表示Heap的起始内存,-Xms:表示Heap的最大内存,若Heap中的内存超过 -Xmx 指定的 -Xms 时,就会抛出 “OutOfMemoryError” 异常

1.1.1 Heap-新生代和老年代

  Heap再细分可以分为新生代和老年代,新生代又可以划分为:Eden空间、From Survivor空间、To Survivor空间

java程序运行内存图 java运行时内存_JVM_02

1.2 方法区(Method Area)

  方法区和堆区一样,同样是允许被所有的线程共享访问。

  方法区中存储了每一个类的结构信息,如运行时常量池字段、方法数据、构造函数、普通方法的字节码内容、类、实例、接口初始化时需要用到的特殊方法等数据;但是在HotSpot中,方法区只是逻辑上的独立,实际上方法区还是包含在堆内存中的,也就是说,方法区在物理上还是属于Heap的。因此方法区也是在JVM启动时被创建的,但大多数人会把它称为永久代(Permanent Generation),主要原因有两点

  1. 可以通过选项:"-XX:MaxPermSize" 指令,可以设置内存大小进行动态扩展
  2. 不会像Heap中一样频繁的被GC回收,甚至可以显示指定是否需要在程序运行时回收方法区中的数据。不显示指定时,GC的回收目标仅针对方法区中的常量池和内存卸载。

  方法区的内存一旦超过 “-XX:MaxPermSize” 所指定的内存大小,就会抛出 “OutOfMemoryError” 异常。

1.2.1 元空间(MetaSpace)与永久代(PermGen)的区别

  元空间使用本地内存,而永久代使用的jvm的内存,但是Java程序在运行的时候,JVM会动态的分配内存,所以即使使用本地内存,也不会无限去扩大使用。

  MetaSpace相比PermGen的优势:

  • 字符串常量池存在永久代中,容易出现性能问题的内存溢出
  • 类和方法的信息大小难以确定,给永久代的大小指定带来困难
  • 永久代会给GC带来不必要的复杂性
  • 方便HotSpot与其它JVM如Jrockit的集成

1.3 运行时常量池

  运行时常量池属于方法区的一部分,一个有效的字节码文件除了包含类的版本信息、字段、方法、接口等描述信息外,还包含常量池表(Constant Pool Table),运行时常量池就是字节码文件中常量池表的运行时表示形式。

  当类装载器成功将一个类或者接口装载进JVM后,就会创建与之对应的运行时常量池,由于运行时常量池的内存分配来源于方法区,一旦超过方法区的指定最大内存,就会抛出 “OutOfMemoryError” 异常。

2、线程私有内存区

  线程私有区不允许被所有线程共享访问,只允许被所属的独立线程进行访问,包括PC寄存器,Java栈以及本地方法栈

2.1 PC寄存器(计数器)

  JVM中的PC寄存器时对物理PC寄存器的一种抽象模拟。

  如果执行的是Java方法,计数器记录的是正在执行的虚拟机字节码指令地址。如果正在执行的是Native方法,则计数器值为空,undefine。

  native方法是通过C实现并未编译成需要执行的字节码指令。native是通过调用系统指令实现的,系统是如何实现多线程,native就是如何实现多线程的。

  此内存区域是唯一一个Java虚拟机规范中没有规定的任何OutOfMemoryError情况的区域

  当AB两个线程争夺CPU使用权时,A线程先向处理器发出指令,但执行到一般A挂起,由B执行,当B结束时,需要唤醒A,同时就需要知道线程A的执行位置,就可以查看线程A的计数器指令来找到A,计数器和线程是一对一关系,即"线程私有"

2.2 Java栈

  Java栈也被成为虚拟机栈,与计数器一样,是线程私有的,并且生命周期与线程的生命周期保持一致。

  Java栈作用域存储栈帧,而栈帧中存储的就是局部变量表、操作数栈以及方法出口等。

  栈中存储的局部变量表就是用于存储各类原始数据类型、对象引用以及returnAddress类型。

  栈允许被实现成固定大小内存或可动态扩展的内存大小,如果被设定为固定大小,一旦线程请求分配的栈容量超过JVM所允许的最大值时,JVM会抛出一个 “StackOverFlowError” 异常,反之,抛出一个 “OutOfMemoryError” 异常。

2.2.1 本地方法栈

  本地方法栈(Native Method Stack),用于支持本地方法(native方法,使用C/C++代码编写的方法)执行,与Java栈的作用类似

2.2.2 递归为什么会引起java.lang.StackOverflowError异常

  由于栈的深度是固定的,递归过深,每次执行递归都会给栈中压入一个数,最后超过了栈的深度,引发了此异常

2.2.3 虚拟机栈过多会引发java.lang.OutOfMemoryError异常

  windows中虚拟机java线程映射到操作系统的内核线程上,执行算数代码有较大可能会导致系统假死

3、性能监控区

  Prims模块的子模块PERF,函数名称以" PERF_ "开头,用于监控JVM内部的PerfData计数器。

  性能监控区是JVM提供的一块内存共享区,专供外部程序访问这块区域中的PerfData(性能监控数据),以此实现外部程序监控JVM的性能指标。

4、自动内存管理

  C/C++可以在代码层面上随意控制一个对象的生命周期,好处便是自由与灵活,但是弊端也很明显,操作较为复杂、在某种情况下会直接或间接导致程序崩溃,并且难以定位。而可能引起系统崩溃的两大原因就是:内存泄漏和内存溢出。

  内存泄漏:也叫存储渗漏,当需要释放一个链表所引用的空间时,错误的只释放了第一个元素,此时其它元素则脱离了程序的控制范围,那么这一块空间将会永远无法被释放而慢慢累积导致系统无法为新对象分配内存空间导致系统崩溃。

4.1 内存分配原理

4.1.1 Java对象内存分配的位置

  Java对象的内存分配可以在堆外进行,但这是为了降低GC回收频率和回收效率的一种辅助手段,堆区仍是分配/存储对象实例的主要区域。

4.2 new一个普通的java对象过程

  当语法层面上new一个普通的java对象时,JVM首先会检查这个new指令的参数能否在常量池中定位到一个类的符号的引用,然后检查与这个符号引用相对应的类是否已经成功经历过加载、解析和初始化等步骤,当类完成装载后,就已经完全可以确定出创建对象实例时所需的内存空间大小,接下来JVM将会对其进行内存分配。

java程序运行内存图 java运行时内存_JVM_03

4.3 内存分配-Hash碰撞

  JVM在分配内存时,不仅要考虑如何分配、在哪分配,还要考虑GC执行完内存回收是否会在内存空间中产生内存碎片。

  如果内存空间以规整有序的方式分部,正用和未用各占一边,彼此间维系着一个记录下一次分配起点的标记指针,当为新对象分配内存时,只需通过修改指针的偏移量将新对象分配在第一个空闲位置上,这种分配方式叫指针碰撞(Bump the Pointer),反之,则只能使用空闲列表(Free List)执行内存分配

4.4 快速分配策略

  分代分为新生代和老年代,新生代又可分为Eden空间、From Survivor空间和To Survivor空间,在JVM的运行时数据区,堆和方法区是线程共享区域,由于对象实例的创建在JVM中是非常频繁的,所以并发环境下,从堆中划分空间是非线程安全的。因此为了保证数据操作的原子性,还有线程安全的考虑,如果一个类在分配内存前成功完成了类装载步骤后,JVM会优先选择在TLAB(Thread Local Location 本地线程分配缓冲区)中为对象实例分配内存空间。TLAB是Java堆区中一块线程私有区域,它包含在Eden空间内,除了可以避免一系列的线程安全问题外,同时还能提升内存分配的吞吐量,因此将这种内存分配方式称之为快速分配策略。

4.5 内存分配-重试机制

  当然不是所有对象都能成功在TLAB中分配内存,但是JVM会将TLAB作为内存分配的首选。

  开发人员可以通过 “-XX:UseTLAB” 设置是否开启TLAB,TLAB在缺省情况下只占了Eden空间的 1% ,但是可以通过 “-XX:TLAB Waste TargetPercent” 设置TLAB空间占用Eden空间的百分比。

  若对象在TLAB空间分配内存失败,JVM会通过锁机制确保数据操作的原子性,直接在Eden空间中分配内存,如果在Eden空间中也无法分配内存,JVM则会执行Minor GC(清除掉垃圾对象,同时将对象放进To Survivor,每个对象的对象头都包含一个Age(4bit),在Surivivor每熬过一次GC,Age都会 +1 ,当达到一定年龄时(默认是15)JVM就会将其放入老年区。) 直至最终可以在Eden空间中分配内存为止(如果是大对象,则直接在老年代中分配内存)。

java程序运行内存图 java运行时内存_java程序运行内存图_04

4.6 对象初始化

  当为对象成功分配内存后,JVM会首先对分配后的内存空间进行零值初始化,为了确保对象的实例字段在Java代码中不用赋初始值就能直接使用。

4.7 更新计数器

  零值初始化后,JVM会初始化对象头和实例数据,最后将对象引入栈后再更新PC寄存器中的字节码指令地址。

4.8 逃逸分析

  Java堆区不是创建对象内存分配的唯一选择,此时对外存储技术就是利用逃逸分析技术筛选出未发生逃逸的对象,然后避开堆区而直接在栈帧中分配空间。

  逃逸分析是JVM在执行性能优化前的一种分析技术,它的具体目标就是分析出对象的作用域。

  一旦一个对象被定义在方法体内部之后,它的访问权限仅限于方法体内,一旦引用被外部成员引用后,这个对象就发生了逃逸,反之,JVM就会在栈帧中为其分配内存空间,由于在栈帧上分配内存,因此GC就无需执行垃圾回收。栈帧会随着方法的创建而创建、结束而结束。

4.9 OOP-Klass模型

  实例对象由对象头、实例数据、填充数据(可能有)组成,对象头又由Mark Word:存储对象运行时数据 和 Klass Pointer:元数据指向方法区中目标类的类型信息。

java程序运行内存图 java运行时内存_Java_05

  OOP-Klass模型的作用:JVM中对象头是由OOP对象 instanceOopDesc 来表示(数组用ArrayOopDesc),对象头中的元数据指针所指向当前目标对象的目标类型是由 Klass 中的 instanceKlass 表示(数组用arrayKlass)。

  JVM可以通过对象引用准确定位到Java堆区中的instanceOopDesc对象,这样就可以成功访问到对象的实例信息,当需要访问目标对象的具体类型时,JVM则可以通过存储在instanceOopDesc中的元数据指针定位到存储在方法区中的instanceKlass对象上。

java程序运行内存图 java运行时内存_java程序运行内存图_06