基于Sun公司的Hotspot虚拟机来学习:
一.jvm内存模型:
从上图可以看出jvm主要分为五大部分:程序计数器,虚拟机栈,本地方法栈,堆,方法区。
1.程序计数器
程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器,为线程私有。
为什么需要程序计数器?
我们知道对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。
注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。
2.虚拟机栈
每个线程都有自己的栈,是线程私有的,生命周期与线程一致,对应着一次次的Java方法调用,栈中的数据都是以栈帧(Stack Frame)的格式存在。在这个线程上正在执行的每个方法都各自对应一个栈颜(Stack Frame)。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。
局部变量表:一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型),returnAddress类型。它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jvm中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。
reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。
对虚拟机栈区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展时(大部分虚拟机支持动态扩展,但是也允许固定长度的虚拟机栈)无法申请到足够的内存,会抛出OutOfMemoryError异常。
栈的大小可以通过 -Xss 参数设置。垃圾回收也不会回收该区域(我正运行方法呢,你把我内存释放了,我运行个屁)
3.本地方法栈
本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。与虚拟机栈一样也会抛出两种异常。(HotSpot虚拟机直接把两块区域合二为一)
4.堆
对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。因此需要重点了解下。
java堆是java虚拟机管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。此外,java堆也是垃圾收集器管理的主要区域,因此也被成为“GC堆”。从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以java堆中还可以细分为:新生代和老年代。如果堆中没有可用内存来完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
5.方法区
方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。
用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。
在hotstop中对方法区的实现为“永久代”,但是在jdk1.8版本已经使用“元空间”替代“永久代”。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中, 而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性: -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
重点:上文说的“永久代”以及“元空间”是Hotstop对方法区的一种实现。方法区是JVM的规范。
在方法区中有个比较特别的存储区域-常量池,虽然在JDK1.8及以后HotStop将永久代改为元空间,但是字符串常量池还是在堆内存中。
Java中的常量池有:class常量池、运行时常量池、String常量池。
在JDK1.6及之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代(位于堆内存中)
在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
在JDK1.8 hotspot移除了永久代用元空间取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(堆外内存)
为什么要使用常量池?
避免频繁地创建和销毁对象而影响系统性能,实现对象的共享(字符串常量池);对于类共用的元数据信息,使用常量池可以共享使用,而不是不同线程、对象都创建一个副本,节省内存开销(class常量池、运行时常量池)。
区域 | 是否会抛出异常 | 是否线程共享 | 参数设置 |
---|---|---|---|
程序计数器 | N | N | |
虚拟机栈 | Y | N | -Xss |
本地方法栈 | Y | N | |
堆 | Y | Y | -Xms设置堆的最小空间 -Xmx设置堆的最大空间大小。 |
方法区 | N/Y(因为字符串常量池还在堆中,如果堆内存不足会抛出OutOfMemoryError) | Y | -XX:MaxMetaspaceSize -XX:MetaspaceSize |
关注公众号发送666领取海量JAVA相关学习资料: