理解Java虚拟机#3 Java内存分配

Java内存分配

一、运行时数据区域

众所周知,程序的运行要把数据和代码装入到内存中运行,所以明白程序执行过程中内存是如何分配的是很有必要的。
Java虚拟机中,根据不同对象的特点,将内存划分为不同的数据区,如下图:
运行时数据区
其中,方法区和堆区是所有线程共享的区域,随着虚拟机进程的启动而存在。
栈区和PC是线程私有的,随着用户线程的启动和消亡。

1.1 程序计数器(PC)

与OS中PC的作用一样,每个线程私有的PC用来指示下一条要执行的字节码指令。
所以分支、循环等流程的实现都需要依靠PC来实现

1.2 Java虚拟机栈

Java虚拟机栈是线程私有的,生命周期与线程相同。
每个方法在执行的同时创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每个方法被调用时,入栈,方法结束时,退栈。

局部变量表存放着编译期间可以知道的数据类型(char boolean等)
局部变量表所需的内存空间在编译期间完成分配,也就是说当一个方法分配栈帧的时候,大小是提前确定的,方法运行期间不会改变局部变量表的大小。

1.3 本地方法栈

作用于Java虚拟机栈类似
当执行Java方法时,使用的是Java虚拟机栈
本地方法时,使用的是本地方法栈

1.4 堆

Java堆是Java虚拟机所管理的内存最大的一块,所有new出来的对象都在这里分配。
堆是所有线程共享的,所以GC时主要是对堆里面的对象进行回收。

为了GC时方便,将内存的堆分为新生代老年代,新生代的对象生命周期更新频繁,每次都有大量的对象死去,少量的存活,老年代的对象生命周期比较长,每次存活率都比较高。

同时,堆区在物理内存上是不连续的,只是逻辑上连续

1.5 方法区

方法区是所有线程共享的。
用来存储加载的类信息、常量、静态变量、即时编译器后的代码等数据。

1.6 运行时常量池

常量池是方法区的一部分
用来存放编译期间或者运行期间产生的常量。

1.7 直接内存

直接内存不属于虚拟机运行时内存的一部分
而是在本机当中剩余内存分配出来的一块空间,通过一些传递数据的手段,从外内存到Java内存进行传递数据。
其大小受到主机内存大小的限制。

二、Java内存分配

根据以上特点,可以总结如下:

  1. 栈:存放基本数据类型,对象的引用。注意对象的引用是放在栈中,而对象本身是放在堆中(new出来的对象)或者常量池中(字符串常量对象)
  2. 堆:存放所有new出来的对象
  3. 常量池:存放字符串常量和基本数据类型的常量(public static final)
  4. 方法区:放静态常量等(static)

2.1 String常量和引用的分配

如下面代码的内存分配:

    String s1 = "Wang";
    String s2 = "Wang";
    String s3 = "Wang";

    String s4 = new String("Wang");
    String s5 = new String("Wang");
    String s6 = new String("Wang");

在这里插入图片描述
首先字符串常量分配在常量池当中,其引用s1等在栈中
new出来的string对象分配在堆中,其引用s4等在栈中
所以可以看出,s1 s2 s3指向的是同一个字符串常量,而s4 s5 s6则不是

这里有一个细节需要注意,new出来的“Wang”对象,会首先去常量池中查找是否已经有“Wang”对象,如果没有则在常量池创建一个,再复制到堆中。

2.2 基础类型的变量和常量

    int i1 = 9;
    int i2 = 9;
    int i3 = 9;
    
    public static final int INT1 = 9;
    public static final int INT2 = 9;
    public static final int INT3 = 9;

在这里插入图片描述
基础的数据类型直接存放在栈中
stactic final的存放在常量池当中

2.3 成员变量和局部变量

局部变量(包括形式参数)分配在栈中,随着方法的消失而消失
成员变量存储在堆中,有GC负责回收

public class People{

    private int day;

    private int month;

    private int year;

    public People(int d, int m, int y) {
        this.day = d;
        this.month = m;
        this.year = y;
    }
}

public class TestPeople {

    public static void main(String[] args) {

        int date = 9;

        TestPeople testPeople = new TestPeople();
        testPeople .change(date);
        People wang= new People(7, 7,1970);
    }

    public void change(int i){
        i = 1324;
    }
}

从main方法的执行分析:

  1. int date = 9;
    date属于局部变量,此时分配在栈中。
  2. TestPeople testPeople = new ();
    testPeople 属于引用,分配在栈中
    对象new TestPeople()分配在堆中
  3. testPeople .change(date);
    i属于局部变量,存放在栈中,随着change方法的结束而消失,因此,在这里并不会改变传入date的值
  4. People wang= new People(7, 7,1970);
    wang是对象引用,存放在栈中
    对象new People存放在堆中
    传入的7 7 1970 对应的d m y属于局部变量,存放在栈中,当构造方法执行完毕时自动消失
    People对象中的day month year为成员变量,分配在堆中
  5. main方法结束时
    date变量 testPeople wang引用都从栈中消失
    此时new出来的TestPeople People没有引用指向他们,等待被GC

在这里插入图片描述

三、内存溢出

3.1 Java堆溢出

根据堆的特性,Java堆主要存放new出来的对象。
所以只要不断的创建对象,并且保持GC Roots到对象之间有可达路径就可以避免这些对象被GC,超过堆的最大容量之后就会造成内存溢出(OutOfMemoryError)。

import java.util.ArrayList;
import java.util.List;

public class HeapOOM {

    static class OOMObject{}

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();

        while(true){
            list.add(new OOMObject());
        }
    }
}

3.2 栈溢出

我们知道栈分配的是局部变量和每一个方法的栈帧,所以不断地调用方法,可以造成StackOverflowError

public class StackSOF {

    public void stackLeak(){
        stackLeak();
    }

    public static void main(String[] args) { // 无限递归调用自身
        StackSOF stackSOF = new StackSOF();
        stackSOF.stackLeak();
    }
}

3.3 方法区和常量池溢出

常量区存放字符串常量等,所以这我们的思想就是在常量池中不断产生字符串常量。
这里我们借助String.intern()方法:如果字符串常量池中已经包含一个此String对象的字符串,则返回常量池中这个对象;否则,将此String对象添加到常量池中并返回其引用。

import java.util.ArrayList;
import java.util.List;

public class RuntiomeConstantPoolOOM {

    public static void main(String[] args) {
        
        List<String> list = new ArrayList<String>();    // 使用list保持对String的引用,防止GC
        int i = 0;
        
        while(true){
            list.add(String.valueOf(i++).intern());
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值