学Java三年有余,对自己来说JVM一直以来都是黑匣子,看不懂,摸不透。作为一个有技术情节、略带些许完美主义情节的攻城狮,不了解JVM似乎有时候夜不能寐,总觉得有一个未知的世界自己需要探索。理论为实践服务,学习JVM不是因为它好玩,其实一点都不好玩,只是因为它有用罢了。实用主义者总比快餐主义让人踏实。 好吧,好奇心害死猫(Curiosity kills the cat),中秋佳节还面对电脑,真是罪过。闲话不多说了,否则显得自己很没礼貌。
学习JVM原理之前,只知道JVM的内存结构有堆、栈之分,堆中存放对象,栈中存放对象的引用。事实上,情况远比上面的一句话复杂的多,当然我仍然没忘记自己是实用主义者,因为我实在想不出有什么理由,去花费大把的时间去和JVM纠缠。
【JVM的内存结构】
简单起见,可以将JVM的内存结构划分为Java堆、虚拟机栈、方法区、本地方法栈、运行时常量池和直接内存。这样的划分不见得合理、或者逻辑清晰,只是为了学习方便而已。如下图所示:
1. Java堆:Heap
是运行时的数据区,主要用来存放Java的对象、数组内容。
2. 虚拟机栈:Virtual Machine Stacks
也可以称之为线程栈,是线程运行的内存区域,存放对象的引用(对象在堆中存放的内存地址),通过对象的引用来访问对象或者数组。
3. 方法区:Method Area
用来存放被虚拟机加载的类的相关信息(如类中的字段、方法的签名等)以及一些其他的元数据。方法区 在JVM规范中被描述为堆(Heap)的一个逻辑组成部分,但是通常称之为非堆(Non-Heap)内存,以表明其和 用来存放对象、数组的堆的不同。在Sun公司提供的HotSpot虚拟机中,方法区使用Java堆(Heap)中的永久代(Permanent Generation)来进行存放。
4. 本地方法栈:Native Method Stacks
用于运行本地方法(Native method)的内存区域,在Sun公司的HotSpot虚拟机中本地方法栈、线程栈共用一块内存区域。
5. 运行时常量池:Runtime Constant Pool
存放运行时的产生的大量常量,如字符串常量,存放在如上所述的方法区。
6. 直接内存:Direct Memory
JDK1.4中加入的NIO(非阻塞I/O)操作类、引入了基于通道(Channel)和缓冲(Buffer)的I/O方式,通过使用Native函数分配堆外内存,然后使用DirectByteBuffer对象来操作这块内存。直接内存和Java堆内存的分配没有任何交集,但是堆内存大小的设置会间接直接内存的可用空间。
【应用实例】
1. 产生Java堆溢出:通过创建大量的对象,并且保持引用来实现。
定义大对象:
- class MyObject {
- byte[] buffer = new byte [1024 * 1024];
- }
创建对象,并添加到List集合以避免被垃圾回收器回收:
- List<MyObject> bufferList = new ArrayList<MyObject>();
- int i = 0;
- while(true) {
- System.out.println("第" + (i++ + 1) + "次添加对象.");
- bufferList.add(new MyObject());
- }
设置虚拟机参数:
右键Run As快捷菜单,选择“Run Configurations...”
切换到Arguments选项卡,在VM Arguments文本框输入参数
- -Xms20m -Xmx20m
运行程序,抛出Java堆内存溢出异常:
java.lang.OutOfMemoryError: Java heap space
2. 线程栈溢出:通过大量的递归调用来消耗线程栈内存。
定义递归调用函数:
- private int i =0;
- private void recursive() {
- System.out.println(++ i);
- recursive();
- }
设置VM Arguments参数:
- -Xss128k
运行该程序抛出栈溢出异常:
java.lang.StackOverflowError
============================
Ok,JVM内存结构学习到此结束,接下来该看看垃圾回收GC啦!