Jvm

1. 类加载器

java类加载器,java提供了3种,然后我们(程序员)还可以自定义加载器,所以,我觉得应该有4种加载器。

1.1 加载器分类

1.1.1启动类加载器Bootstrap ClassLoader

  1. 启动类加载器,又称引导类加载器,使用C语言实现,嵌套在JVM内部,也就是说启动类加载器是JVM的一部分
  2. 用来加载java的核心库:JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容,用于提供JVM自身需要的类
  3. 既然是C语言实现的内容加载器,所以并不继承java.lang.ClassLoader,没有上层加载器
  4. 加载扩展类和系统类加载器,并指定为他们的父类加载器
  5. 出于安全考虑,启动类加载器值加载包名为java、javax、sun等开头的类

1.1.2 扩展类加载器

1.9之前的版本叫ExtClassLoader,1.9以后为PlatformClassLoader,这里以1.8版本描述,也就是ExtClassLoader.

  1. JAVA语言实现,为sun.misc.Launcher.的内部类,派生于ClassLoader类
  2. 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录jre/lie/ext下加载类库。如果用户创建的jar放在此目录中,也会自动由扩展类加载器加载
  3. 上层加载类是启动类加载器

1.1.3 系统类加载器AppClassLoader

  1. 系统类加载器,又称应用程序类加载器,由java语言实现,同样为sun.misc.Launcher.的内部类,派生于ClassLoader类
  2. 上层加载类为扩展类加载器
  3. 负责加载classpath或系统属性java.class.path指定路径下的类库
  4. 我们写的java类都是这个加载器进行加载的,可以通过ClassLoader.getSystemClassLoader()方法,直接获取

在这里插入图片描述

1.1.4 自定义加载器

1.1.4.1 为什么要自定义加载器

因为系统的ClassLoader只会加载指定目录下的class文件,如果你想加载自己的class文件,那么就可以自定义一个ClassLoader
另外也出于安全考虑,java代码容易被编译和篡改,防止源码泄露,所以我们可以对我们的代码进行加密解密。
也就是我们可以根据自己的需求,通过自定义类加载来实现。

1.1.4.2 如何实现自定义类加载器

下面为类加载的源码:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
           //从内存中查找类,如果有则直接返回
            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) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }

但是findClass(name)方法,ClassLoader并没有实现,而是由其子类URLClassLoader.findClass(name)实现的。

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                            	//defineClass方法主要是把字节数组转化为类的实例,final类型,不可被覆盖,是一个native方法,具体也是由虚拟机实现
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

所以,我们如果要自定义类加载器,就继承ClassLoader,重写findClass方法即可,也不会破坏双亲委派模型(下面有说到)。因为双亲委派机制的体现在loadClass方法中。具体实现方式,请google。。。

1.2 加载器之间的关系

从眼观上看,各个类加载器之间是父子继承关系,但其实他们只是包含关系而已,并非继承,只是因为各个加载器各司其职,作用分工不同,所以在类加载的时候,为了能够正常使用而定制了一个规则,正式因为这个规则才让他们被我们看上去有了类似于继承的关系。这个规则就是类加载的双亲委派机制

1.2.1 什么是双亲委派

如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时,子加载器才会尝试自己去加载。

1.2.2 双亲委派具体工作流程

类加载

  1. 当JVM加载Test.class类的时候,首先会到自定义加载器中查找,看是否已经加载过,如果已经加载过,则返回该类。
  2. 如果自定义加载器没有加载过,则询问上一层加载器(即AppClassLoader)是否已经加载过Test.class。如果加载过,则直接返回,如果没有加载过,则询问上一层加载器(ExtClassLoader)是否已经加载过。如果没有加载过,则继续询问上一层加载(BoopStrap ClassLoader)是否已经加载过。
  3. 如果顶层BoopStrap ClassLoade也r没有加载过,则到自己指定类加载路径下查看是否有Test.class字节码,有则加载并返回加载后的类c = findBootstrapClassOrNull(name)。
  4. 如果还是没找到调用c = findClass(name)到加载器ExtClassLoader指定的类加载路径下查找class文件,有则加载并返回类。
  5. 依此类推,最后到自定义类加载器指定的路径还没有找到Test.class字节码,则抛出异常ClassNotFoundException。

1.2.2 为什么需要双亲委派

因为他们各司其职,采取这种方式的话可以避免,

  1. 重复加载
    例如A.class,在扩展类中加载了,如果不采用这个模式,用户自己也写个A.class,那系统类加载器岂不是又记载了一次
  2. 防止java核心类被篡改
    也正因不会重复加载,所以写程序时,使用的java核心类库是jdk本身的,而不会出现用户写的类库,即java的核心类库不会被篡改

例如A.class,在扩展类中加载了,如果不采用这个模式,用户自己也写个A.class,那系统类加载器岂不是又记载了一次。这样就能防止java核心代码被篡改。
但是双亲委派模型不是一种强制性约束,也就是你不这么做也不会报错怎样的,它是一种JAVA设计者推荐使用类加载器的方式。

1.2.2 破坏双亲委派

在上述“自定义类加载器”的时候,有描述过双亲委派机制的体现是在loadClass中,所以,如果要破坏双亲委派机制就必须得重写loadClass方法

1.2.3 什么时候需要破坏双亲委派

我们知道了类加载机制,在实际的应用中双亲委派解决了java 基础类统一加载的问题,但是却着实存在着一定的缺席。jdk中的已经码好的基础类被作为用户典型的api进行调用,但是也存在被api调用用户的代码的情况,典型的如SPI(Service Provider Interface)结构:调用独立厂商实现部署在应用程序的classpath下的接口提供者(SPI, Service Provider Interface)的代码。

1.2.3.1 SPI介绍

jdk中除了定义大量的基本类及方法外,还通过接口定义了一些规范,没有具体实现,众所周知的JDBC就是个典型的例子,jdk定义了接口规范,供应商提供服务,供应商的库总不能放JDK目录里吧。所以java从1.6搞出了SPI结构,就是为了优雅的解决这类问题—api调用用户提供的代码。那具体是怎么破坏的呢?

1.2.3.2 模型破坏举例(JDBC)
public void test1() throws SQLException {
        Connection connection =
                DriverManager.getConnection("","","");
    }

@CallerSensitive
public static Connection getConnection(String url,
    String user, String password) throws SQLException {
    java.util.Properties info = new java.util.Properties();
    if (user != null) {
        info.put("user", user);
    }
    if (password != null) {
        info.put("password", password);
    }
 
    return (getConnection(url, info, Reflection.getCallerClass()));
}

小插曲,先解释一下**getCallerClass()**方法,这个方法是得到调用者的类。所在的方法必须用@CallerSensitive进行注解,通过此方法获取class时会跳过链路上所有的有@CallerSensitive注解的方法的类,直到遇到第一个未使用该注解的类,避免了用Reflection.getCallerClass(int n) 这个【过时】方法来自己做判断。
Reflection.getCallerClass(int n),0 和小于0 则返回 Reflection类,1-返回自己的类,2-返回调用者的类,层次上传···,所以我们可以推断getCallerClass()不带参数的方法,得到的类就是调用test1所在的类。
在这里插入图片描述
在这里插入图片描述

public class Test
{
    public static void main(String[] args)
    {
        Test2 test=new Test2();
        test.g();
    }
}
 
 
 class Test2
{
    public  void g(){
        gg();
    }
    public  void gg(){
        System.out.println("-1 : "+Reflection.getCallerClass(-1));
        System.out.println("0 : "+Reflection.getCallerClass(0));
        System.out.println("1 : "+Reflection.getCallerClass(1));
        System.out.println("2 : "+Reflection.getCallerClass(2));
        System.out.println("3 : "+Reflection.getCallerClass(3));
        System.out.println("4 : "+Reflection.getCallerClass(4));
        System.out.println("5 : "+Reflection.getCallerClass(5));
    }
 
}

-1 : class sun.reflect.Reflection
0 : class sun.reflect.Reflection
1 : class lee.Test2
2 : class lee.Test
3 : class lee.Test
4 : null
5 : null

回到正题,我们再跟进getConnection方法,有下面一段代码:

    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        // synchronize loading of the correct classloader.
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }

其根本原因就是getContextClassLoader()方法:在设计SPI结构时,就要处理api在调用供应商的代码时,供应商的代码如何被加载的问题。线程上下文类加载器(Thread Context ClassLoader),这个上下文类加载器可以通过Thread.setContextClassLoader()方法进行设置,如果创建线程时没有设置,它将会从父线程中继承一个;如果都没有设置过,那么这个类加载器默认就是系统类加载器。
在这里插入图片描述

2.类的生命周期

在这里插入图片描述

2.1 加载

类加载,什么时候进行加载,规范中并没有进行强制规范,在各个不同的虚拟机具体实现,但是对于初始化状态,虚拟机规定了,在有且仅有这几种情况下,必须进行初始化。
1.用到new、getstatic、putstatic或invokestatic这4条字节码指令时
2.使用reflect包的方法对类进行反射调用的时候
3.初始化一个类的时候,如果父类没有被初始化,则需要先触发其父类的初始化
4.当Java虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类
5. 当使用JDK 1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应类没有初始化时,必须触发其初始化(这点看不懂就算了,这是1.7的新增的动态语言支持,其关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,这是一个比较大点的话题,这里暂且打住)

类加载过程:

  1. 读取class文件中的二进制字节流
  2. 将类中的静态数据的存储结构转化到方法区的运行时数据结构
  3. 在方法区中实例化一个class对象

也就是说在正式使用前,类的信息都被加载到了方法区中

2.2 连接

2.2.1 验证

对字节流中的信息进行验证,是否符合当前虚拟机的要求,是否会对虚拟机产生危害

2.2.2 准备

为类变量(仅包括类变量)在方法区中分配内存,并设置初始值,比如程序员代码int a=4,此时a被赋予了0,因为value复制为4的putsattic指令是程序被编译后,存放于类构造器方法中,所以在初始化的是才会被赋予4。仅包括类变量(被static修饰的变量),不包含实例变量,实例变量将会在对象实例化的时候随着对象一起分配在java堆中。

2.2.3 解析

将常量池内的符号引用替换为直接引用的过程

2.3 初始化

调用new T的时候,也就是执行类构造方法,此时才会执行指令进行初始化操作,是类加载过程的最后一步,在这里开始执行程序员的代码,对变量进行初始化,比如上述的a将被赋予4。至此,才是一个完整的类创建完成,接着,就是在堆中开辟空间,创建实例。
此前,一直在纠结什么时候在堆中开辟空间,后来回头想想也没有意义,就如同什么时候进行类加载,我们只要确定在开始使用前,使用类需要准备的所有工作都已经完成了就行,没必要钻这牛角尖。

然后就是使用,直到卸载(GC回收),这就是一个类完整的生命周期结束。

new一个对象的时候,首先去方法区中,看有没有这个类的信息,如果有就直接拿过来用,如果没有就进行类加载–初始化,
初始化后,根据具体情况对类进行实例化,在堆中开辟空间
在进行new一个对象实例的时候,尽快new多次,该类的初始化操作只在首次加载的时候执行,也就是第一次new的时候,其它几次new不会进行初始化操作。

初始化时,包括3部分,全局变量初始化、代码块初始化、构造函数初始化,执行顺序也是如此,不会因为书写顺序而改变

public class Foo {
    int i = 1;

    Foo() {
        System.out.println(i);             //-----------(1)
        i=3;
        System.out.println(i);
    }

    {
        i = 2;
    }

    protected int getValue() {
        return i;
    }

    public static void main(String[] args) {
        new Bar();
    }
}

output:
2
3

2.4 实例化与初始化的区别

个人理解类的初始化与实例化的区别是:
类的初始化是指类加载过程中的初始化阶段对类变量按照程序猿的意图进行赋值的过程;而类的实例化是指在类完成加载到内存中后创建对象的过程,而类的是实例化依赖于类加载和初始化

3.类的运行时数据区域

类加载完之后,把信息放到哪呢,这一章就说类加载后及使用时的数据区域----运行时数据区域。
还是那种图,你看腻了的图:
在这里插入图片描述

3.1栈区(虚拟机栈)

线程私有,生命周期与线程相同,即线程创建时,创建对应的栈,线程销毁时,亦销毁。每个方法在执行时,都会创建一个栈帧用于存储局部变量表、操作数栈、方法出口等信息。每一个方法从调用至结束的过程,就对应着一个栈帧的入栈、出栈。因为它随着线程的生命周期,所以栈区不是垃圾回收的主要对象。
在这里插入图片描述
局部变量表:存放了编译器可知的各种基本数据类型(boolean、byte等)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向另一个代表对象的句柄或其他次对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,讲抛出StackOverflowError异常
  2. 虚拟机栈可以动态拓展,当扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

3.2本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈执行的Java方法,而本地方法栈则为虚拟机使用到的Native方法。本地方法栈在虚拟机中并没有强制规定,因此有的虚拟机直接将本地方法栈与虚拟机栈合二为一了。
本地方法栈与虚拟栈一样,同样会抛出StackOverflowError、OutOfMemoryError异常

3.3堆

Java堆是虚拟机所管理的内存中最大的一块。被所有线程共享,在虚拟机启动时创建,几乎所有的对象实例都在这里分配,因为随着JIT编译器发展等技术成熟,所有对象分配在堆上也渐渐不是那么“绝对”了,但绝大多数的对象还是被分配在堆上的。因此Java堆是垃圾收集器管理的主要区域。Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样(或者说,像链表一样虽然内存上不一定连续,但逻辑上是连续)。如果在堆中没有内存完成实例分配,而且堆也没办法再扩展时,将会抛出OutOfMemoryError异常

3.4方法区

被所有线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据,垃圾收集在这个区域是比较少出现,但并非数据进入了方法区就成为了永久代。该区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

3.5程序计数器

线程私有,占用较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器,由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器

3.6运行时常量池

是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用户存放存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。比如String s = “af”,af就放在这。
既然是方法区的一部分,自然受到方法区的限制,当无法申请到内存时会抛出OutOfMemoryError异常。

3.7直接内存

直接内存(Direct Memory,又称堆外内存)并不是虚拟机运行时数据去的一部分,也不是Java虚拟机规范中规定的内存区域,但是这块区域被频繁的使用到,而且也会导致OutOfMemoryError异常。
在JDk1.4中加入的NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象作为这块内存地址的引用。这样避免数据在Java对和Native堆中来回复制数据。

堆外内存
DirectByteBuffer对象的数据保存在native heap中,引用保存在HeapBuffer中。另外,这个引用是直接分配在堆得Old区的,因此其回收时机是在FullGC时。因此,需要避免频繁的分配直接内存,这样很容易导致Native Memory溢出。
扩展:为什么要有这个直接内存呢,是因为Java要想与外界通讯,必须要将Java堆中的数据复制到native堆中,而现在你直接将对象开辟在了native堆,也就是直接将数据写在native堆中,因此避免了在操作数据时还要将Java堆中的数据复制到native堆中,因此性能更好。
这块内存的分配不受Java堆大小的限制,但是,既然是内存,肯定还是会受到限制。其大小依赖于操作系统对进程的最大值(对于32位系统就是3~4G,各种系统的实现并不一样)的设置,以及生成的Java字节码大小、创建的线程数量、维持java对象的状态信息大小(用于GC)以及一些第三方的包,比如JDBC驱动使用的native内存。

4.对象的创建及访问定位

上述我们知道了运行区域的作用,那么剩给程序员的就是内存的分配了,那么内存是怎么创建的呢,以及对象是怎么访问的呢?

4.1 对象的创建

虚拟机遇到一条new指令的时候,首先将去检查这个指令涉及的对象是否能在内存中找到,如果找不到则执行类的加载过程,接下来为对象分配内存的时候,这个对象所需内存的大小在类加载完成后便可以完全确定,为对象分配空间就是把一块确定大小的内存从Java堆中划分出来。

  1. 指针碰撞
    如果Java中的内存是规整的,就是所有用过的内存放在一边,空闲的放在一边,中间放着一个指针作为分界点的指示器,那分配内存就是将指针向空闲空间那边挪动一段与对象大小相等的距离。
    在这里插入图片描述
  2. 空闲列表
    如果Java中的内存不是规整的,就是已使用的与未使用的相互交错,那么虚拟机就必须维护一个列表记载那块内存是可用的,分配内存是从列中中找一块足够大的空间划分给对象,并更新列表上的内容。
    在这里插入图片描述
    选择哪种内存分配方式时由Java堆是否规整决定的,而Java堆是否规整又由垃圾收集器是否带有压缩功能决定的,所以可以说是堆内存分配是靠垃圾收集器决定的。

4.1 对象的访问定位

建立对象,自然是为了使用对象,Java程序需要通过栈上的reference数据来操作堆上的具体对象,这是众所周知的
,而reference类型在Java虚拟机的规范中只规定了它是一个指向对象的引用,并没有定义这个引用该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。
目前主流的访问方式有两种,分别是:句柄和直接指针

  1. 句柄
    Java堆中划分出一块内存来当做句柄池,有了句柄,reference存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
    在这里插入图片描述
  2. 直接指针
    如果使用直接指针访问,那么Java 堆中对象的布局就必须考虑如何放置访问类型数据的想关信息,而reference 中存储的直接就是对象的地址。在这里插入图片描述
    hotspot【好像】是使用的直接指针。

上面说了这么多,不能只管往里塞,不吐吧,所以接下来就是Jvm的垃圾回收。

4. GC垃圾收集

说起垃圾收集,大部分人认为这项技术是伴随着Java诞生的,但事实上,GC的历史比Java久远。经过半个多世纪的发展,内存的动态分配和回收技术已经相当成熟,一切看起来都进入了“自动化”时代,那为什么还有学习GC呢,?答案很简单:需要排查各种内存溢出、泄漏问题时,当垃圾收集称为系统达到高并发量的瓶颈时等等,我们就需要对其进行必要的监控和调节。
垃圾回收,听其名,是对垃圾进行回收,所以,我们就要思考垃圾的定义范围、如何处理垃圾、什么时候进行垃圾处理三面去学习

4.1 什么是垃圾

在回收之前,第一件事就是要确定它们是“活着”的还是已经“死去”(即不可能再被任何途径使用的对象)了。

4.1.1 引用计数法

引用计数法的逻辑非常简单,但是存在问题,java并不采用这种方式进行对象存活判断。
引用计数法的逻辑是:在堆中存储对象时,在对象头处维护一个counter计数器,如果一个对象增加了一个引用与之相连,则将counter++。如果一个引用关系失效则counter–。如果一个对象的counter变为0,则说明该对象已经被废弃,不处于存活状态。
逻辑简单,那java为什么没有使用呢,其最主要的原因就是很难解决对象之间相互循环引用的问题。
在这里插入图片描述
然后obj1、obj2的值只是地址,现在我们把obj1=null,obj2=null,那么现在:
在这里插入图片描述
这种情况,更加引用计数法的逻辑,堆中的这两个对象不会被回收。

4.1.2 可达性分析算法

在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相引用,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。

不过在进行真正的垃圾回收之前,程序会再次进行确认一遍,是否都是垃圾。
在这里插入图片描述
在Java语言中,可作为GC Roots的对象包括下面几种:

  1. 虚拟机栈(栈桢中的本地变量表)中的引用的对象
  2. 方法区中的类静态属性引用的对象
  3. 方法区中的常量引用的对象
  4. 本地方法栈中JNI(Native方法)的引用的对象

4.1.3 再谈引用

无论是引用计数法还是可达性分析法,判定对象时都与“引用”有关。前面两种算法对于那些想回收又不想回收的对象显得无能为力,希望能通过程序描述这一类对象:当内存空间足够时,则保留在内存中,如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。jdk在1.2之后,对引用的概念进行了扩充:

  1. 强引用
    就是Object o = new Object()这类的引用,只要强引用还存在,垃圾收集器就不会回收被引用的对象
  2. 软引用
    软引用用来描述一些还有用但并非必须的对象,通过SoftReference类来实现软引用,这些被关联的对象在将要发生内存溢出异常之前被列入回收范围等待第二次回收,如果这次回收还没有足够的内存空间才会抛出内存溢出异常
    SoftReference sr = new SoftReference(prev);
  3. 弱引用
    在垃圾回收器工作时,无论当前内存空间是否够用,都会被回收掉,通过WeakReference类来实现弱引用
  4. 虚引用
    被称为幽灵引用或幻影引用,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
    虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
    我们应用程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动
Object obj = new Object();
ReferenceQueue refQueue = new ReferenceQueue();
PhantomReference<Object> phantomReference = new PhantomReference<Object>(obj,refQueue);

4.1 垃圾收集器

找到了哪些是垃圾,那么要怎么收集它呢?篇幅过大,另起炉灶
垃圾收集器

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值