本文由作者通过《深入理解Java虚拟机》总结而来
转载注明出自bestsort.cn,谢谢合作
概览
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途, 以及创建及销毁的时间,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁.在 Java7 中,Java虚拟机所管理的内存包括下图所示的几个运行时数据区域
在 Java8 中,内存结构发生了些许变化。最主要的还是移除了方法区,确定了“元数据区”的概念。如下图
方法区(Method Area)
这里,我们不按照《深入理解JAVA虚拟机》里的顺序来介绍,而是先介绍方法区,并试着分析一下它为什么被抛弃。
概念
方法区(Method Area)是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息:
- 非静态的属性
- 非静态的方法的元数据
- 运行时常量池
- 方法和构造函数编译后的代码
- 类加载初始化或者实例对象初始化用到的特殊方法
虽然 Java虚拟机规范 把方法区描述为堆的逻辑部分,但是它却有一个别名叫做 Non-堆(非堆),目的应该是与 Java堆区分开来
经常会听到有人把方法区叫做永久代(Permanent Generation)。这是因为在 HotSpot(目前使用最广泛的虚拟机) 中选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区(其他虚拟机并没有这样实现,所以也就没有永久代这一说法),实际上两者并不等价。将GC分代扩展至方法区后,HotSpot 的垃圾收集器就可以像管理 Java堆 一样去管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。乍一看,方法区貌似是个好东西。但是需要注意的是,在 Java8 已经取消了方法区这个概念,而是用元数据区来接管方法区的一些工作。
在方法区内,所有线程共享,生命周期与JVM一致,无需地址连续的内存。
上文中说过常量池等一些类信息被存储在方法区内,但是这样有很多坏处: 容易造成内存溢出;也会产生性能问题. 因为每次Full GC 都会连带永久代一起GC。而且类及方法的信息难以确定其大小。如果加载比较多的类很有可能造成内存溢出。而如果为方法区指定较大的内存的话,势必会压缩老年代内存空间,又容易造成老年代的溢出
Java8 改动
在 Java8 中,方法区被移除,类的元数据被放在 native 堆中(点此查看通知邮件)这个空间被叫做元数据区.而字符串常量被移动到了 Java堆 中;
元空间的特点
- 充分利用了 Java 语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
- 每个加载器有专门的存储空间
- 只进行线性分配
- 不会单独回收某个类
- 省掉了 GC 扫描及压缩的时间
- 元空间里的对象的位置是固定的
- 如果 GC 发现某个类加载器不再存活了,会把相关的空间整个回收掉
元空间的内存分配模型
- 绝大多数的类元数据的空间都从本地内存中分配
- 用来描述类元数据的类也被删除了
- 分元数据分配了多个虚拟内存空间
- 给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型;sun / 反射 / 代理对应的类加载器的块会小一些
- 归还内存块,释放内存块列表
- 一旦元空间的数据被清空了,虚拟内存的空间会被回收掉
- 减少碎片的策略
以下部分参考自java-latte.blogspot
我们可以通过下面这张图片看到如何为元数据分配虚拟内存空间,以及如何为每个类加载器加载虚拟存储空间。CL 是 Class Loader 的缩写
你可以看到虚拟内存空间是如何分配的 (vs1,vs2,vs3) ,以及类加载器的内存块是如何分配的。CL 是 Class Loader 的缩写。
理解_mark和_klass指针
要想理解上面这张图,首先得搞清楚这些指针都是什么东西。
JVM 中,每个对象都有一个指向它自身类的指针,不过这个指针只是指向具体的实现类,而不是接口或者抽象类。
- 32 位的 JVM:
- _mark : 4 字节常量
- _klass: 指向类的 4 字节指针 对象的内存布局中的第二个字段 (_klass,在 32 位 JVM 中,相对对象在内存中的位置的偏移量是 4,64 位的是 8) 指向的是内存中对象的类定义。
- 64 位的 JVM:
- _mark : 8 字节常量
- _klass: 指向类的 8 字节的指针
- 开启了指针压缩的 64 位 JVM:
- _mark : 8 字节常量
- _klass: 指向类的 4 字节的指针
只有是 64 位平台上启用了类指针压缩才会存在类指针压缩空间(Compressed Class Pointer Space)。对于 64 位平台,为了压缩 JVM 对象中的_klass 指针的大小,引入了类指针压缩空间(Compressed Class Pointer Space)。
压缩后内存布局如下:
改动后的优势
如果你理解了元空间的概念,很容易发现 GC 的性能得到了提升。
- Full GC 中,元数据指向元数据的那些指针都不用再扫描了。很多复杂的元数据扫描的代码(尤其是 CMS 里面的那些)都删除了。
- 元空间只有少量的指针指向 Java 堆。这包括:类的元数据中指向 java/lang/Class 实例的指针;数组类的元数据中,指向 java/lang/Class 集合的指针。
- 没有元数据压缩的开销
- 减少了根对象的扫描(不再扫描虚拟机里面的已加载类的字典以及其它的内部哈希表)
- 减少了 Full GC 的时间
- G1 回收器中,并发标记阶段完成后可以进行类的卸载
- Hotspot 中的元数据现在存储到了元空间里。mmap 中的内存块的生命周期与类加载器的一致。
- 类指针压缩空间(Compressed class pointer space)目前仍然是固定大小的,但它的空间较大
- 可以进行参数的调优,不过这不是必需的。
- 未来可能会增加其它的优化及新特性。比如, 应用程序类数据共享;新生代 GC 优化,G1 回收器进行类的回收;减少元数据的大小,以及 JVM 内部对象的内存占用量。
因为上面的种种理由,最终在Java8中,元数据区替换掉了方法区
堆(Java Heap)
说完了非堆(前面有提到过方法区又名为Non-Heap(非堆)),接下来就说一下堆吧.
对于大多数应用来说,Java 堆是 Java 虚拟机所管理的内存中最大的一块.对于大多数刚刚创建的Java应用来说,如果觉得运行卡顿,第一想到的可能是提高-Xmx
和-Xms
的数值,这个在网上一些关于IDEA卡顿的文章里也有这个建议(IDEA本身就是使用Java构建的)
Java 堆在虚拟机启动时创建,这块区域唯一的目的就是存放对象实例(包括字符串常量也在这里面). 几乎所有的对象实例都要在这里分配内存。在Java虚拟机规范中的描述是: 所有的对象实例以及数组都要在堆上分配。当然有时候为了调优和减少GC次数,程序员会选择在栈上分配对象实例,或者是进行标量替换。当然这里并不会详细分析这些策略,今后可能会试着分析一下这些方法。
在Java 堆中,可大致分为新生代和老年代。其中又可细分为Eden,From Survivor,To Survivor等. 其具体的从属和大小关系如下图所示
其中Form和To之间是动态切换的,以后我会单独写一下堆内存的分配、回收策略
程序计数器(Program Counter Register)
程序计数器是当前线程正在执行的字节码的地址。程序计数器是线程隔离的,每一个线程在工作的时候都有一个独立的计数器。
在Java虚拟机中,多线程是靠CPU时间片轮转来调度的,也就是说一个线程可能还没有结束就被挂起。而当它再次获取时间片时,就需要从挂起的地方继续执行。在Java虚拟机中,通过程序计数器来记录程序的字节码执行位置。因为记录的信息很少,所以程序计数器占用的内存空间几乎可以忽略不计。而且程序计数器是java虚拟机规范中唯一一个没有规定任何OutofMemeryError的区域
Java虚拟机栈(Java stack)
如果把方法类比为C++中的函数的话,就会发现栈的作用几乎一样。在Java中,每个方法在运行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。其中,局部变量表存放了基本数据类型,对象引用类型(只是引用,不是对象本身。可类比C++中的指针)
栈和程序计数器一样,也是线程隔离的。
本地方法栈(Native Method Stack)
本地方法栈和虚拟机栈的区别在与虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。在虚拟机规范中堆本地方法栈
Native方法
要了解Native方法,我们首先需要知道JNI这个概念
JNI允许程序员用其他编程语言来解决用纯粹的Java代码不好处理的情况,例如,Java标准库不支持的平台相关功能或者程序库。也用于改造已存在的用其它语言写的程序,供Java程序调用。许多基于JNI的标准库提供了很多功能给程序员使用,例如文件I/O、音频相关的功能。当然,也有各种高性能的程序,以及平台相关的API实现,允许所有Java应用程序安全并且平台独立地使用这些功能。
native关键字
被native关键字声明的方法说明该方法不是以Java语言实现的,而是以本地语言实现的。这也就是我们上面所说的Native方法本地方法和其它方法不一样,本地方法意味着和平台有关,因此使用了native的程序可移植性都不太高。另外native方法在JVM中运行时数据区也和其它方法不一样,它有专门的本地方法栈。native方法主要用于加载文件和动态链接库,由于Java语言无法访问操作系统底层信息(比如:底层硬件设备等),这时候就需要借助C语言来完成了。被native修饰的方法可以被C语言重写。
堆外内存也是通过Native函数库分配的