目录
[1. C/C++程序员](#1. C/C++程序员)
[2. Java程序员](#2. Java程序员)
[1. 程序计数器](#1. 程序计数器)
[2. Java虚拟机栈](#2. Java虚拟机栈)
[3. 本地方法栈](#3. 本地方法栈)
[4. Java堆](#4. Java堆)
[5. 方法区](#5. 方法区)
一、内存管理
1. C/C++程序员
每一个对象生命从开始到终结的维护责任由C/C++程序员自己完成,容易出现内存泄漏和内存溢出
问题。
2. Java程序员
在虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,正
是因为Java程序员把控制内存的权力交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问题,
如果不了解虚拟机是怎样使用内存的,那排查错误、修正问题将会成为一项异常艰难的工作。
二、运行时数据区
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区
域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域
则是依赖用户线程的启动和结束而建立和销毁。
Java虚拟机所管理的内存将会包括以下几个运行时数据区域
Java虚拟机运行时数据区
画图说明一下它们的使用顺序。
jdk1.7之前,HotSpot虚拟机对于方法区的实现称之为"永久代", Permanent Generation。
jdk1.8之后,HotSpot虚拟机对于方法区的实现称之为"元空间", Meta Space。
方法区是Java虚拟机规范中的定义,是一种规范,而永久代和元空间是 HotSpot 虚拟机不同版本
的 两种实现。
1. 程序计数器
程序计数器(Program Counter Register)就是一块较小的内存空间,它可以看作是当前线程所执
行的字节码的行号指示器。
在Java虚拟机的概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要
执行的字节码指令它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能
都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个
确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。
因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器各条线
程之间计数器互不影响,独立存储,我们称这类内存区域为"线程私有"的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。
此内存区域没有规定任何OutOfMemoryError情况的区域。
2. Java虚拟机栈
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周
期与线程相同。
虚拟机栈描述的是Java方法执行的线程内存模型
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量
表、操作数栈、动态连接、方法出口等信息。
每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
这块内存区域规定了两类异常状况
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
- 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryErro异
常。
3. 本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机
栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地
(Native)方法服务。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和
OutOfMemoryError异常。
4. Java堆
对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,Java世界里"几乎"所有的对象实例都在这里分配内存。
从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经
常会出现"新生代""老年代""永久代""Eden空间""From Survivor空间""To Survivor空间"等名
词,说明的是这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个
Java虚拟机具体实现的固有内存布局,更不是《Java虚拟机规范》里对Java堆的进一步细致划
分。
不少资料上经常写着类似于"Java虚拟机的堆内存分为新生代、老年代、永久代、Eden、
Survivor......"这样的内容。
在十年之前(以G1收集器的出现为分界),作为业界绝对主流的HotSpot虚拟机,它内部的垃圾收
集器全部都基于"经典分代"来设计,需要新生代、老年代收集器搭配才能工作,在这种背景下,上
述说法还算是不会产生太大歧义。
但是到了今天,垃圾收集器技术与十年前已不可同日而语,HotSpot里面也出现了不采用分代设计
的新垃圾收集器,再按照上面的提法就有很多需要商榷的地方了。
从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread
Local Allocation Buffer,TLAB),以提升对象分配时的效率。
不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存
储的都只能是对象的实例,将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存。
根据《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应
该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。
但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会
要求连续的内存空间。
Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可
扩展来实现的(通过参数-Xmx和-Xms设定)。
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出
OutOfMemoryError异常。
5. 方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载
的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
在JDK 8以前,把方法区称呼为"永久代"(PermanentGeneration)永久代有-XX:MaxPermSize
的上限, 即使不设置也有默认大小,而到了JDK 8,完全废弃了永久代的概念,改用与JRockit、
J9一样在本地内存中实现的元空间Metaspace)来代替,
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。
三、Hotspot运行时数据区
jdk1.6
jdk1.7
jdk1.8+
四、分配JVM内存空间
分配堆的大小
--Xms(堆的初始容量)
-Xmx(堆的最大容量)
分配方法区的大小
-XX:PermSize
永久代的初始容量
-XX:MaxPermSize
永久代的最大容量
-XX:MetaspaceSize
元空间的初始大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释
放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize
最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio
在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio
在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
分配线程空间的大小
-Xss:
为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M