Java虚拟机在执行java程序的时候会把它所管理的内存划分为若干个不同的数据区域。
java虚拟机内存分布图
接下来我们分别来解释各个内存区的作用。
定义:程序计数器是一块较小的内存空间,它的作用可以看做是当前线程执行的字节码的行号指示器,即由他告诉线程下一步该执行哪行的代码。
对于多线程来说,各个线程之间的计数器互不影响,独立存储,我们称这类内存区域为"线程私有"内存。
如果正在执行的是Native方法,这个计数器则为空。网上看了一下:原因是因为此时会再开一个线程去执行native方法,新线程的程序计数器是null,旧线程的程序计数器还是自己原来的那个计数器,且旧线程处于阻塞状态,等新线程执行完毕。
这个内存区是唯一在Java虚拟机规范中没有规定任何OutOfMemoryError(内存溢出)情况的区域。因为它占的空间特别特别小几乎可以忽略不计。
定义:虚拟机栈描述的是Java方法执行的内存模型。
Java虚拟机栈也是线程私有的,生命周期与线程相同。
每个方法被执行的时候会创建一个栈帧,,用于存储局部变量、操作栈、动态链接、方法出口等信息。
每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
虚拟机栈中有一个局部变量表,而我们经常关注的就是局部变量表。
局部变量表存放了编译期可知的各种基本数据类型(Boolean,byte,char,short,int,long,float,double即java八个基本数据类型) 、对象引用和returnAddress类型。
其中64位长度的long和double类型数据会占用2个局部变量空间,其余的数据类型只占用一个。
局部变量表所需的内存空间在编译期间完成分配。在方法运行期间不会修改局部变量表的大小。
定义了两种异常情况
异常1:StackOverflowError:线程请求的栈深度大于虚拟机允许的深度
异常2:OutOfMemoryError:如果扩展时无法申请到足够的内存时会抛出该异常。
发挥的作用和Java虚拟机栈类似。区别在于虚拟机栈为虚拟机执行java方法服务,而本地方法栈是为虚拟机使用到的Native方法服务。也会抛出同样的两个异常:StackOverflowError和OutOfMemoryError。
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中存储的就是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体类型地址。
使用指针:java堆对象中就必须考虑如何放置访问类型数据的相关信息。reference中直接存储的就是对象地址。句柄和指针的优缺点:
句柄的好处:reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收时会经常移动对象,即重排序)时只会改变句柄中的实例数据指针。而reference本身不需要被改变
指针的好处:速度更快,节省了一次指针定位的时间开销。
综上所述,内存分布图如下:
内存泄露的例子:
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
单个线程下,无论是由于栈帧太大,还是虚拟机容量太小,当内存无法分配的时候,虚拟机都抛出StackOverflowError异常。
例子:
@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(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身消耗的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”。
例子:
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线程的映射到操作系统的内核线程上,所以又很大风险会造成死机。所以又兴趣可以试试!
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的应用中,需要特别注意类的回收状况
可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与java对的最大值(-Xmx)一样。