jvm原理
一、jvm
jvm是什么
JVM是Java Virtual Machine的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
二、Java 内存区域
1、程序计数器
看作当前现成的行号指示器,线程私有,内存较小,如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
2、 Java 虚拟机栈
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型,每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、
操作数栈、
动态链接、
方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
3、本地方法栈
本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。hotspot直接把2和3合并了.
4、java 堆
java堆事所有线程共享的一块区域,存放对象的实例.所有对象都在这里分配内存,java堆事垃圾收集管理的区域,所以也被称为GC堆,OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
5、方法区
各个线程共享的区域,存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据.也称非堆.
6、运行时常量池
是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。
7、直接内存
非虚拟机的数据区部分,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。
三、对象的创建过程
1、对象的创建
遇到new指令时,首先检查能否在常量池中定位到一个类的符号引用,检查这个符号引用代表的类是否已经被加载、解析、初始化.如果没有,则进行加载.
类加载检查通过后,为新对象分配内存,在堆的空闲内存划分一块区域
每个线程在堆中会有私有的分配缓冲区,可以避免并发情况下频繁创建对象造成的线程不安全.
内存分配完后,初始化为0(不包括对象头),接下来填充对象头,把对象是哪个类的实例、如何找到类的元数据信息,对象的哈希吗,对象的GC分带年龄存入对象头
执行new之后init才算是一盒对象创建的真正的完成.
2、对象的内存分布
在hotspot中,分为3个区域:对象头(header)、实例数据(Instance Data)和对齐填充(padding)
对象头:第一部分用于存储对象自身运行时数据.GC分代年龄,hash码,锁状态标志,线程池有的锁,32位虚拟机占32bit,64位虚拟机占64bit,官方称为mark word,第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例.如果是java数组,对象头中还会有一块记录数组好惨风度的数据.因为普通对象可以通过java员数据确定大小,而数组对象不可以.
实例数据:程序代码中所定义的各种类型的字段内容.
对齐填充:占位,保证对象大小是某个字节的整数倍.
3、对象的访问定位
使用对象时,通过栈上的reference数据来操作堆上的具体对象.
通过句柄访问:
java堆中分配一块内存作为句柄池.refernce存储的是句柄地址.如图
使用直接指针访问
四、垃圾回收器与内存分配
程序计数器,虚拟机栈,本地方法栈随着线程生灭,而java堆和方法区则不一样,一个接口中多个实现类需要的内存可能不一样,一个方法中多个分支需要的内存可能也不一样.只有在运行时我们才知道哪些对象会被创建,这部分内存的回收和分配都是动态的,垃圾回收关注的就是这部分内存.
1、引用计数法
给对象添加一个引用计数器,难以解决循环引用问题.
2、可达性分析法
通过GC root的对象作为起始点,从这些节点出发走过的路径成为引用链.当一个对象到达GC roots没有任何引用链相连,说明对象不可用.
可以作为GC roots的对象:
- 虚拟机栈中引用的对象
- 方法去中类静态属性引用的对象
- 方法区中常量引用的对象
- JNI引用的对象
3、垃圾回收算法
1、标记清除算法
直接清除标记
效率不高,会产生大量的碎片
2、复制算法
将空间分成两部分,没次只对其中一块进行GC.当这快内存使用完,就将复活的对象复制到另一块上
解决了前一种方法的不足,但是会造成空间利用率低下.因为大多数新生代对象都不会熬过第一次GC,所以没必要1:1划分空间.可以划一大块eden和小survivor中空间,没次使用eden空间和其中一块survivor,当回收时,将eden和survivor中还存活的对象一次性复制到另一块survivor上,最后清理survivor.
3、标记整理算法
清除标记后,进行碎片整理,
4、分代回收
根据存活对象划分几块内存区,一般分为新生代和老年代.然后根据各个年代的特点制定相应的回收算法.
新生代:每次GC都有大量对象死去,只有少量存过,选择复制算法比较合理
老年代:标记整理或标记清除.