Java虚拟机浅探
- 简介
对于java开发人员了来说,对java虚拟机肯定有着或多或少的了解。因为有了虚拟机的存在,才会使得java的内存管理变得那么方便,不再像C++那样使用new/delete来直接管理内存。知名的java虚拟机有:Sun Classic/Exact VM(第一款商用虚拟机,已退役)、Sun HotSpot VM(最常用)、IBM J9 VM、Apache Harmony、Mircrosoft JVM等。了解虚拟机的机制和规则,对大型服务器的管理和开发都具有重要意义。
- 初步研读总结
如图虚拟机的大致构成如上图,虚拟机通常由方法区,虚拟机栈,本地方法栈,堆和程序计数器构成。
程序计数器:程序计数器可以看成是当前线程所执行的字节码的行号指示器,也就是类似取址寄存器,解释器会取得它的值来执行下一条代码的指令。它只占很小的一块内存,通常是可以忽略的。
java虚拟机栈:这块内存是线程私有的,生命周期和线程的生命周期是一致的。它描述的是运行时方法的模型,每个方法会创建一个栈帧,存储局部变量、操作数栈、动态链接、方法出口等信息。
本地方法栈:java包中有部分方法带有native关键字,它的描述大致和虚拟机栈是差不多的,只不过面对的是native方法。
java堆:它是线程共享的一块资源,算是内存中最大的一块,存储大多数的对象实例,也就是new出来的那些。这块区域通常分为eden、新生代、老年代。当堆无法分配内存的时候就会产生:OOM(out of memory)异常。
方法区:和java堆有着差不多的特质,是各个线程共享的内存区,但是这个内存区(在HotSpot上)不会被GC(也成为永久代)。
运行时常量池:它是方法区的一部分,存储编译时期生成的各种字面量和符号引用。
对象的创建:入门的时候hello world写的大致是Hello h = new Hello();这个new的指令就是产生一个对象,也就是在java堆中申请一个内存。一般虚拟机会先检查常量池中是否存在这个对象的引用、该类是否被解析加载过,如果没有则执行相应的类加载过程。java堆中内存是一块规划好的内存,它的两种规划方式是指针碰撞(Bump the Pointer)和空闲列表(Free List)。指针碰撞是内存化为使用过的内存和未使用的内存,中间用指针来隔绝,这是防止内存泄漏的手段(若A在申请内存的时候B没来得及释放,A分配到了B的内存就会引起内存泄漏);空闲列表就是使用一个列表记录内存中尚未被使用的内存,也是一种防止内存泄漏的机制。内存分配完成之后就会把分配到的内存初始化为0值,所以我们申请的类的字段通常默认都是0.
对象的访问定位:第一种方式是通过java栈中的reference来指向堆中对象句柄的部分,句柄管理着具体的对象实例的内存,还有存在常量池的java数据类型;第二种方式是直接不用句柄,reference指向的是堆内存的具体对象位置。
- 实例分析
打开eclipse,偏好设置,preferences的java下的install jres,设置内存的下限和上限
写一个语句来看看内存是否设置成功
当前堆内存的kb数,经过计算略小于32m一点,说明设置成功了。
接下来我们写一段程序
从结果来看,申请了273485个对象之后产生了内存溢出的异常,并且提示的是heap space。这说明了堆内存溢出。一般这种异常在程序中处理优化的思路就是,检查是否有无用对象存活过久没有gc、提高虚拟机内存的限制。
除了堆内存的溢出之外,方法栈和虚拟机栈也会有溢出的异常。它们的溢出分为两种,一种是扩展时候申请内存不足则会抛出OOM,另一种是栈空间不足就会有stackoverflowerror,事实我只能测试出后者。我通过实验方法探究出栈深度大概在18000这样的深度,配合堆内存来估计的话,这个栈深度也不会少于2000,对于大型系统来说,除非死循环递归不然应该不会超过这个深度,如果存在这个异常,一是调整程序,二是提高虚拟机的内存。
接下来我们继续实验,通过创建线程让内存溢出。
它的提示是,unable to create new native thread,表示本地方法栈已经溢出。这样的错误只能去优化你的程序了。
java程序员最熟悉的常量池使用就是String.intern()了,它是检查常量池是否存在,存在就返回这个字符串的引用,不然就创建这个字符串在常量池并返回引用。
常量池溢出我一直没测试出来,不知道是不是1.8的新特性。
书上还有PermGen space和本机内存溢出等,至此初步了解了java内存的基本结构和一些常见错误的产生和处理思路,对虚拟机的理解也加深了。
- 参考
《深入理解Java虚拟机(第二版)》 第二章