对于任意一个类,都需要经历这样一个过程:
这个过程就是类的生命周期,从加载到初始化,属于类加载过程,其中验证、准备和解析都属于连接操作。
加载是整个类加载中的一个阶段,该阶段通过类加载器将类的字节码文件加载到内存中,并创建对应的Class对象。说白了就是找到.class文件。
验证加载阶段得到的.class文件是否合法(.class文件的格式是固定的)
为.class文件中的类变量(static修饰)分配内存空间并初始化(不会赋值)。
对于public static int num = 10;
准备阶段num初始化为0而不是10
将字符串常量池中的符号引用变为直接引用,即初始化常量。
符号引用就相当于字符串在文件内部的位置,不能直接通过符号引用访问到字符串;直接引用就是绝对位置,即可以通过这个引用直接访问到字符串。
执行类的初始化(包括静态变量的赋值和静态代码块的执行,如果有父类先加载父类)
准备阶段和解析阶段是可以顺序互换的,但是都要在初始化阶段之前。
双亲委派模型是一种类加载机制,在加载阶段执行。
JVM内置了三种类加载器,分别用于加载不同的类:
当一个类收到类加载请求,他不会直接去加载,而是把这个请求委托给父类加载器,如果父类加载器还有父类,就继续向上委托,直到顶层的启动类加载器。顶层的启动类加载器没有父类,就去查找对应范围内是否存在该类,存在就加载并返回,否则交给子类加载器。如果最底层的类加载器也没有找到,则抛出ClassNotFoundException(受查异常)
假如有A和B两个类需要加载,它们的父类是C,在加载A类之前就会先加载C类,此后再去加载B类就不用加载C了;如果直接从顶层类加载器开始加载,就可能出现同一个类被不同的类加载器重复加载的情况,从而得到不同的对象。
没有双亲委派模型(先访问顶层类加载器),就意味着可以自己实现一些核心类,这显然是不允许的,并且也不能保证安全。
双亲委派模型并不是强制的,有些情况下需要打破,例如热部署等
在传统的编程语言(如C/C++)中,需要手动分配和释放内存。这往往会出现内存泄漏和内存溢出的问题,为了尽量避免这样的问题,JVM引入了垃圾回收机制,自动的释放无用内存,从而减少开发人员的负担。
JVM确认是否为垃圾的算法是可达性分析,它的实现思路是:
遍历所有的GC Roots,找到它们的直接引用,并把这些引用标记为可达;然后遍历这些可达对象,继续找到它们的直接引用,并标记为可达;重复以上步骤直到遍历到没有引用。最后,没有标记为可达的对象就是垃圾。(整个走过的路径就称为引用链)
在Java语言中,可作为GC Roots的对象包含下面几种:
为什么要触发STW?
假如在检查某条引用链时,某个其他线程删除了这条引用链上的某个对象,那么从删除位置开始往后的对象都是不可达的,这时可达性分析就可能出现误判。因此必须在进行可达性分析前暂停所有的用户线程。
不仅仅是Java实现了垃圾回收机制,也有很多其他的语言也实现了垃圾回收机制,如python、php等则是使用引用计数算法来判断是否为垃圾的。
引用计数算法的实现思路为:
给对象增加一个计数器,每当有一个对象引用它时,就让计数器加一,每一个引用失效时,就让计数器减一,当计数器为0时,认为对象不再使用。
引用计数算法的思路简单,效率也比可达性分析算法要高,但是Java却不使用,原因就在于引用计数无法解决循环引用的问题。
例如:
public class Test {
public Test n;
public static void main(String[] args) {
Test test1 = new Test();// test1引用次数为1
Test test2 = new Test();// test2引用次数为1
test1.n = test2;// test2引用次数为2
test2.n = test1;// test1引用次数为2
test1 = null;// test1引用次数为1
test2 = null;// test2引用次数为2
}
}
当把test1和test2都置为null后,显然是没有任何对象会使用到这两个引用的,也就是说这两个对象已经是垃圾了,但由于这两个对象互相引用,计数器不为0,就不进行回收。这就是引用计数法的弊端,无法解决循环引用问题。
确定哪些对象是垃圾后,就要执行垃圾回收算法来清除这些垃圾。主要的垃圾回收算法有:
顾名思义,就是先标记再清除。通过前文所述操作对所有用不到的对象标记后,统一将其清除。
标记-清除算法虽然实现思路简单,但是却有着致命缺点:
标记清除后会产生大量不连续的内存碎片,在后续申请内存时,如果剩余内存空间还有1G,但是最大连续的剩余内存空间还不到500MB,那这时去申请500MB的内存空间都申请不下来。
从上图可以看到,虽然剩余空间还有5格,但是要申请一个大小为三格的对象都申请不下来。
为了解决标记-清除算法造成的内存碎片化的问题,引入了复制算法。
复制算法的实现思路为:
首先把内存块分成均匀的两块,每次只使用其中的一块内存,在进行垃圾回收时,把存活的对象复制到另一块内存中,然后再清除这一块内存。这样就解决了内存碎片化的问题。
复制算法的实现也比较简单,但是缺点也很明显:
内存空间利用率很低,每次只能用一半丢一半。另外,假如内存中存活的对象很多,而死亡的很少,这时复制算法效率也是很低的。
标记-整理算法也是对标记-清除算法的一种改进,在标记完不会立即进行清除,而是把所有的存活对象往一端移动,把所有的存活对象和死亡对象隔开以后,在对死亡对象进行清除。
标记-整理算法和复制算法一样,当存活对象很多时,效率会很低。
实际开发中的内存情况是多种多样的,因此不能简单的使用上述的某一种算法来实现垃圾回收。分代算法通过区域的划分,在不同的区域执行不同的垃圾回收算法,从而更好地实现垃圾回收。合适的才是最好的。
分代算法将内存区划分为新生代和老年代两大内存区块,根据经验而谈:如果一种事物已经存在了了很久,那么它很可能会继续存在很久。就像音乐一样,如果某一首歌已经火了好多年了,那么它很可能继续火很多年;当然更多的歌都只是昙花一现。依据这条经验,内存区划分为新生代和老年代,在新生代中,每次垃圾回收都有大批对象死去,只有少量存活(就像经典歌曲往往是少数的),因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用标记-清除或者标记-整理算法。