内容节选自《深入理解JAVA虚拟机》
1.JVM的内存组成
线程隔离的区域:
程序计数器:管理程序运行的顺序,每一个处理器(核)都会执行一条线程的指令,
各个线程计数器互不影响,独立存储,因此是私有独立的。
虚拟机栈(栈):记录每一个方法从调用到结束的过程,就是一次栈帧的入栈出栈。
因此生命周期与线程一致。
里面包含:局部变量表,数栈,动态链接
局部变量表:八种数据类型以及引用类型(存放的是地址),其中double和float占用2个Slot,其他占用1个
局部变量表所需内存空间是固定不变的,在编译期间完成分配,运行期间不会改变空间大小。
既然是固定的,说明栈的深度是确定的,当线程申请的深度(递归运算)超过jvm所规定的深度,
则报StackOverFlowError,但是jvm是可以动态扩展的,当线程申请的深度超过扩展最大深度时,
则报OutOfMemoryError,内存溢出。
本地方法栈:执行本地方法。
线程共享的区域:
方法区:类信息,常量,静态变量等,当内存不够时则报OutOfMemoryError异常。常量池就属于方法区
堆:堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。
(方法区和堆组成我们常说的堆内存,方法区存储的信息与类同在,堆存储的信息与对象同在)
对象的创建过程:
当虚拟机遇到一条new指令时,首先去常量池判断这个指令参数是否存在类的符号引用,
并且判断该引用所代表的类是否加载,解析,初始化过。如果没有,就先进行类加载。
(说白了就是判断你new的是不是一个从来没有new过的对象,并且该对象所属的类也是未加载过的)
其次就是为对象分配内存空间
内部实现是通过指针碰撞,也就是一个指针指向一个内存块
(类似于一排麻将,一个指针指一个麻将),如果有新的对象产生,指针就往旁边移动一个位置。
另一种情况是内存分布不均匀不整齐,类似于一桌子的麻将,只有几个麻将是可以用,这时候就需要
一个列表记录可用的内存(麻将),称之为空闲列表。
重点:那么问题了,当创建对象的动作很频繁的时候,指针都还未改变位置,新的对象就创建好了,并且用了
指针将要指的那块内存,如何做到线程安全呢?
方式一:分配内存空间的动作进行同步处理(也就是A执行完,B再执行)
方式二:分配内存空间的动作按照线程划分在不同空间之中,也就是每个线程拥有自己的一块小内存。
称之为本地线程分配缓冲。
关于方式二:①TLAB空间很小,存不了大对象②只是说这块内存其他线程无法使用,但是可以访问。
③允许浪费空间,导致空间不连续,积少成多,不好打理。
最后就是将内存空间置为零(不包括对象头),对象头里面设置你这个对象属于哪个类的实例,
你这个对象的哈希码,元数据信息,GC分代年龄。
还有最后一步,执行init方法,初始化。
对象访问定位两种方式
一种是指针直接访问,说白了就是this.play(),A.play()这种方式,速度快,指向的是对象的直接地址
另一种就是句柄池,就是分为对象实例数据指针(与对象共存的数据)和对象类型数据指针(与类共存的数据)。
当对象移动时,只是改变对象的实例数据指针,对象类型数据不会改变,这就意味着使用句柄很稳定,
而直接使用指针,当指针改变了,就是指向另一个对象的直接地址了。(这句话正确性有待商榷)
2.JVM垃圾回收
问题一:如何判断一个对象是否死亡呢?
引用记数法:给每个对象添加一个引用计数器,当它引用其他对象时加1,引用失效时减1,
当计数器为0时则证明该对象没有被任何对象引用,就可以回收。
局限性:当两个对象之间互相引用,他们的计数器不为0,但是没有其他对象引用他们,
因此这两个对象也应该是要回收的。
可达性分析法:将GC-ROOT作为顶端根节点,从根节点向下顺着路径进行搜索,这段路径
则称之为引用链,当根节点顺着引用链无法达到一个对象节点时(抑或是该对象与根节点
之间没有引用链相连),则认为该对象为可回收对象。
问题二:以上方法都是通过是否引用来判断对象是否回收,如果存在一些“食之无味,弃之可惜”
的对象呢?也就是这些对象需要但又不经常使用,这种情况只通过引用去判断就显得狭隘了。
引用分级
强引用:普遍使用和存在的对象 例如 Object obj = new Object(),只要强引用还在
回收器永远不会回收它引用的对象
软引用:一些还有用处但不是必要的对象,在系统内存溢出之前,将他们进行回收,
(说白了就是用软引用关联的对象就是避免内存溢出而将他们回收的)可以用SoftReference类实现
弱引用:比软引用低一个等级,当下一次垃圾回收时,无论内存是否足够,
都只会回收弱引用关联的对象
(说白了就是弱引用关联的对象在下一次垃圾回收时回收掉)可以用weakReference类实现
虚引用:与虚引用关联的对象被垃圾回收器回收后会收到一个系统通知。使用PhantomReference类实现。
问题三:说了那么多废话,还是没有说到如何判断一个对象死亡?
判定一个对象死亡需要两次标记,第一次通过可达性分析后,没有与引用链关联的对象将会被标记
并且进行一次筛选,筛选的条件是该对象是否需要执行finalize方法(两种情况虚拟机默认不执行,
一种是虚拟机已经调用过finalize方法,另一种时finalize方法被重写)。
对象的自救,finalize方法
当对象被判定需要执行finalize方法时,会进入到一个F-Queue队列,虚拟机会创建一个低优先级的线程去执行它,但不意味着虚拟机等待整个方法结束,在过程中如果存在对象执行缓慢,导致后面的对象保持等待状态,整个内存系统就可能崩溃,或者是在执行finalize方法之前对象只要关联到引用链上的某个对象,例如通过this关键字赋值给对象的成员变量或者类变量,便可以逃过一劫。
(图片来自于互联网)
重点:这种方法不推荐使用,原因是开销大,不稳定。。
用什么方法呢?try finally 不仅仅处理异常,还能够进行垃圾回收的处理。
回收方法区:就是对方法区的类信息,静态变量,或者常量进行回收
主要回收废弃常量和无用的类,效率不是很高,因为很麻烦
回收废弃常量很简单,只要该常量没有被别的地方所引用,就可以直接清理
回收无用类就比较复杂,如何判断无用类呢?
①该类的所有实例都被回收。
②加载该类的ClassLoader被回收
③该类对应的java.lang.class对象没有被任何地方引用,无法
在任何地方通过反射访问该类的方法
但是满足以上条件也不是意味着该类一定会被回收,还有各种参数进行设置。
垃圾回收算法
1.标记-清除算法
将需要回收的对象进行标记,然后再统一清除
缺点:易留下不连续的内存空间。
2.复制算法
将一块内存区域分为两块,其中一块为空白区域,另一块为分配区域。
当分配区域所剩无几时,也就是要进行回收时,将存活的对象复制到
到另一块区域,并且按指针顺序排列,接着就将分配区域清理掉。
优点:效率高,操作简单
缺点:直接砍了一半内存。并且当存活对象过多时,则要复制多次,
因此老年代不使用复制算法。
3.标记-整理法
将存活对象移动到内存区域的边界,然后将边界以外的区域直接清除。
适用于老年代,这样要清理的区域也就很少了。
4.分代回收算法(重点)