JVM(一) GC垃圾回收,类加载器,java生命周期,强引用,软引用,弱引用,虚引用
一、GC垃圾回收
1.1 、判断对象是否“已死”的方法
1.引用计数器算法
对象中添加一个引用计数器,如果引用计数器为0,则表明已死, 如果有地方引用它就加1,引用失效就减1。但是这会导致一个致命的问题,对象循环引用,比如A引用B,B引用A,但是实际上他们俩的存在都没有意义了。
2.可达性分析算法:
这种算法整个对象实例以一个树呈现,根节点是一个称为“GC Root”的对象,从这个对象开始向下搜索并做标记, 遍历完这棵树之后,未被标记的对象就会被认为“已死”,即可被回收的对象。
1.2 、常见GC回收算法
1.标记-清除算法:
如果再被标记之后直接清除会带来另一个问题–内存碎片化,如果下次有比较大的对象需要在堆上分配比较大的内存空间的时候,可能会出现无法找到连续的空间从而导致另一次垃圾回收。
2.复制算法(Java堆中新生代的垃圾回收算法):
首先还是先标记待回收和不用回收的内存,下一步将不用回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域就是连续的,这会损失掉部分内存空间用来复制。这种算法适用于对象存活率比较低的场合,经常用于新生代的垃圾回收。
新生代内存会分为一个较大的Eden区和两个较小的Survivor区(比例8:1:1),当回收时,将Eden区和一个Survivor区的存活对象复制到另一个Survivor区,最后清理掉Eden区和刚才用过的Survivor区。如果几次清理都有对象反复存活,那就将该对象移至老年代。
3.标记-整理算法
首先从根节点开始进行标记算法,然后将所有被标记对象移至内存一端,然后将边界以外的内存直接清理掉,改垃圾回收算法适用于对象存活率高的场合(老年代)。
4.分代收集算法
不同的对象的生命周期是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内不同区域采用不同的回收策略是分代收集算法。
对新生代采用复制算法,对老年代采用标记-清理或者标记-整理算法。
1.3、Minor GC和Full GC 的触发条件
Minor GC触发条件:当Eden区满的时候,触发Minor GC
Full GC触发条件:
(1)调用System.GC()时,系统建议Full GC,但不一定执行
(2)老年代空间不足时
(3)方法区空间不足时
(4)通过Minor GC之后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
二、类加载器
2.1、类加载器
类加载器用来加载java类到JVM中,java虚拟机使用个Java类的方式如下:
java源程序(.java文件)经过javac编译为Java字节码文件(.class文件),java类加载器是
jre的一部分,java类加载器负责动态加载java类到java虚拟机的内存空间里。
类通常是按需加载,即第一次使用该类时才加载。
Java中的类加载器可以分为两类,一类是系统提供的,另一类开发人员可以自己编写。
系统提供的一般有下面几种:
- 引导(Bootstrap)类加载器:不继承自java.lang.Classloader,负责加载核心java库,存储在<JAVA_HOME>/jre/lib目录中、
- 扩展(Extension)类加载器:用来在<JAVA_HOME>/jre/lib/ext或java.ext.dirs指明的目录中加载java的扩展库。Java虚拟机的实现会提供一个扩展库目录,
该类加载器可以在目录里面查找并加载Java类。 - Apps类加载器(也称为系统类加载器):根据java应用程序的类路径(java.class.path或者CLASSPATH环境变量)来加载类,一般来说,java应用的类都是由它来完成加载的。
除了系统提供的类加载器之外,开发人员可以通过继承java.lang.ClassLoader的方式实现自己的类加载器。
2.2、双亲委派模型
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器,每个加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(ClassNotFountException),子加载器才会去尝试加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,查看这个类是否被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 调用父类的加载器加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果父加载器和Bootstrap加载器都没有找到的话
// 调用当前类加载器的findClass方法来完成类加载。
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
2.3、类加载器的工作原理
委托,单一性和可见性。
委托机制:
假如你要加载一个Test.class,首先加载这个类的请求由Apps类加载器委托给它的父类加载器Extension类加载器,然后再委托给Bootstrap类加载器,如果父类加载器都没有找到,再由Apps类加载器查找加载。
可见性机制:
子类加载器可以看到父类加载器加载的类,反之则不行,比如Test.Class类已经被Apps类加载器加载过了,然后想要使用Extension类加载器加载这个类的话,就会抛出ClassNotFoundException异常。
单一性机制:
父类加载过的类不能被子加载器加载第二次。
三、java类的生命周期
指一个class文件从加载到卸载的过程。一个java类完整的生命周期会经过加载、连接、初始化、使用和卸载五个过程。
类加载过程包括加载、连接、初始化几个过程,最终形成JVM可以直接使用的java类型的全过程。
- 1.加载 : 查找并加载类的二进制数据
- 2.连接:
验证:主要目的就是确保被加载类的正确性。比如class文件必须以0xCAFEBABE开头,另外版本号也要做验证,比如用java1.8编译后的class文件要在java1.6上运行,因为版本问题就不会通过。
准备:准备过程就是分配内存,给类的一些字段设置初始值。比如 public int num = 1;在准备阶段num的值就会被设置为0,到后面初始化阶段才会赋值为1。但是对于static final(常量),在准备阶段就会被赋值。比如public static final int num= 1;在准备阶段num的值就是1。
解析:将符号引用替换为直接引用(类所在的内存地址)。 - 3.初始化:主要包括执行类构造方法,static变量赋值语句,static语句块。如果一个子类要初始化,必先初始化其父类。
四、强引用,软引用,弱引用,虚引用
4.1、强引用
Object o = new Object();
强引用的特点:
- 1、可以直接访问目标对象
- 2.、引用指向的对象在任何时候都不会被系统回收,JVM宁愿抛出OOM异常,也不会回收强引用所指向的对象
- 3、强引用可能导致内存泄露,为了避免内存泄露,在使用完之后可以把对象设置为null,如果是集合的话可以使用list.clear()
4.2、软引用
如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足时,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str); // 软引用
软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用所引用对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);
当软引用中的内存不足时,JVM首先将软引用中的对象设置为null,然后通知垃圾回收器进行回收。
if(JVM内存不足) {
// 将软引用中的对象引用置为null
str = null;
// 通知垃圾回收器进行回收
System.gc();
}
也就是说,垃圾回收线程会在抛出OutOfMemory之前回收软引用对象,而且虚拟机会尽可能优先回收长时间不用的软引用对象,对那些刚构建的软对象虚拟机会尽可能保留,
这就是ReferenceQueue的原因。
4.3、弱引用
弱引用与软引用的区别在于,弱引用具有更短暂的生命周期。在垃圾回收器进行扫描的时候,一旦发现了只有弱引用的对象,不管当前空间足够与否,都会回收它的内存。
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
str = null;
JVM会首先将软引用中的对象设置为null,然后通知垃圾回收器进行回收。
str = null;
System.gc();
弱引用也可以和一个引用队列(ReferenceQueue)相关联。
4.4、虚引用
虚引用并不会决定对象的生命周期,如果一个对象只具有虚引用,那么它就和没有任何引用一样,在任何时候都可以被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
创建虚引用,必须要与引用队列(ReferenceQueue)相关联。
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);