Java虚拟机第一篇
熟读《深入理解java虚拟机》,将自己的所学所感记录下来
运行时数据区
根据java虚拟机规范,JVM在执行程序时会将它所管理的内存划分为5大块数据区域,分为叫做方法区,堆,虚拟机栈,本地方法栈,程序计数器。方法区和堆都是所有线程共享的数据区,其余是线程隔离的数据区
1.虚拟机栈和本地方法栈
虚拟机栈是线程隔离的数据区,理所当然它的生命周期随着线程的创建而产生,线程的销毁而终止。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧
(stackframe)用于存储局部变量表
、操作数栈
、动态链接
、方法出口
等信息。每个方法从调用到完成就是一个栈帧在虚拟机栈中入栈到出栈的过程。
大多数程序员所认知的堆和栈就是JVM的全部,而所指的栈就是虚拟机栈了,或者可以说是虚拟机栈的局部变量表部分。局部变量表存放了编译器可知的各种数据类型(java的8大基本数据类型),对象引用类型
(它不等同于对象本身,可能是一个指向对象的指针或者是指向一个代表对象的句柄或其他于此对象相关的位置)和returnAddress
类型(指向一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用两个局部变量空间,其余数据类型只占用一个。局部变量表所需的内存空间在编译期间完成匹配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在java虚拟机规范中,对这个区域定义了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,且扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈和虚拟机栈所发挥的作用是非常相似的,他们之间的区别是本地方法栈是为native
方法服务的。
2.程序计数器
程序计数器是一块较小的内存空间,由线程自身维护,我们可以将它看作是当前线程所执行的字节码的行号指示器。由于Java的多线程实际上是由线程轮流切换并分配处理器时间片
来实现的,只是切换很快就让人以为是并发的。
因此,为了线程切换后能够回到原来的位置,每条线程都需要有一个独立的程序计数器。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native
方法,这个计数器值则为空。此内存区域是java虚拟机规范中唯一一个不会存在OutOfMemoryError。
3.Java堆
Java堆的唯一目的
是分配对象实例
,几乎所有的对象实例都在这里分配内存,java规范的定义是:所有对象实例和数组都在堆上分配内存,但是随着JIT编译器
的发展和逃逸分析技术
的逐渐成熟,栈上分配
、标量替换优化技术
将会导致一些变化。
4.方法区
方法区存放的是已被虚拟机加载的类信息
,常量
,静态变量
、即时编译器编译后的代码
等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做非堆
,目的是与堆区分开来。
对于习惯在Hotspot虚拟机上开发、部署程序的人来说,很多人愿意把方法区称为“永久代
”,本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代实现方法区而已,便于HotSpot收集器管理这部分内存。
5.运行时常量池
运行时常量池是方法区的一部分,Class文件中除了有类的版本
、字段
、方
法、接口
等描述信息外,还有一项是常量池
,用于存放编译期生成的各种字面量
和符号引用
,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池对于Class文件常量池的另一个重要特性是具备动态特性,Java语言并不要求常量只会在编译期生成,也就是并非预置入Class文件中常量池的内容才能进入方法区常量池,运行期间的例子就是String
的intern()
方法
6.直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分的频繁使用也会导致OutOfMemoryError
。
在jdk1.4中加入了NIO
(NewInput/Output)类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库
直接分配堆外内存
,然后通过一个存储在Java堆中的DirectByteBuffer对象
作为这块内存的引用进行操作。这样能在一些场合中显著提升性能,因为避免了在Java堆和Native堆中来回复制数据。
垃圾收集
怎样判断对象已死,回收它的内存?
1.引用计数算法
给对象添加一个引用计数器,当有一个引用时+1,引用失效就-1.
优点:实现简单,判断效率高,在大部分情况下是一个不错的算法,著名的应用案列有微软公司的COM技术,Python语言,使用ActionScript3的FlashPlayer等。
缺点:很难解决对象的相互循环引用问题。
2.可达性分析算法
基本思路:通过一系列的称为“GCRoots”的对象作为起始点,从这些节点往下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象无用。
在Java语言中,可作为GC Roots的对象包括下面几种:
1)、虚拟机栈中引用的对象
2)方法区中类静态变量属性引用的对象
3)方法区中常量的引用对象
4)本地方法栈中JNI引用的对象
3.再谈引用
无论是那种算法判断存活都与“引用”相关,所以为了将引用定义的更加清晰,以便于内存空间的管理,Java将引用定义了4种,强应用,软引用,弱引用,虚引用。这4种引用强度依次减弱
- 强应用(Strong Reference)
- 软引用(Soft Reference)
- 弱引用(Weak Reference)
- 虚引用(Phantom Reference)
强引用
强应用就是代码中普遍存在的写法。1
Object o = new Object();
软引用
软引用就是用来描述一些还有用但并非必须的对象,在内存不足的情况下它才会被回收,Java提供了SoftReference类来实现软引用1
2
3Object o = new Object();
SoftReference<Object> sr = new SoftReference<>(o);
Object object = sr.get();弱引用
弱引用也是用来描述非必须对象的,它的引用强度更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收之前,Java提供了WeakReference类来实现弱引用。使用方法和软引用一致。- 虚引用
它是最弱的一种引用,它的存在不会影响对象的回收,也无法使用它来获取对象的实例,它存在的唯一目的是在与它关联的对象实例被回收的时候收到一个系统通知
回收方法区
永久代的垃圾回收主要有两个方面:废弃常量和无用的类。
- 废弃常量
以常量池的字面量为例,假如"abc"
字符串已经存在于常量池了,但是在当前系统中没有一个地方引用了这个常量,如果发生内存回收且有必要的话他就会被回收。 - 无用的类
判断无用的类要同时满足以下3个条件- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
满足这些条件就有可能被虚拟机回收。
Java分代收集
1.Java年轻代和年老代
年轻代分为一个
Eden
区和两个survivor
区(分别叫做from
和to
),比例一般为Eden:survivor=8:1
,新new的对象都是分配在Eden区中(一些大对象特殊处理),经过第一次GC过后,如果对象依然被引用,那么就会移动到survivor
区,对象在survivor
区每多熬过一次MonitorGC
,年龄就会增加一岁,当年龄达到一定时,会被移动到年老代中(Old区)。年轻代的对象大部分都是朝生夕死的,所以GC采用的是复制算法,复制算法就是将内存分为两块,一块用完了就将活着的对象移到另一块上面去,好处是不会产生内存碎片,缺点是会造成一半的内存浪费。年老代:Old区
当年老代被塞满之后就会触发Full GC
,非常耗时,Old区GC算法为标记-清除或者标记-整理算法。
标记-清除算法就是将内存还存活的对象标记,然后清除没有标记的对象,它存在两个问题,一是效率问题,标记和清除效率都不高,而是空间问题,清除之后会造成大量的空间碎片,就使得如果有比较大的对象没有连续空间分配就会再次触发GC。标记-整理
算法和标记-清除
算法的不同就是,所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。堆中分配很大的对象
所谓大对象,是指需要大量连续内存空间的Java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC
为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection
开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction
,这个参数用于设置在执行多少次不压缩的FullGC
后,跟着来一次带压缩的System.gc()
方法的调用
此方法的调用是建议JVM进行Full GC
,虽然只是建议而非一定,但很多情况下它会触发Full GC
,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可以通过-XX:+ DisableExplicitGC
来禁止RMI
调用System.gc
。