先验知识
java堆、栈
栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。堆
- 是一个运行时数据区,可动态分配内存,也无需提前告知编译器其生存周期。堆由垃圾回收自动管理。缺点是存取速度慢。
- 全局
- 内存中的数据是无序的,即先分配的和随后分配的内存并没有什么必然的位置关系,释放时也可以没有先后顺序。
- 手动释放
eg:
String str1 =new String ("abc"); String str2 =new String ("abc"); System.out.println(str1==str2); // false
一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。
静态变量、全局变量、通过new、newarray、anewarray和multianewarray等指令建立的对象通常存在于堆中。无需显式释放。
栈
- 优势是存取速度比较快,仅次于寄存器。但是缺点是不够灵活,存在栈中的数据大小与生存期必须是确定>的。
- 栈中的数据可以共享。
- 只有一个出入口的队列,即后进先出(First In Last Out),先分配的内存必定后释放。
- 自动清理
eg:栈的数据共享
int a = 3; int b = 3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处 理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。
当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。JAVA中的基本类型,其实需要特殊对待:创建一个并非是“引用”的“自动”变量。这个变量拥有它的“值”,并置于堆栈中,因此更高效。
eg:
关于String str = "abc"的内部工作。Java内部将此语句转化为以下几个步骤:
(1)先定义一个名为str的对String类的对象引用变量:String str;
(2)在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并 将o的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。
(3)将str指向对象o的地址。值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用!
为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。String str1 = "abc"; String str2 = "abc"; System.out.println(str1==str2); //true
注意:我们这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。
结果说明,JVM创建了两个引用str1和str2,但只创建了一个对象,而且两个引用都指向了这个对象。更多详情请见:java堆、栈的区别(上文内容参考自此文章)
java native关键字
运行时数据区域
程序计数器
作用:
当前线程执行字节码的行号指示器。虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在线程切换后要依赖一个独立的计数器来恢复到正确的执行位置。
特点:
- 各线程拥有各自独立的计数器,互不影响
- 线程执行java方法,计数器记录的是正在执行的虚拟机字节码指令地址
- 线程执行native(浅显理解为非java代码),计数器值为空
虚拟机栈
描述java方法执行的内存模型:方法执行时会创建一个栈帧
(存储局部变量表、操作数栈、动态链接、方法出口等信息)。
栈帧在虚拟机中从入栈到出栈的过程对应方法从调用到执行完成的过程。
上文所讲的栈指的是虚拟机栈中局部变量表
部分。
下面描述局部变量表部分作用于特点
作用:
存放基本数据类型(编译期可知)、对象引用(指向对象起始地址的引用指针、代表对象的句柄或与此对象相关的位置)、returnAdress(指向一条字节码指令的地址)。
特点:
- 线程私有
- long、double 类型占用两个局部变量空间(slot),其余类型只占用一个
- 编译期完成空间分配,进入方法时帧中分配的空间大小完全确定,运行期不变
异常:
-Xss设置栈内存容量
- stackOverFlowError:线程请求栈深度(方法调用时入栈的帧数量;在虚拟机栈容量一定时,局部变量表内容越多,栈帧越大,栈深度越小)大于虚拟机允许深度
- outOfMemoryError:虚拟机动态扩展时无法申请到足够内存
package oom;
/**
* 虚拟机栈和本地方法栈溢出
* @author Madison
* @date 2014-7-11
* VM Args:-Xss128k
*/
public class JavaVMStackSOF
{
private int stackLength = 1;
public void stackLeak()
{
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable
{
JavaVMStackSOF oom = new JavaVMStackSOF();
try
{
oom.stackLeak();
}
catch(Throwable e)
{
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
运行结果:
stack length:2402
Exception in thread "main" java.lang.StackOverflowError
at oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)
at oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)
......后续异常栈信息省略
由上可知:在单线程操作时,无论是由于栈帧太小,还是虚拟机栈容量太小,内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
操作系统分配给每个进程的内存是有限制的。譬如32位Windows限制为2GB。虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为2GB(操作系统限制)减去
Xmx
(最大堆容量),再减去MaxPermSize
(最大方法区容量),程序计数器消耗的内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。每个线程分配的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
这一点需要在开发多线程应用的时候特别注意,出现StackOverflowError异常时有错误堆栈可以阅读,相对来说,比较容易找到问题的所在。而且,如果是使用虚拟机默认参数,栈深度在大多数情况下达到1000~2000完全没有问题,对于正常的方法调用,这个深度应该完全够用了。但是,如果建立过多线程导致的内存溢出,在不减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。如果没有这方面的经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。
创建线程导致内存溢出异常代码如下,运行该代码可能会导致操作系统假死。
package oom;
/**
* 创建线程导致内存溢出
* @author Madison
* @date 2014-7-11
* VM Args:-Xss2M
*/
public class JavaVMStackOOM
{
private void dontStop()
{
while(true)
{
}
}
public void stackLeakByThread()
{
while(true)
{
Thread thread = new Thread(new Runnable()
{
@Override
public void run()
{
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args)
{
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError:unable to create new native thread
本地方法栈
作用:为虚拟机使用到的Native方法服务。HotSpot中本地方法栈与虚拟机栈合并。
异常与虚拟机栈相同。
java堆
-Xms和-Xmx控制
作用:对象实例与数组存储的空间,非绝对。。
特点:
- 所有线程共享,虚拟机启动时创建
- 垃圾收集器管理的主要区域,故常被称为“GC堆”
- 可处于物理不连续,逻辑连续的内存空间
- 可扩展
异常:
- outOfMemoryError:堆中没有内存完成实例分配且堆无法扩展
方法区
作用:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
特点:
- 所有线程共享
- HotSpot中GC分代收集扩展至此部分,1.7以后将永久代字符串常量值移除
- 可处于物理不连续,逻辑连续的内存空间
- 可扩展
异常: - outOfMemoryError:堆中没有内存完成实例分配且堆无法扩展
运行时常量池
方法区的一部分
class文件中包含:类的版本、字段、方法、接口等描述信息,还有常量池信息
作用:存放编译期生成的字面量和符号引用
特点:
- 动态性,运行时也可将新的常量放入常量池中
异常:
- outOfMemoryError:常量池无法申请到内存时
关于String.intern()的一些有趣案例
-
new String都是在堆上创建字符串对象。当调用 intern() 方法时,编译器会将字符串添加到常量池中(stringTable维护),并返回指向该常量的引用。
-
通过字面量赋值创建字符串(如:String str=”twm”)时,会先在常量池中查找是否存在相同的字符串,若存在,则将栈中的引用直接指向该字符串;若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。
常量字符串的“+”操作,编译阶段直接会合成为一个字符串。如string str=”JA”+”VA”,在编译阶段会直接合并成语句String str=”JAVA”,于是会去常量池中查找是否存在”JAVA”,从而进行创建或引用。
对于final字段,编译期直接进行了常量替换(而对于非final字段则是在运行期进行赋值处理的)。
final String str1=”ja”;
final String str2=”va”;
String str3=str1+str2;
//在编译时,直接替换成了String str3=”ja”+”va”,根据第三条规则,再次替换成String str3=”JAVA”
常量字符串和变量拼接时(如:String str3=baseStr + “01”;)会调用stringBuilder.append()在堆上创建新的对象。
-
JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。
String str2 = new String("str")+new String("01");
str2.intern();
String str1 = "str01";
System.out.println(str2==str1);
//在JDK 1.7下,当执行str2.intern();时,因为常量池中没有“str01”这个字符串,所以会在常量池中生成一个对 堆中的“str01”的引用(注意这里是引用 ,就是这个区别于JDK 1.6的地方。在JDK1.6下是生成原字符串的拷贝),而在进行String str1 = “str01”;字面量赋值的时候,常量池中已经存在一个引用,所以直接返回了该引用,因此str1和str2都指向堆中的同一个字符串,返回true。
String str2 = new String("str")+new String("01");
String str1 = "str01";
str2.intern();
System.out.println(str2==str1);
//将中间两行调换位置以后,因为在进行字面量赋值(String str1 = “str01″)的时候,常量池中不存在,所以str1指向的常量池中的位置,而str2指向的是堆中的对象,再进行intern方法时,对str1和str2已经没有影响了,所以返回false。
常见面试题:
Q:下列程序的输出结果:
String s1 = “abc”;
String s2 = “abc”;
System.out.println(s1 == s2);
A:true,均指向常量池中对象。
Q:下列程序的输出结果:
String s1 = new String(“abc”);
String s2 = new String(“abc”);
System.out.println(s1 == s2);
A:false,两个引用指向堆中的不同对象。
Q:下列程序的输出结果:
String s1 = “abc”;
String s2 = “a”;
String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:false,因为s2+s3实际上是使用StringBuilder.append来完成,会生成不同的对象。
Q:下列程序的输出结果:
String s1 = “abc”;
final String s2 = “a”;
final String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:true,因为final变量在编译后会直接替换成对应的值,所以实际上等于s4=”a”+”bc”,而这种情况下,编译器会直接合并为s4=”abc”,所以最终s1==s4。
Q:下列程序的输出结果:
String s = new String(“abc”);
String s1 = “abc”;
String s2 = new String(“abc”);
System.out.println(s == s1.intern());
System.out.println(s == s2.intern());
System.out.println(s1 == s2.intern());
A:false,false,true。
直接内存
非运行时数据区的一部分,但也会产生outOfMemoryError
案例: jdk1.4后出现NIO,基于通道(Channel)和缓冲区(Buffer)的I/O方式,可使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样可提高性能,因为避免了java堆和Native堆中的数据复制
特点:
- 会受到本机总内存(RAM+SWAP或分页文件)大小及处理器寻址空间限制