文章目录
- 1、堆(线程共享)
- 2、方法区(线程共享)
- 3、虚拟机栈(线程私有)
- 4、本地方法栈(线程私有)
- 5、程序计数器(线程私有)
- 6、易错点
源自:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) 周志明
1、堆(线程共享)
Java 堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 里“几乎”所有的对象实例都在这里分配内存。
Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩
展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再
扩展时,Java虚拟机将会抛出OutOfMemoryError异常。原因有二:
- JVM堆内存设置不够。可以通过-Xms、-Xmx来调整。
- 代码中创建了大量大对象,并且不能被垃圾收集器收集(存在被引用)
2、方法区(线程共享)
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占有的内存。
当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。
3、虚拟机栈(线程私有)
Java虚拟机栈(Java Virtual Machine Stacks)它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
这里需要关注一下入栈的过程:
如果方法里声明了基本数据类型(byte、short、int、long、double、float、char、boolean)的变量,那么它们都存储在栈中;如果方法中new了新的对象,那么会先去堆中创建该对象,然后栈中存储该对象的引用地址。如果方法中引用了已创建的对象,那么栈中存储该对象的引用地址。
如果某个线程的线程栈的内存被耗尽,没有足够的内存资源去创建栈帧,就会发生内存溢出。
例如如下代码:
public class Test {public static void m2(){m2();}public static void main(String[] args) {m2();}
}
上面这串代码的执行过程是:线程先执行main方法,同时会创建main方法的栈帧插入到该线程的线程栈中,当执行到m2()方法时,创建m2()方法的栈帧插入到该线程的线程栈中,执行到m2()方法里的m2()方法时,创建栈帧,插入到线程栈中,后面进行无脑创建栈帧、入栈。当创建一定数量的栈帧后,剩下的线程资源无法再创建新的栈帧
就会报StackOverflowError异常(堆栈溢出异常)(当前虚拟机栈不可以动态扩展)
异常截图:
如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
4、本地方法栈(线程私有)
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
5、程序计数器(线程私有)
程序计数器(Program Counter Register)也被称为 PC 寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器。程序计数器的作用是记录当前线程下一条要运行的指令,这样保证了线程在切换回来时能回到正确的位置继续开始执行。
6、易错点
- 根据方法区中的类型信息去创建对象时,该类的静态属性不会出现在新创建的对象中,原因是对于类来说,每个静态属性只存在一份,不属于该类的某个对象。所以当你去打印一个新创建的对象时,只会打印出非静态的属性的值
public class UserParam {public static int a=0;private String userName;private String nickName;private UserParam userParam;public int getTest() {return test;}public void setTest(int test) {this.test = test;}public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}public String getNickName() {return nickName;}public void setNickName(String nickName) {this.nickName = nickName;}private int test;public UserParam getUserParam() {return userParam;}public void setUserParam(UserParam userParam) {this.userParam = userParam;}@Overridepublic String toString() {return "UserParam{" +"userName='" + userName + '\'' +", nickName='" + nickName + '\'' +", userParam=" + userParam +", test=" + test +'}';}
}
public static void main(String[] args) {UserParam userParam = new UserParam();UserParam.a=2;UserParam userParam1 = new UserParam();System.out.println(userParam);System.out.println(userParam1);
}
打印结果如下:
UserParam{userName='null', nickName='null', userParam=null, test=0}
UserParam{userName='null', nickName='null', userParam=null, test=0}
- 栈帧中的基本数据类型变量,只要赋值了,除非再次对其进行赋值,否则值不会改变。
public class Test {public static void main(String[] args) {UserParam userParam = new UserParam();UserParam userParam1 = new UserParam();userParam1.setTest(userParam.getTest());System.out.println(userParam1);userParam.setTest(10);System.out.println(userParam1);}}
打印结果:
UserParam{userName='null', nickName='null', userParam=null, test=0}
UserParam{userName='null', nickName='null', userParam=null, test=0}
- 引用类型会根据引用数据的改变而改变。
public class Test {public static void main(String[] args) {UserParam userParam = new UserParam();UserParam userParam1 = new UserParam();userParam1.setUserName("aaaaa");func(userParam,userParam1);System.out.println(userParam);}public static void func(UserParam userParam,UserParam userParam1){userParam.setUserParam(userParam1);userParam1.setUserName("bbbbbbbb");}}
打印结果:
UserParam{userName='null', nickName='null', userParam=UserParam{userName='aaaaa', nickName='null', userParam=null, test=0}, test=0}
UserParam{userName='null', nickName='null', userParam=UserParam{userName='bbbbbbbb', nickName='null', userParam=null, test=0}, test=0}
可以看到,随着userParam1中userName的改变,userParam中的userParam也变了。原因是栈帧中引用类型变量存储的是堆中实例对象的地址,当实例对象改变,也意味着引用类型变量改变。
- 包装类型有拆装箱的过程,取值情况与基本数据类型一样
public class Test {public static void main(String[] args) {UserParam userParam = new UserParam();userParam.setTest(10);UserParam userParam1 = new UserParam();userParam1.setTest(userParam.getTest());userParam.setTest(100);System.out.println(userParam1);}}
打印结果如下:
UserParam{userName='null', nickName='null', userParam=null, test=10}
- jdk1.8中,String存在于堆中的字符串常量中,也是个对象。(堆的唯一目的就是用来存放实例对象)
public class Test {public static void main(String[] args) {UserParam userParam = new UserParam();userParam.setUserName("yhz");UserParam userParam1 = new UserParam();userParam1.setUserName(userParam.getUserName());userParam.setUserName("aaaa");System.out.println(userParam1);}}
打印结果:
UserParam{userName='yhz', nickName='null', userParam=null, test=null}
原因是:
- 创建完userParam对象,在给userParam设置userName为“yhz”时,会先去堆中的字符串常量池中创建“yhz”这个实例,然后将"yhz"实例的地址返回给userParam的userName。
- 创建完userParma1对象,在给userParma1设置userName为userParam.getUserName()时,userParam.getUserName()返回的userParam中userName保存的字符串常量池中"yhz"实例的地址,于是userParam1中userName指向字符串常量池中"yhz"实例(保存了字符串常量池中"yhz"实例的地址)。
- 当给userParam设置userName为"aaaa"时,会先去堆中的字符串常量池中创建“aaaa”这个实例,然后将"aaaa"实例的地址返回给userParam的userName,最后userParam的userName指向了"aaaa"然而userParam的userName还是指向"yhz"。
关于字符串常量池的一些内幕