jvm垃圾回收之类加载机制

类的生命周期:

image-20210513100523250

  1. 加载:.class文件(二进制数据)——>读取到内存——>数据放进方法区——>堆中创建对应Class对象——>并提供访问方法区的接口

  2. 验证、准备、解析:

    1. 验证主要是用来检查class文件格式是否正确

    2. 准备阶段:为类变量分配内存

      public static int test = 100; //准备阶段,test=0; public int test2 = 100;

      public static final int test3 = 100; //准备阶段,test3=100

    3. 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,一般不关注。

  3. 初始化:到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。

    Java程序对类的使用方式可分为两种:主动使用与被动使用。一般来说只有当对类的首次主动使用的时候才会导致类的初始化,所以主动使用又叫做类加载过程中“初始化”开始的时机。

    类的主动使用包括以下六种:

    1. 创建类的实例,也就是new的方式
    2. 访问某个类或接口的静态变量,或者对该静态变量赋值(被final修饰除外,会放入常量池)
    3. 调用类的静态方法
    4. 反射(如 Class.forName(“java.lang.String”))
    5. 初始化某个类的子类,则其父类也会被初始化
    6. Java虚拟机启动时被标明为启动类的类,还有就是Main方法的类会首先被初始化

    最后注意一点对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块)。

类加载器

image-20210513105737975

双亲委派模型:

image-20210513105910728

双亲委派机制:

  1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成;
  2. 当 ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成;
  3. 如果 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class), 会使用 ExtClassLoader来尝试加载;
  4. 若ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常 ClassNotFoundException。

类层次关系

1

自定义一个类加载器:

@Data
public class MyClassloader extends ClassLoader {
    private String root;
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /**
     * 获取类的二进制数据
     *
     * @param className
     * @return
     */
    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) throws Exception {

        MyClassloader classLoader = new MyClassloader();
        classLoader.setRoot("C:\\Users\\Administrator\\Desktop\\javhl\\app-starter\\target\\classes");

        Class<?> testClass = null;
//            testClass = classLoader.loadClass("com.javhl.course.clazzloader.FatherSonClassTest");
        testClass = classLoader.findClass("com.javhl.course.clazzloader.FatherSonClassTest");
        Object object = testClass.newInstance();
        System.out.println(object.getClass().getClassLoader());

    }
}

JDK8之后的内存区域

image-20210513112648104

内存结构图

image-20210513112823529

元空间(Metaspace):

  1. 从JDK8开始,永久代(PermGen)的概念被废弃掉了,取而代之的是一个称为Metaspace的存储空间。Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。当然你也可以通过以下的几个参数对Metaspace进行控制:
  2. -XX:MetaspaceSize=N:这个参数是初始化的Metaspace大小,该值越大触发Metaspace GC的时机就越晚。随着GC的到来,虚拟机会根据实际情况调控Metaspace的大小,可能增加上线也可能降低。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用java - XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:Metaspacesize为21810376B(大约20.8M)。
  3. -XX:MaxMetaspaceSize=N:这个参数用于限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。
  4. -XX:MinMetaspaceFreeRatio=N:当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增长Metaspace的大小。在本机该参数的默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。
  5. -XX:MaxMetasaceFreeRatio=N:当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。在本机该参数的默认值为70,也就是70%。

哪部分内存区域需要回收?

  1. 程序计数器、虚拟机栈、本地方法栈是线程私有的,随线程而生;随线程而灭。栈中的栈帧随着方法的进入和退出执行出栈和入栈操作,每一个栈帧分配多少内存基本上是在类结构确定下来时就已经是已知的; 因此,这几个区域的分配和回收都具备确定性。方法结束或者线程结束时,内存自然就跟着回收了。
  2. 而Java堆和方法区则不一样,一个接口中多个实现类需要的内存可能不一样,一个方法中多个分支需要的内存也可能不一样,只有在程序运行期间才知道创建哪些对象,这部分内存的分配和回收都是动态的。垃圾收集器关注的是这部分内存。

引用计数法(无法解决循环引用问题)

image-20210513113431013

可达性分析算法

image-20210513113817654

Java引用类型:

  1. 强引用。Java中默认声明的就是强引用,比如:只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了

    Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收obj = null; //手动置null
    
  2. 软引用( java.lang.ref.SoftReference )。软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。

  3. 弱引用(java.lang.ref.WeakReference )。弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。如:JDK中的ThreadLocalMap的Key就是弱引用。

  4. 虚引用( java.lang.ref.PhantomReference)。虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收。虚引用必须和引用队列(ReferenceQueue)一起使用。当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

posted @ 2022-02-21 22:16  隐风  阅读(58)  评论(0)    收藏  举报