JVM加载,Java垃圾回收机制
JVM加载过程:
JVM是Java Virtual Machine(Java虚拟机)的缩写
JVM的组成部分
1. 类加载器 Class Loader
类加载器的作用是加载类文件到内存,比如编写一个HelloWorld.java程序,然后通过javac编译生成class文件。由Class Loader将class文件加载到内存中。但是Class Loader加载class文件有格式要求。
注意:Class Loader只管加载,只要符合文件结构就加载,至于能不能运行,是由Execution Engine负责。
2. 执行引擎 Exexution Engine
执行引擎也叫作解释器,负责解释命令,提交操作系统执行。
3. 本地接口 Native Interface
本地接口的作用是为了融合不同的编程语言为Java所用。它的初衷是为了融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须要有一个聪明的、睿智的调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载加载native libraries。目前该方法只有在与硬件有关的应用中才会使用,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用WebService等。
4. 运行数据区 Runtime data area
运行数据区使整个JVM的重点。我们所写的程序都被加载到这里,之后才开始运行,Java生态系统如此的繁荣,得益于该区域的优良自治。
JVM加载.class文件的原理机制
Java垃圾回收机制
在java中,内存动态分配和垃圾回收的问题 交给JVM处理,
什么样的对象才是垃圾?
在Java中这个对象没有被其他对象所引用,该对象就是无用的.此对象称为垃圾.
Java中标记垃圾的算法有两种:1引用计数法和可达性分析算法,
1.引用计数法:
引用计数法就是给对象中添加一个引用计数器,每当一个地方引用,计数器+1,引用失效,计数器-1,任何时候计数器位0的对象就是不可能再被使用,可当做垃圾回收.
优点:执行效率高,程序执行受影响较小.
缺点:无法检测出循环引用的情况,导致内存泄漏.
public class MyObject { public MyObject childNode; }
public class ReferenceCounterProblem { public static void main(String[] args) { MyObject object1 = new MyObject(); MyObject object2 = new MyObject(); object1.childNode = object2; object2.childNode = object1; } }
object1和object2并没有任何价值,但是他们循环引用,造成内存泄露。
可达性分析算法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
那么什么对象可以作为GCRoot?
-
虚拟机栈中的引用对象
-
方法区中的常量引用对象
-
方法区中的类静态属性引用对象
-
本地方法栈中的引用对象
-
活跃线程中的引用对象
可达性分析算法如下图所示
那么不可达的对象是否是必死之局呢?答案也是否定的
在可达性分析法中不可达的对象,它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
如何将垃圾回收?
在Java中存在着四种垃圾回收算法,标记清除算法、复制算法、标记整理算法以及分代回收算法。我们接下来会分别介绍他们。
标记清除算法
该算法分为“标记”和“清除”两个阶段:标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。它是最基础的收集算法,效率也很高,但是会带来两个明显的问题:
-
效率问题
-
空间问题(标记清除后会产生大量不连续的碎片)
该算法具体流程如下图所示
复制算法
为了解决效率问题,我们开发出了复制算法。它可以将内存分为大小相同的两块,每次使用其中的一块。当第一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
简单来说就是该对象分为对象面以及空闲面,对象在对象面上创建,对象面上存活的对象会被复制到空闲面,接下来就可以清除对象面的内存。
这种算法的优缺点也比较明显
-
优点:解决碎片化问题,顺序分配内存简单高效
-
缺点:只适用于存活率低的场景,如果极端情况下如果对象面上的对象全部存活,就要浪费一半的存储空间。
标记整理算法
为了解决复制算法的缺陷,充分利用内存空间,提出了标记整理算法。该算法标记阶段和标记清除一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。 如下图所示:
分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法就是根据具体的情况选择具体的垃圾回收算法。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
Java手动回收垃圾:
1.对象=null;
2.System.gc();
如果内存确实很紧张,调用System.gc() 方法来建议垃圾回收器开始回收垃圾。通知GC运行,但是Java语言规范并不保证GC一定会执行。
3.finalize()
finalize()是在java.lang.Object里定义的,也就是说每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用。其实gc可以回收大部分的对象(凡是new出来的对象,gc都能搞定,一般情况下我们又不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。
特殊情况下,需要程序员实现finalize,当对象被回收的时候释放一些资源,比如:一个socket链接,在对象初始化时创建,整个生命周期内有效,那么就需要实现finalize,关闭这个链接。
使用finalize还需要注意一个事,调用super.finalize();
一个对象的finalize()方法只会被调用一次,而且finalize()被调用不意味着gc会立即回收该对象,所以有可能调用finalize()后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会调用finalize(),产生问题。 所以,推荐不要使用finalize()方法,它跟析构函数不一样。