JVM 的内存分配与垃圾回收

JVM 运行时内存区如下,其中白色部分线程私有(java 栈,本地方法栈,程序计数器),蓝色部分为线程共享(方法区,堆)。

这里写图片描述

1. java 堆区

如上图,堆区用于存储对象实例对象的内存区。这部分也是GC(garbage Collection ) 执行垃圾回收的关键区域。
这里写图片描述
jvm 程序运行时内存常用配置参数如下

-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M
-vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了
-Xms128m JVM初始分配的堆内存
-Xmx512m JVM最大允许分配的堆内存,按需分配

2. method area

方法区存储每一个Java 类的结构信息。
运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容以及类、实例、接口、接口初始化需要用到的特殊方法和数据。

-XX:PermSize=64M JVM初始分配的非堆内存
-XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配

运行时常量池:常量包含编译期就能明确的数值字面量

3. PC 寄存器

 线程私有。存储正在执行的字节码指令地址。如果是native 方法,这时PC 寄存器的值就为空。

4. Java虚拟机栈

 线程私有。 存储栈帧(局部变量表、操作数栈和方法出口)

5. 本地方法栈

 线程私有。 用于支持本地方法(C/C++代码编写的方法)

Q: 当使用关键字new 创建一个java 对象时,JVM 如何分配内存?

  1. 首先检查是否在常量池中存在一个与new 字符串参数相同的类的符号引用,然后检查相对应的类是否已经成功经历过加载、解析和初始化等步骤。
  2. 经过第一步的检查能确定这个类所需要的内存大小,接下来进行内存分配。
  3. 基于线程安全考虑,JVM 会优先考虑在本地线程分配的缓存区(Thread Local Allocation, 坚持TLAB)中为对象实例分配内存空间。TLAB 是在Java 堆区中的一块线程私有区域,同时,它属于Eden 空间内。缺省情况,它占Eden 空间的1%。通过 “-XX:TLABWasteTargetPercent” 可以改变此百分比。
  4. 一旦对象在TLAB 空间分配内存失败,JVM 会尝试通过加锁确保直接在Eden 空间中直接分配内存的操作的原子性。如果Eden 区再也没有满足类所需的内存,则JVM 会执行Minor GC。但是,如果对象实在太大,则可以在老年代空间直接分配内存。
  5. 还有一种情况,可以采取逃逸分析分析出对象的作用域。 举例来说,如果定义在方法内的对象并没有被任何的外部成员对象所引用时,JVM 就会为其在栈帧中分配内存空间,这种情况无需垃圾回收,对象随栈帧的弹出而释放掉所占用的空间。

对象内存布局与OOP-Klass 模型

对象实例化分为两部分。

  1. 初始化对象头 (hashcode, GC分代年龄、线程持有的锁、元数据指针等等)
  2. 实例数据(当前对象中各自类型的字段信息)

OOP-Klass 模型是JVM用于表示Java 类以及对象实例的一种数据结构
OOP(Orinary Object Pointer)
Klass(C++对等体)

GC 垃圾回收

垃圾回收之前,要标记内存中哪些是存活对象,哪些是已经死亡。
两种常见的垃圾标记算法。

  1. 引用计数算法。
  2. 根搜索方法。(Hotspot 和大部分JVM采用). 根对象集合包括:Java 栈中的对象引用,本地方法栈的对象引用,运行时常量池的对象引用、方法区中类静态属性的对象引用。

垃圾回收的算法常见有3种。

  1. 标记-清除算法(Mark-Sweep),容易导致内存碎片。
  2. 复制算法(Copying)
  3. 标记-压缩算法(Mark-Compact)标记-清除算法的改进版,容易导致内存碎片。

    复制算法简介:
    这里写图片描述

    在HotSpot Eden 空间和两位两个Survivor 空间缺省所占比例是8:1.
    当执行异常Minor GC 时, Eden 空间和From中的存活对象会被复制到To 空间。当有两种情况例外 1. 如果存活对象的分代年龄超过选项”-XX: MaxTenuringThreshold” 所指定的阈值 2 当To 空间的容量达到阈值时。 这两种情况下,存活的对象直接晋升到老年代中。
    剩下的对象就是已死亡的对象,所以回收内存空间。
    最后 From 空间和To 空间将交换指针位置。

JVM 自动内存管理的实现

串行回收与并行回收的区别

这里写图片描述

Serial 收集器:串行回收

作用于新生代,采用复制算法,串行回收和“Stop-the-World” 的方式回收内存。
“-XX:UseSerialGC” 手动指定使用Serial 收集器执行内存回收任务。

ParNew 收集器: 并行回收

可以充分利用多CPU、多核等物理资源优势,迅速完成垃圾收集,提升程序的吞吐量。
“-XX:+UseParNewGC” 手动指定使用ParNew 收集器执行内存回收任务。

Parallel 收集器:程序吞吐量优先

和ParNew 收集器不同,Parallel 收集器可以控制程序的吞吐量大小,可以通过选项
“-XX:GCTimeRatio” 设置执行内存回收的时间所占JVM 运行总时间的比例,
在程序吞吐量优先的应用情景中,Parallel 收集器+ Parallel Old 收集器组合执行Server 模式下的内存回收将会是不错的选择。

Concurrent-Mark-Seep 收集器:低延迟

CMS 收集器天生为并发而生,对于需要快速做出响应,不愿有过多延迟的系统尤为适合。
采取并行回收,由于执行”Stop-the-World” 的暂停工作线程的时间并不会很长,因此延迟少。
通过选项:“-XX:CMSInitiatingOCCupancyFraction” 用于设置当老年代中的内存使用率达到多少百分比的时候执行内存回收。(JDK6及其以上默认值是92%)

G1(Garbage-First) 收集器:区域化分代式

G1 收集器的设计初衷就是为了替代CMS收集器而生,它是基于并行和并发、低延迟以及暂停时间更加可控的区域化分代式垃圾收集器。
G1 重塑了整个Java堆区,不再区分新生代和老年代,而是讲Java堆区分成约2048 个大小相同的独立Region 块,每个Region 块可能连续,也可能不连续,大小被控制在1MB~32MB之间,这样做的原因是能更好地提升GC的回收效率和缩短”Stop-the-World” 机制的暂停时间以换取更大程序吞吐量。 这是因为G1 收集器在执行内存回收时,能够优先释放掉整个Java 堆区中一些占用内存较大的Region块,而无需像其他收集器一样直接扫描整个java 堆区。

参考文献

《Java 虚拟机精讲》

猜你喜欢

转载自blog.csdn.net/lilele12211104/article/details/79839669