虚拟机栈
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧。
我们经常说的堆内存和栈内存,所指的栈就是指虚拟机栈。
局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型。
打印栈信息
jstack pid
线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError
/**
* VM Args: -Xss160k
*/
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;
}
}
}
Java堆
Java堆是Java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域。
此内存区域的唯一目的就是存放对象实例。
几乎所有的对象实例都在这里分配内存。
是垃圾收集器管理的主要区域,也被称作“GC堆”。
打印堆信息
jmap -dump:format=b,file=test.bin pid
jhat test.bin
java堆的参数设置
-Xms:设置堆的最小值
-Xmx:设置堆的最大值
-XX:+HeapDumpOnOutOfMemoryError:在出现内存溢出异常时Dump出当前的内存堆转储快照
Java堆溢出
import java.util.ArrayList;
import java.util.List;
/**
* VM ARGS: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
这一次我们换一种堆分析工具,用Eclipse Memory Analyzer来分析快照文件。
案例:fastjson内存泄露测试
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.util.ParameterizedTypeImpl;
import java.lang.reflect.Type;
/**
* VM ARGS: -Xms100m -Xmx100m -XX:+HeapDumpOnOutOfMemoryError
*/
public class Main {
public static void main(final String[] args) {
UserInfo userInfo=new UserInfo();
userInfo.setName("zyr");
userInfo.setPassword("123");
WrapReturn wrapReturn = new WrapReturn();
wrapReturn.setResult(userInfo);
byte[] bytes = JSON.toJSONBytes(new WrapReturn(userInfo));
while (true){
Object o = JSON.parseObject(bytes, new ParameterizedTypeImpl(new Type[]{UserInfo.class}, null, WrapReturn.class));
}
}
}
用jvisualvm观察内存使用情况:
方法区
方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
也有人会把方法区称为永久代,本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区。
运行时常量池
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用。
除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池的另一个重要特征是具备动态性,运行期间也可能将新的常量放入池中,用得比较多的是String类的intern()方法。
String a = "test";
String b = "test";
String c = new String("test");
String d = new String("test");
System.out.println(a == b);
System.out.println(a == c);
System.out.println(c == d);
问题:
- 为什么a == b?
因为它们指向常量池里同一块引用。 - 为什么a != c?
因为a为常量池中的引用,c为堆中的实例引用。 - 为什么c != d?
因为c和d为堆中的两个不同实例的引用。 - 怎样修改代码,让a == b == c == d?
①用.equal方法来做比较,这是常用的方法,生产中都应该用此方法做比较。
②使用intern(),让所有的比较都基于常量池中的引用,a.intern() == b.intern() == c.intern() == d.intern()。这里只是为了加强大家对常量池的理解,生产环境不要这样使用。
String类的intern()方法定义:
public native String intern();
注意看,这是一个native方法,它的返回值是String,指返回常量池中的字符串。
运行时常量池溢出
import java.util.ArrayList;
import java.util.List;
/**
* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPool10M {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
java与php的比较
《深入理解Java虚拟机》中,作者常说:Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。
作为前php开发程序员,最后我来谈谈java与php的差别。
打个不太恰当的比喻,php就像一台游戏机,而java像一台计算机。
如果只是网站开发,php的nginx + php-fpm + mysql模型已经非常成熟,体现了unix的软件思想,与其他命令组合来解决问题。一个请求对应一个进程,用完即扔,完全不需要考虑内存方面的问题(就算有内存泄露,重启进程就好了),简单粗暴。在传统的网页开发兴起时风靡一时。就如同游戏机一样,针对性特别强,简单高效。
而使用java开发网站项目,不仅需要了解业务,对底层的运行原理都要有所涉及,并且需要针对性能调优。程序涉及到的方方面面都要了解,虽然更加复杂,也提供了更强大的功能。比如最近兴起的服务化,基于java可以轻松地实现,用php做就有点捉襟见肘了。就如同计算机一样,什么都能做,不仅能实现游戏的功能,还能上网聊天,这是游戏机做不到的。
参考资料:
《深入理解Java虚拟机》第2版
深入理解Java虚拟机笔记一(Java内存区域与内存溢出异常)
《Java虚拟机原理图解》3、JVM运行时数据区
Java的native方法
parseObject是否存在内存泄漏情况
深入解析String#intern