深入理解Java虚拟机:JVM内存管理与垃圾收集理论

  1. HotSpot虚拟机中含有两个即时编译器,分别是编译耗时短但输出代码优化程度较低的客户端编译 器(简称为C1)以及编译耗时长但输出代码优化质量也更高的服务端编译器(简称为C2)
    - 能提前编译的提前编译,不能提前编译的交给 JVM 去解耦!

阅读的疑问???

  1. import 导入的背后原理是?
  2. C++ 线程切换时的上下文保存在哪里?java 就是保存在
  3. 本地(Native) 方法服务是什么东西?
  4. 即时编译技术,逃逸分析技术,栈上分配、标量替换优化,轻量级锁、重量级锁,元空间
  5. 对类型的卸载是什么意思?
  6. String 类的 intern() 方法怎么玩?
  7. 对象的内存布局中对齐填充(Padding)与 OS 中内存对齐的关系?
  8. 为什么对象头需要存储这么多的东西呐?
  9. 基本类型与包装的基本类型有什么区别?为什么说基本类型快?
  10. 为什么对于不同版本的Java虚拟机和不同的操作系统,栈容量最小值可能会有所限制,这主要取决于操作系统内存分页大小。
  11. 如何自定义类加载器?比如:大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类框架

第二部分 自动内存管理

第2章 Java内存区域与内存溢出异常

在这里插入图片描述

1.程序计数器
  1. 当前线程所执行的字节码的行号指示器。协程应该就是通过控制它来实现的。
  2. 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地 址;如果正在执行的是本地(N at ive)方法,这个计数器值则应为空(U ndefined)。此内存区域是唯 一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
2.Java虚拟机栈

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
14. 局部变量表(存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始 地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址))所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定 的,在方法运行期间不会改变局部变量表的大小。
15. 在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError异常。
16. 只要是申请到内存就只会发生 StackOverflowError异常,如果无法申请到才会发生 OutOfMemoryError 异常。

3.本地方法栈
  1. 作用同Java虚拟机栈,区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
4.Java堆
  1. 根据《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间中
  2. 在Java堆中没有内存完成实例分配,并且堆也无法再 扩展时,Java虚拟机将会抛出 OutOfMemoryError 异常。
5.方法区(也即:永久代(PermGen))
  1. 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
  2. 这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤 其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列 表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存 泄漏。
  3. 6.运行时常量池(属于方法区的一部分):Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
  4. Java语言并不要求常量 一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常 量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的
    intern()方法。

因此在 Hotspot 虚拟机中,整个堆和方法区被分成如下的几块。其中 Eden 和Survivor见:(https://blog.csdn.net/liushengxi_root/article/details/122858901)
在这里插入图片描述
在 Java7 之前Hotspot中方法区位于永久代中。同时,永久代和堆是相互隔离的,但它们使用的物理内存是连续的。

永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。

在 Java8 及之后,HotSpots 取消了永久代,那么是不是就没有方法区了呢?当然不是,方法区只是一个规范,只不过它的实现变了。

在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)

针对Java8的调整,我们再次对内存结构图进行调整。
在这里插入图片描述
默认情况下元空间是可以无限使用本地内存的,但为了不让它如此膨胀,JVM同样提供了参数来限制它的使用。

  • XX:MetaspaceSize,class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时,会主动调整。如果释放的多了,那就适当调低,释放的少了,那就适当调高。
  • XX:MaxMetaspaceSize,最大空间。默认是没有限制的。
  • XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集。
  • XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集。
6.直接内存(我理解就是堆外内存吧)

HotSpot虚拟机对象探秘

1.对象的创建

除了最为常见的new语句之外,我们还可以通过反射机制、Object.clone方法、反序列化以及Unsafe.allocateInstance方法来新建对象

其中,Object.clone方法和反序列化通过直接复制已有的数据,来初始化新建对象的实例字段。Unsafe.allocateInstance方法则没有初始化实例字段,而new语句和反射机制,则是通过调用构造器来初始化实例字段。

当我们调用一个构造器时,它将优先调用父类的构造器,直至Object类。这些构造器的调用者皆为同一对象,也就是通过new指令新建而来的对象。也就是说:通过new指令新建出来的对象,它的内存其实涵盖了所有父类中的实例字段。也就是说,虽然子类无法访问父类的私有实例字段,或者子类的实例字段隐藏了父类的同名实例字段,但是子类的实例还是会为这些父类实例字段分配内存的。

  1. 检查这个指令的参数是否能在常量池中定位到 一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程
  2. Java编译器会在遇到new关键字的地方同时生成new指令和 invokespecial指令,这两条字节码指令来创建对象。
2.对象的内存布局

在这里插入图片描述

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头
  1. HotSpot虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部 分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它 为“Mark Word”。
    在这里插入图片描述
  2. 对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针 来确定该对象是哪个类的实例
  3. 如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小(我理解应该也是为了能够快速获取长度size等值)。

压缩指针

在64位的Java虚拟机中,对象头的标记字段占64位,而类型指针又占64位。因此为了尽量较少对象的内存使用量,64位Java虚拟机引入了压缩指针的概念(对应虚拟机选项-XX:+UseCompressedOops,默认开启),将堆中原本64位的Java对象指针压缩成32位的,使得对象头的大小从16字节降至12字节。

打个比方,路上停着的全是房车,而且每辆房车恰好占据两个停车位。现在,我们按照顺序给它们编号。也就是说,停在0号和1号停车位上的叫0号车,停在2号和3号停车位上的叫1号车,依次类推。

原本的内存寻址用的是车位号。比如说我有一个值为6的指针,代表第6个车位,那么沿着这个指针可以找到3号车。现在我们规定指针里存的值是车号,比如3指代3号车。当需要查找3号车时,我便可以将该指针的值乘以2,再沿着6号车位找到3号车。

这样一来,32位压缩指针最多可以标记2的32次方辆车,对应着2的33次方个车位。当然,房车也有大小之分。大房车占据的车位可能是三个甚至是更多。不过这并不会影响我们的寻址算法:我们只需跳过部分车号,便可以保持原本车号*2的寻址系统。

上述模型有一个前提,你应该已经想到了,就是每辆车都从偶数号车位停起。这个概念我们称之为内存对齐(对应虚拟机选项-XX:ObjectAlignmentInBytes,默认值为8)。

当然,就算是关闭了压缩指针,Java虚拟机还是会进行内存对齐。此外,内存对齐不仅存在于对象与对象之间,也存在于对象中的字段之间。比如说,Java虚拟机要求long字段、double字段,以及非压缩指针状态下的引用字段地址为8的倍数。

字段内存对齐的其中一个原因,是让字段只出现在同一CPU的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的
在这里插入图片描述

实例数据

实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会 受到虚拟机分配策略参数(-XX:FieldsAllocationSty le参数)和字段在Java源码中定义顺序的影响。 HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放(其实可以看出真的是为了内存对齐),在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果HotSpot虚拟机的 +XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空 隙之中,以节省出一点点空间。

对齐填充

占位的作用!

3.对象的访问定位

对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:

两者的图示如下:
在这里插入图片描述
在这里插入图片描述

使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference本身不需要被修改。(解耦啦)

针对虚拟机 HotSpot 而言,它主要使用第二种方式进行对象访问,即直接指针方式!!!!

实战:OutOfMemoryError异常

1.Java堆溢出(最常见)
  1. 第一步首先应确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
  2. 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息 以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
2.虚拟机栈和本地方法栈溢出

由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置 本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设定。一共存在两种异常:
32. 如 果 线 程 请 求 的 栈 深 度 大 于 虚 拟 机 所 允 许 的 最 大 深 度 , 将 抛 出 St a c k O v e r f l o w E r r o r 异 常 。
33. 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError 异常。

3.方法区和运行时常量池溢出(常见)
  1. 使用**-Xmx参数限制最大堆**到6M B
  2. Caused by: java.lang.OutOfMemoryError: PermGen space
4.本机直接内存溢出
  1. 直接内存(Direct Memory)的容量大小可通过**-XX:MaxDirectMemorySize参数来指定**,如果不 去指定,则默认与Java堆最大值(由-Xmx指定)一致
  /**
     * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * @author zzm
     */
    public class DirectMemoryOOM {
        private static final int _1MB = 1024 * 1024;
        
        public static void main(String[] args) throws Exception {
            Field unsafeField = Unsafe.class.getDeclaredFields()[0];
            unsafeField.setAccessible(true);
            Unsafe unsafe = (Unsafe) unsafeField.get(null);
            while (true) {
                unsafe.allocateMemory(_1MB);
            }
        }
    }

由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常 情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectM emory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。

垃圾收集理论

JVM之垃圾收集

JVM 为什么使用元空间替换了永久代?

https://zhuanlan.zhihu.com/p/111809384

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值