Java虚拟机在执行java程序的时候会把它所管理的内存划分为若干个不同的数据区域。
java虚拟机内存分布
java虚拟机内存分布图
接下来我们分别来解释各个内存区的作用。
程序计数器
定义:程序计数器是一块较小的内存空间,它的作用可以看做是当前线程执行的字节码的行号指示器,即由他告诉线程下一步该执行哪行的代码。
对于多线程来说,各个线程之间的计数器互不影响,独立存储,我们称这类内存区域为"线程私有"内存。
如果正在执行的是Native方法,这个计数器则为空。网上看了一下:原因是因为此时会再开一个线程去执行native方法,新线程的程序计数器是null,旧线程的程序计数器还是自己原来的那个计数器,且旧线程处于阻塞状态,等新线程执行完毕。
这个内存区是唯一在Java虚拟机规范中没有规定任何OutOfMemoryError(内存溢出)情况的区域。因为它占的空间特别特别小几乎可以忽略不计。
Java虚拟机栈
定义:虚拟机栈描述的是Java方法执行的内存模型。
Java虚拟机栈也是线程私有的,生命周期与线程相同。
每个方法被执行的时候会创建一个栈帧,,用于存储局部变量、操作栈、动态链接、方法出口等信息。
每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
虚拟机栈中有一个局部变量表,而我们经常关注的就是局部变量表。
局部变量表存放了编译期可知的各种基本数据类型(Boolean,byte,char,short,int,long,float,double即java八个基本数据类型) 、对象引用和returnAddress类型。
其中64位长度的long和double类型数据会占用2个局部变量空间,其余的数据类型只占用一个。
局部变量表所需的内存空间在编译期间完成分配。在方法运行期间不会修改局部变量表的大小。
定义了两种异常情况
异常1:*Error:线程请求的栈深度大于虚拟机允许的深度
异常2:OutOfMemoryError:如果扩展时无法申请到足够的内存时会抛出该异常。
本地方法栈
发挥的作用和Java虚拟机栈类似。区别在于虚拟机栈为虚拟机执行java方法服务,而本地方法栈是为虚拟机使用到的Native方法服务。也会抛出同样的两个异常:*Error和OutOfMemoryError。
Java堆
对大多数应用来说。java堆是虚拟机管理内存中最大的一块。java堆是被所有线程共享的一块区域,在虚拟机启动时创建。
这块区域的唯一目的就是存放对象实例。
java堆是垃圾收集器管理的主要区域。
Java堆可以处于物理上不连续,逻辑上连续的内存空间。
方法区
是各个线程的共享区域。它用于存储已经被虚拟机加载的类信息、常量、静态变量。即时编译器编译后的代码等数据。
和java堆一样不需要连续的内存和可以选择固定大小而且可扩展。
可以选择不实现垃圾收集。即垃圾收集行为在这个区域比较少出现。
很多人称方法区为“永久代”,即一创建就不会被垃圾回收,其实不准确。
这个区域的内存回收目标主要是正对常量池的回收和堆类型的卸载。
当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。
运行时常量池
是方法区的一部分。Class文件有常量池信息,用于存放编译期生成的各种字面量和符号引用,这部分内容是在类加载后放到方法区的运行常量池中。
一般来说,处理保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
具备动态性,可以在运行期间将新的常量放入池中。这种特性用的最多的就是String的intern方法。
当常量池无法申请到内存时,抛出OutOfMemoryError异常。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但是被频繁的使用,也会导致OutOfMemoryError异常。
就是通过IO的方式,使用Native函数知己分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。
不占java堆大小,但是肯定占本子内存大小
对象访问
A a= new A();
我们来分析一下上面的代码。
首先在编译的时候,A类型的类信息就已经存在了方法区,声明A对象 “A a”,会反映到虚拟机栈中的本地变量表中,作为衣蛾refenence类型数据出现。“new A()”,实例化对象,语义会反映到java堆内存中,在java堆中必须包含能查到此对象类型数据的地址信息,这些数据类型信息就是在方法区了。
reference类型在java虚拟机规范中只规定了一个指向对象的引用,所以实现方式有两种。
使用句柄:Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体类型地址。
句柄和指针的优缺点:
句柄的好处:reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收时会经常移动对象,即重排序)时只会改变句柄中的实例数据指针。而reference本身不需要被改变
指针的好处:速度更快,节省了一次指针定位的时间开销。
综上所述,内存分布图如下:
内存溢出异常
java堆溢出
出现堆内存溢出,异常堆栈信息:java.lang.OutOfMemoryError:Java heap space...
遇到内存溢出,先要分析是内存泄露还是内存溢出
如果是内存泄露,查看泄露对象到GC Roots的引用链。(内存泄露就是对象已经不用了,可以被回收了,但是没有及时被回收)
内存泄露的例子:
public class OutOfMemoryErrorTest { List resultList = new ArrayList<>(); @Test public void heapException(){ int i = 1; while(true){ byte[] b = new byte[1024*1024*100];//100M一次 resultList.add(b); System.out.println(String.format("打印了%s",i)); i++; } } }
报错:java.lang.OutOfMemoryError: Java heap space
虚拟机栈和本地方法栈溢出
会抛出的两个异常:*Error异常:如果线程请求深度大于虚拟机所允许的最大深度,抛出该异常
异常信息:Exception in thread “main” java.lang.*Error
OutOfMemoryError异常:虚拟机在扩展栈时无法申请足够的内存空间。
异常信息:Exception in thread “main” java.lang.OutOfMemoryError:unablw to create new native thread
-Xoss 参数(设置本地方法栈大小)虽然存在,但实际上是无效的。栈容量只由-Xss 参数设定。
-Xss 参数设置栈内存容量
单个线程下,无论是由于栈帧太大,还是虚拟机容量太小,当内存无法分配的时候,虚拟机都抛出*Error异常。
例子:
@Test public void jVMStackErrorTest() throws InterruptedException { Thread thread = new Thread(new JVMStackError()); thread.start(); thread.join(); } class JVMStackError implements Runnable{ int i = 1; void foreachThread(){ System.out.println(String.format("重复调用%s次此方法",i)); i++; foreachThread(); } @Override public void run() { foreachThread(); } }推导内存:譬如32为Windows限制是2GB,虚拟机提供了参数来控制java对内存和方法区这两部分内存的最大值。剩余的内存2GB,减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身消耗的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”。
如果是建立多线程导致的内存溢出,再不拿减少线程数或更换64位虚拟机的情况下。就只能通过最大堆和减少栈同理来换取更多的线程。
例子:
public void createThreadErrorTest() throws InterruptedException { int i = 1; while(true){ Thread thread = new Thread(new CreateThreadError(i)); thread.start(); i++; } } class CreateThreadError implements Runnable{ int i ; public CreateThreadError(int i) { this.i = i; } @Override public void run() { System.out.println(String.format("第%s个线程",i)); } }
注意:因为java线程的映射到操作系统的内核线程上,所以又很大风险会造成死机。所以又兴趣可以试试!
运行时常量池溢出
String.intern()这个Native方法:如果池中已经包含了一个等于此String对象的字符串,则返回发表池中这个字符串的String对象,否则,将此String对象包含的字符串添加到常量池中,并且返回String对象的引用。-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而限制常量池的容量。因为常量池在方法区中。
异常信息:
Exception in thread “main” java.lang.OutOfMemoryError: PerGen space,这就说明报错是常量池内存溢出。
例子:
@Test public void constantPoolError(){ List resultList = new ArrayList<>(); long l = 1; StringBuilder sbf = new StringBuilder("1"); while(true){ resultList.add(sbf); /*循环一次是10M大小*/ for (int i = 0; i < 1024*1024*100; i++) { sbf.append("1"); } String.valueOf(sbf).intern(); System.out.println(l++ + sbf.toString()); } }
方法区溢出
方法区存放Class的相关信息,如类名,访问修饰符,常量池,字段描述、方法描述等。方法就是通过反射技术持续动态生成class文件,
在经常动态生成大量Class的应用中,需要特别注意类的回收状况
本机直接内存溢出
可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与java对的最大值(-Xmx)一样。