JVM内存管理以及内存溢出情况 博客分类: jvm
首先我们要了解JVM内存划分,以及每个部分的功能
JVM内存管理大概来说分为 堆 和栈 两个部分
堆上是所有线程共享的数据区,而栈上数据是私有的
上图是JAVA运行时虚拟机时的数据区
方法区: 保存类的class信息,类描述,类的方法描述等,其中还有一部分就是 运行时的常理池
运行时的常理池
/** * java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean, * 另外两种浮点数类型的包装类则没有实现。 * 另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用对象池, * 也即对象不负责创建和管理大于127的这些类的对象。以下是一些对应的测试代码: * String 也是实现了常理池的 intern()方法参考 * @author * @version 1.0 * @since 1.0 */ public class Test { public static void main(String[] args) { //5种整形的包装类Byte,Short,Integer,Long,Character的对象, //在值小于127时可以使用常量池 Integer i1 = 127; Integer i2 = 127; System.out.println(i1 == i2);//输出true //值大于127时,不会从常量池中取对象 Integer i3 = 128; Integer i4 = 128; System.out.println(i3 == i4);//输出false //Boolean类也实现了常量池技术 Boolean bool1 = true; Boolean bool2 = true; System.out.println(bool1 == bool2);//输出true //浮点类型的包装类没有实现常量池技术 Double d1 = 1.0; Double d2 = 1.0; System.out.println(d1 == d2);//输出false } }
引用《深入理解java虚拟机》
2.4.3 方法区和运行时常量池溢出
由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行。前面提到JDK 1.7开始逐步“去永久代”的事情,在此就以测试代码观察一下这件事对程序的实际影响。
String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK 1.6及之前的版本中,由于常量池分配在永久代内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量,如代码清单2-6所示。
代码清单2-6 运行时常量池导致的内存溢出异常
- /**
- * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
- * @author zzm
- */
- public class RuntimeConstantPoolOOM {
- public static void main(String[] args) {
- // 使用List保持着常量池引用,避免Full GC回收常量池行为
- List<String> list = new ArrayList<String>();
- // 10MB的PermSize在integer范围内足够产生OOM了
- int i = 0;
- while (true) {
- list.add(String.valueOf(i++).intern());
- }
- }
- }
运行结果:
- Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
- at java.lang.String.intern(Native Method)
- at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)
从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。
而使用JDK 1.7运行这段程序就不会得到相同的结果,while循环将一直进行下去。关于这个字符串常量池的实现问题,还可以引申出一个更有意思的影响,如代码清单2-7所示。
代码清单2-7 String.intern()返回引用的测试
- public class RuntimeConstantPoolOOM {
- public static void main(String[] args) {
- public static void main(String[] args) {
- String str1 = new StringBuilder("计算机").append("软件").toString();
- System.out.println(str1.intern() == str1);
- String str2 = new StringBuilder("ja").append("va").toString();
- System.out.println(str2.intern() == str2);
- } }
- }
这段代码在JDK 1.6中运行,会得到两个false,而在JDK 1.7中运行,会得到一个true和一个false。产生差异的原因是:在JDK 1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。而JDK 1.7(以及部分其他虚拟机,例如JRockit)的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。对str2比较返回false是因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这些区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。虽然直接使用Java SE API也可以动态产生类(如反射时的GeneratedConstructorAccessor和动态代理等),但在本次实验中操作起来比较麻烦。在代码清单2-8中,笔者借助CGLib直接操作字节码运行时生成了大量的动态类。
堆上方的对象实例和数组
推荐阅读
-
JVM内存管理以及内存溢出情况 博客分类: jvm
-
【转】java虚拟机运行时的内存分类以及出现异常分析 博客分类: JVM java虚拟机 jvm 异常
-
Java内存分配 博客分类: Java基础 java内存堆栈JVM
-
JVM内存状况查看方法和分析工具 博客分类: Java JVM内存状况查看方法和分析工具
-
JVM理论与实践【堆内存结构与垃圾回收】 博客分类: Java jvmgc堆内存垃圾回收虚拟机
-
JVM理论与实践【堆内存结构与垃圾回收】 博客分类: Java jvmgc堆内存垃圾回收虚拟机
-
JVM故障诊断与性能优化 博客分类: JVM 内存模型
-
Java内存模型-JMM 和 jvm优化 博客分类: Java JMM
-
JVM内存分析以及4种内存溢出 博客分类: java汇总 JVMGCHotSpot
-
Sun JVM内存管理与Java虚拟机(JVM)垃圾回收器的工作机制 博客分类: OOM jvm虚拟机javasun工作