对于Java程序员来说,由于JVM有自动内存管理机制(内存分配、内存回收),所以我们写代码时不需要考虑内存的使用问题。看起来当程序员把内存控制的权限交给JVM后一切都变得简单起来了,但是一旦遇到内存泄漏或者内存溢出时,程序员就无所适从了。所以了解Java程序的内存和JVM是如何管理内存就很重要,这是Java开发进阶的必由之路。
在整个的学习过程中,会有很多概念和问题。在试图调试JVM暴露给我们的参数控制也有很多概念和问题。如果不理解这些,整个学习就谈不上自洽。我在这个过程中遇到以下问题:
1、内存划分定义不清晰的区域:Native-Memory(本地内存)、Non-Heap(非堆)、Off-Heap(堆外)、Direct-Memory(直接内存)、JVM自身使用的内存、Virtual-Memory(虚拟内存)、Resident-Memory(常驻内存)、Reserved-Memory(保留内存)、Committed-Memory(提交内存)
2、运行时数据区 vs JVM管理的内存 vs JVM自身使用的内存
3、为什么要有Metaspace(元空间)?为什么要移除永久代?为什么元空间的出现解决了永久代OOM问题?
4、参数的默认值以及最佳设置实践
怎样的学习方式才算好的完整的?通过过程了解JVM内部
1、虚拟机启动过程发生了什么?
2、程序执行过程发生了什么?
什么是 Native-Memory
Oracle的官方文档里的Native Memory Tracking似乎暗示了本地内存的定义,此外官方NMT对该工具跟踪的内存有严格的定义:
Since NMT doesn't track memory allocations by non-JVM code, you may have to use tools supported by the operating system to detect memory leaks in native code.
由于 NMT 不跟踪非 JVM 代码的内存分配,您可能必须使用操作系统支持的工具来检测本机代码中的内存泄漏。
NMT工具将本地内存(Java8)分为以下几类:
- Java Heap(The heap where your objects live):mmap
- Class(Class meta data):malloc、mmap
- Thread(Memory used by threads, including thread data structure, resource area and handle area and so on):stack、malloc、arena
- Thread stack(Thread stack. It is marked as committed memory, but it might not be completely committed by the OS)
- Code(Generated code):malloc、arena
- GC(data use by the GC, such as card table):malloc、mmap
- Compiler(Memory used by the compiler when generating code):malloc、arena
- Internal(Memory that does not fit the previous categories, such as the memory used by the command line parser, JVMTI, properties and so on):malloc、mmap
- Symbol:malloc、arena
- Memory Tracking(Memory used by NMT itself):malloc
- Pooled Free Chunks(Memory used by chunks in the arena chunk pool):malloc
- Shared space for classes(Memory mapped to class data sharing archive)
- Unknown(When memory category can not be determined.
Arena: When arena is used as a stack or value object
Virtual Memory: When type information has not yet arrived):arena
如果要看懂它,我们还需要两个方面的概念:1. malloc、mmap、stack、arena是什么意思?2. 这里的分类和我们学习JVM运行时数据区什么关系?
malloc、mmap、stack、arena是什么意思?
- malloc
- 在我目前的学习过程中,只知道是一个分配内存的动作,而且似乎与直接内存有关,因为它没有区分reserved和committed
- todo
- mmap
- 与NIO有关,似乎是单独开辟的一块内存,它减少了文件IO的步骤,使得IO效率得到提升
- mmap会区分reserved和committed,说明它应该是有参数控制的
- todo
- stack
- 线程用到的栈,应该对应到Java虚拟机栈以及本地方法栈
- arena
引用NMT中的一段解释:
Arena is a chunk of memory allocated using malloc. Memory is freed from these chunks in bulk, when exiting a scope or leaving an area of code. These chunks may be reused in other subsystems to hold temporary memory, for example, pre-thread allocations. Arena malloc policy ensures no memory leakage. So Arena is tracked as a whole and not individual objects. Some amount of initial memory can not by tracked.
Arena 是使用 malloc 分配的一块内存。当退出作用域或离开代码区域时,内存会从这些块中批量释放。这些块可以在其他子系统中重用以保存临时内存,例如线程前分配。Arena malloc 策略确保没有内存泄漏。所以 Arena 是作为一个整体而不是单个对象进行跟踪的。无法跟踪一些初始内存量。
arena与malloc似乎很相似,但NMT为何要区分它们,暂不知道
这里的分类和我们学习JVM运行时数据区什么关系?
- PC程序计数器,线程私有的,属于Thread
- Java虚拟机栈,线程私有的,属于Thread
- 本地方法栈,线程私有的,属于Thread
- 堆,线程共享,属于Java Heap,目前包含:对象实例(包括java.lang.Class 的实例)、数组、字符串常量池、类的静态变量[在7中从instanceKlass末尾(位于PermGen内)移动到了java.lang.Class对象的末尾(位于普通Java heap内)]、TLAB(线程分配缓冲区)等
- 元空间,线程共享,由于它包含类加载器?、类信息、运行时常量池(符号引用Symbols和字面量?等),属于Class
- Code Cache,存放 JIT 编译器编译后的本地机器代码,属于Code
- GC部分表示目前已经占用了15MB的内存空间用于帮助GC
- Symbol部分表示诸如string table及constant pool等symbol占用
- Internal部分表示命令行解析、JVMTI等占用
- Native Memory Tracking部分表示该功能自身占用
元空间
- Java虚拟机规范只有方法区(Method Area),是各个线程共享的内存区域,包含:被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。Java虚拟机把方法区描述为堆的一个逻辑部分,它有个别名叫Non-Heap(非堆)。
-
在JDK1.8之前实现上,HotSpot虚拟机使用永久代(Permanent Generation)来实现方法区,它的设计团队选择把GC分代收集扩展至方法区。方法区与堆连续,那么在逻辑上可以认为当虚拟机判断空间分配不足时,会同时触发老年代和永久代(方法区)的GC时,在GC的过程也会同时扫描老年代和永久代(方法区)。此外虚拟机必须要设置PermSize(属于堆的一部分,所以也受限于-Xmx),PermGen很难调整,PermGen中类的元数据信息在每次FullGC的时候可能被收集,但成绩很难令人满意。而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class总数,常量池的大小,方法的大小等
- 在JDK1.8,Hotspot取消了永久代。永久代真的成了永久的记忆。永久代的参数-XX:PermSize和-XX:MaxPermSize也随之失效。元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)
- 默认情况下元空间是可以无限使用本地内存的,但为了不让它如此膨胀,JVM同样提供了参数来限制它使用的使用
- -XX:MetaspaceSize,class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。
- -XX:MaxMetaspaceSize,可以为class metadata分配的最大空间。默认是没有限制的。
- -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集。
- -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集。
Java 堆
- 对象实例
- 类初始化生成的对象
- 基本数据类型的数组也是对象实例
- Class类对象
- 字符串常量池
- 字符串常量池原本存放于方法区,jdk7开始放置于堆中。
- 字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table
- 静态变量
- 静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中
- 线程分配缓冲区(Thread Local Allocation Buffer)
- 线程私有,但是不影响java堆的共性
- 增加线程分配缓冲区是为了提升对象分配时的效率
直接内存
Reserved-Memory(保留内存)与Committed-Memory(提交内存)
在NMT介绍中,有这么一段描述。从堆的配置来看,最少设置会先commit,所以为了加快后续内存增加使用的速度,把Xms和Xmx设置成一样,在启动阶段就commit,省去了后续commit的过程和时间
you will see reserved and committed memory. Note that only committed memory is actually used. For example, if you run with -Xms100m -Xmx1000m, the JVM will reserve 1000 MB for the Java Heap. Since the initial heap size is only 100 MB, only 100MB will be committed to begin with. For a 64-bit machine where address space is almost unlimited, there is no problem if a JVM reserves a lot of memory. The problem arises if more and more memory gets committed, which may lead to swapping or native OOM situations
您将看到保留和提交的内存。请注意,实际上只有提交的内存才是真的被使用(操作系统)。例如,如果您使用 运行-Xms100m -Xmx1000m,JVM 将为 Java 堆保留 1000 MB。由于初始堆大小只有 100 MB,因此开始时只会提交 100 MB。对于地址空间几乎不受限制的64位机器,如果JVM预留了大量内存也没有问题。如果提交的内存越来越多,就会出现问题,这可能会导致交换或本机 OOM 情况
used、free、waste
- 我目前认为内存使用大概是以下流程:reserve ---> commit ---> use ---> free。先向操作系统申请,然后提交到操作系统,此时内存都是free,然后JVM使用内存(比如new、load)之后,内存就变为used,此时committed的内存就包含free和used,之后used的内存也可以free(GC垃圾回收)
- 我们可以做个简单的测试
Chunks(待补充)
常见问题
什么是Native方法?
由于java是一门高级语言,离硬件底层比较远,有时候无法操作底层的资源,于是,java添加了native关键字,被native关键字修饰的方法可以用其他语言重写,这样,我们就可以写一个本地方法,然后用C语言重写,这样来操作底层资源。当然,使用了native方法会导致系统的可移植性不高,这是需要注意的。
成员变量、局部变量、类变量分别存储在内存的什么地方?
类变量
- 类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁
- 在java8之前把静态变量存放于方法区,在java8时存放在堆中
成员变量
- 成员变量是定义在类中,但是没有static修饰符修饰的变量,随着类的实例产生和销毁,是类实例的一部分
- 由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中
局部变量
- 局部变量是定义在类的方法中的变量
- 在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中
由final修饰的常量存放在哪里?
- final关键字并不影响在内存中的位置,具体位置请参考上一问题。
类常量池、运行时常量池、字符串常量池有什么关系?有什么区别?
- 类常量池与运行时常量池都存储在方法区,而字符串常量池在jdk7时就已经从方法区迁移到了java堆中。
- 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符,在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池;
- 对于文本字符来说,它们会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池;字符串常量池存储的是字符串对象的引用,而不是字符串本身。
什么是字面量?什么是符号引用?
字面量
java代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示:
int a=1;//这个1便是字面量
String b="iloveu";//iloveu便是字面量
符号引用
由于在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,所以如果你在一个类中引用了另一个类,那么你完全无法知道他的内存地址,那怎么办,我们只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取他的内存地址。
例子:我在com.demo.Solution类中引用了com.test.Quest,那么我会把com.test.Quest作为符号引用存到类常量池,等类加载完后,拿着这个引用去方法区找这个类的内存地址。