导读:JVM是Java的精髓所在,但有时候也是Java的短板所在,所以作为一个Java程序员,掌握JVM的知识,将会帮助我们进一步掌握Java,本文所有知识点来自《深入理解Java虚拟机》
初入JVM
JVM有一个特性,那就是自动垃圾回收,帮助管理内存,让Java程序员不需要手动的去释放内存,但是有时候也是因为它的自动垃圾回收的“不彻底”,才会导致程序应用出现内存泄漏或者内存溢出
Java虚拟机运行时的数据主要保存在2个区域,一个是共享区域,一个是线程独享
Java运行时数据区分为:堆,方法区,虚拟机栈,本地方法栈,程序计数器(该区域是唯一一个不会发生内存泄漏和内存溢出的地方),前两者是共享内存的区域,后三者是线程独享的。堆一般是用来存放对象实例的(所以它也是JVM回收垃圾比较频繁的地方,一般也称为GC堆),方法区(该区域一般被称为“永生代”,这部分主要针对常量池的回收,对类型的卸载)一般用来存放类信息,常量,全局变量,即时编译时的代码数据等。程序计数器其实就是一个字节码标记的工具,字节码解释器工作时就是通过改变这个计数器的值来选取下一条执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等基础功能都需要依赖这个计数器来完成。而虚拟栈是方法执行时创建的一个栈帧用于存储局部变量表、操作数栈、动态链接、方法入口等等。局部变量表存放了编译期可知的各种基本数据类型,对象引用,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置。一个方法分配虚拟机栈的大小是完全确定的,不会在方法运行时改变。方法运行时,如果超过栈的深度,那么会抛出栈溢出异常StackOverflowError。
除了JVM分配的内存,JVM还提供了本地接口直接调用分配系统的内存,这里称为直接内存。比如JDK1.4后NIO可以直接操作系统内存,这样的操作效率是特别高的,因为可以省去从JVM的内存区域复制到系统内存区域的这份操作。不过受计算机内存的影响,这部分也会出现内存溢出的异常
对象的创建
虚拟机遇到一条new指令时,首先去检查这个指令的参数能否在常量池中定位到一个类的引用符号,并且检查这个符号引用代表的类是否已经被加载,解析,和初始化过。如果没有,那必须先执行相应的类加载过程。对象所需要的内存在类加载的过程就已经确定了。对象的创建实际上就是在堆上划一份内存。分配内存有两种方式,一种是“指针碰撞”,一种是“空闲列表”。
(读了第二章节,我才发现原来内存分配也会产生并发安全的问题。所以分配空间的时候需要做同步处理,虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆上预先分配一小块内存,称为本地线程分配缓冲)
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域,分别为对象头,实例数据,和对齐填充。
对象头包含两部分信息,第一部分用于存储对象自身运行时的数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,另一部分就是类型指针。即对象指向它的类元数据的指针,虚拟机通过这个指针来确定是哪个类的实例。如果对象是一个数组,那么对象头还需要含有这个数组的长度信息。第三部分对齐填充不是必然存在,因为VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,对象的大小必须8字节的倍数。而对象头部分正好是8字节的倍数,所以当实例数据没有对齐时,这个时候对齐填充就会发挥作用。
对象的访问定位
通过栈上的引用来访问对象,而访问的方式有两种:句柄和直接指针。
如果使用句柄的话,堆会划出一部分内存作为句柄池,而引用就是指向句柄的地址,然后句柄池中存有对象实例数据的地址和类型数据的地址。这两种访问方式各有优势,使用句柄来访问的最大好处就是引用中存储的是稳定的句柄地址,对象被移动时,只会改变句柄池中指针的地址信息,(垃圾回收时对象移动是常用的事情)而使用直接指针的好处就是访问速度快,HotSpot的访问方式是使用直接指针的方式。
关于内存泄漏和内存溢出
内存泄漏是指GCROOT可以到达,所以一直存在内存之中,没被回收,而内存溢出是指内存超过限度值。
附录:有关本节的JVM参数指令(单位均为M)
-Xms(设置堆内存的最小值)
-Xmx(设置堆内存的最大值)
-Xss(设置虚拟机栈的大小)
-XX:PermSize(方法区的最小值)
-XX:MaxPermSize(方法区的最大值)
-XX:MaxDirectMemorySize(直接内存的最大值,如果不设置默认跟堆内存一样大小)