字符串池、运行时常量池、Class常量池
首先了解一下java内存模型:
Java虚拟机内存区域划分图:
区域
是否线程共享
是否会内存溢出
程序计数器
否
不会
java虚拟机栈
否
会
本地方法栈
否
会
堆
是
会
方法区
是
会
1. 程序计数器(Program Counter Register)
程序计数器就是记录当前线程执行程序的位置,改变计数器来确定下一条执行的指令,例如:循环、分支、方法跳转、异常处理、线程恢复都是依赖程序计数器来完成。
java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,为了线程切换能够恢复到正确的位置上,每个线程都需要一个独立的程序计数器,因此它是线程隔离的。
如果线程在执行一个Java方法,则该线程的程序计数器记录的是正在执行的虚拟机字节码的指令地址,如果正在执行的是Native方法,则该程序计数器的值为空(undefined),此内存区域是Java虚拟机规范唯一没有规范任何OutOfMemeryError
2. Java虚拟机栈(vm stack)
java虚拟机栈是线程私有的,生命周期与线程相同。虚拟机在执行Java程序的时候,每个方法都会创建一个栈帧,栈帧存在在Java虚拟机栈中,通过压栈出栈的方式进行方法调用。
栈帧又分为:局部变量表、操作数栈、动态链接、方法出口,例如:局部变量存放在Java虚拟机栈中的局部变量表中。局部变量表中存放8中基本数据类型,若为引用类型则存引用地址。
*注意:
当用户请求web服务器时,每个请求开启一个线程负责用户的响应计算(每个线程分配一个虚拟机栈),如果并发量大,则可能出现内存溢出(OutOfMemeryError),解决方案:可以适当将每个虚拟机栈的大小调小,减少内存的使用量来提高并发量。
但是当栈内存调小之后,又会引发方法调用深度的问题。每一个方法都会产生一个栈帧,若方法调用深度很深,则意味着栈中存在大量的栈帧,可能会导致栈内存溢出(StackOverFlowError)
3. 堆(heap)
堆是被所有线程共享的区域,是在虚拟机启动时创建的。堆中存放的都是对象的实例。垃圾回收主要回收的就是堆区。为了提升垃圾回收的性能,又将堆区划分为新生代(young)和老年代(old),更细分的话,新生代又可以划分为Eden区和两个Survivor区 (from survivor和to survivor)。
Eden :新创建的对象放在Eden区。
From Survivor和To Survivor:保存新生代gc后还存活的对象,Hotspot虚拟机Eden:Survivor=4:1,即Eden:From Survivor:To Survivor=8:1:1。
老年代对象存活时间较长,经过多次新生代垃圾回收(默认15次)后对象进入老年代。
当堆中分配内存过多,且大部分对象正在使用,则会报内存溢出(OOM)。
4. 方法区
方法区是所有线程共享的区域,用于存放已被虚拟机加载的类信息、常量、静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分,也被称为永久代(permanment generation)。
永久代也会进行垃圾回收,主要针对常量池回收,类型卸载(例如反射生成的大量零时使用的Class等信息)。
常量池主要存放编译器生成的各种字节码和符号引用,常量池也具有一定的动态性,里面可以存放编译期生成的常量,运行期间的常量也可以加到常量池中,比如String的intern()方法。当方法区满时,无法再分配空间就会报内存溢出异常(OOM)。
Java8中已经没有方法区了,取而代之的是元空间。
5. 本地方法栈(Native Method Stack)
本地方法栈为虚拟机提供使用到的本地方法服务(Native)。本地方法栈是线程私有的,功能和虚拟机栈非常类似,线程在调用本地方法时存储本地方法的局部变量表,操作数栈等信息。
本地方法栈是调用非java实现的方法,例如Java调用C语言老操作某些硬件信息。
6. 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机内存规范中中定义的内存区域,但是这部分内存也被频繁被使用,也会导致OutOfMemaryError出现。
jdk1.4中加的NIO中,ByteBuffer提供的一个方法是allocateDirect(int capacity),这是一种基于通道(channel)和缓冲区(buffer)的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer对象作为这块堆内存的引用进行操作,这样由于避免了在Java堆和Native堆中来回复制数据,从而在某些场景中性能提升显著。
其次了解Java对象在JVM中内存分配
以Java8为例:
public class Student {
private String name;
private static Birthday birthday = new Birthday();
public Student(String name) {
this.name = name;
}
public static void main(String[] args) {
Student s = new Student("zhangsan");
int age = 10;
System.out.println(age);
}
}
class Birthday {
private int year = 2010;
private int month = 10;
private int day = 1;
}
以Student类执行到main方法的最后一行时来分析java实例对象在内存中的分配情况。
如下图:
从图中可以看出,普通Java示例对象的内存分配主要在三个区域:虚拟机栈、堆、方法区。
言归正传
1. 字符串池
在Java中有两种创建字符串对象的方式:
采用字面量的方式赋值,例如String name = "chenliang";
采用new关键字的方式新建一个字符串对象,例如String name = new String("chenliang");
这两种方式在性能和内存占用方面存在着差异。
方式一:
public static void main(String[] args){
String name1 = "chenliang";
String name2 = "chenliang";
System.out.println(name1 == name2);
}
采用字面量的方式创建一个字符串时,JVM首先回去字符串常量池中去查找是否存在"chenliang"这个对象,如果不存在,则在字符串常量池中创建"chenliang"对象,然后将该对象的引用地址返回给字符串常量name1,则此时name1会指向字符串常量池中"chenliang"这个对象,若存在,则不创建任何对象,直接将字符串常量池中"chenliang"对象的引用地址返回,赋值给字符串常量。
本例的结果为: true
方式二:
public static void main(String[] args){
String name3 = new String("chenliang");
String name4 = new String("chenliang");
System.out.print(nam3 == name4);
}
采用new关键字去新建一个字符串对象时,JVM首先在字符串池中查找是否存在"chenliang"这个字符串对象,若有则不在字符串池中再去创建"chenliang"对象,而是直接在堆中创建"chenliang"字符串对象,然后将堆中该对象的引用地址返回给name3,若字符串池中没有该对象,则会在字符串池中创建一个该对象,再去堆中创建一个"chenliang"对象,并将堆中该对象的引用地址返回给name3。
本例的结果为: false
这是由于通过new关键字创建对象时每次new出来的都是一个新的对象,所以说引用name3和name4是指向两个不同的对象,所以使用 == 比较的结果为false。
###### intern()方法的使用:
一个初始为空的字符串池,它是由String类独立维护。当调用intern()方法时,若字符串池中已经包含一个等于此String对象的串(equals(object)方式确定),则返回池中的字符串。否则,将此String串加入字符串池中,并返回该String对象的引用。对于任意两个字符串s和t,当且仅当s.equals(t)为true时,s.intren() == t.intern()才为true。所有的字面量字符串和字符串赋值常量表达式都是使用intern()方法进行操作的。
每一个字符串常量都指向字符串池中或者堆内存中的一个字符串实例。
字符串对象的值是固定的,一旦创建则不能被修改。
字符串常量或常量表达式中的字符串都被使用String.intern()方法在字符串常量池中保留了唯一实例。
注意:字符串池中维护了共享的字符串对象,这些字符串不会被垃圾回收器回收。
2. 运行时常量池
运行时常量池(Runtime constant pool),它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后放到常量池中。
运行时常量是相对于常量来说的,它具备一个重要的特征就是动态性,Java语言并不要求常量一定在编译期产生,运行期也可以产生新的常量,运行期产生的常量放在运行时常量池中。这里的常量包括:基本类型的包装类型和String(使用String.intern())。
3. Class常量池
常量池中主要存放两大类常量:字面量和符号引用。字面量相当于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。符号引用则属于编译原理方面的概念,包括了如下的常量类型:
类和接口的全限定名。
字段名称和描述符。
方法名称和描述符。
参考文章:
指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。 附加内存泄露概念:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。 ↩