类加载机制 和垃圾回收机制

类加载机制

Java 分类:
>JVM:虚拟机:用于将程序员写的源代码编译成可执行的class文件。
>JRE:Java的运行环境,可以执行class文件。
>JDK:Java开发的集成环境,给程序员编程用的。
>JVM小于JRE,JRE小于JDK,JKD包含JRE,JRE包括JVM

	随然偏离主题 但个人觉得还是把他们说在一起  好点 做个回忆!

类的加载过程如图,再做具体的介绍。

类加载
加载:加载是类加载的第一个阶段,通过类的全限定名来找到对应的class文件,将此class文件生成一个class对象。

这个过程主要就是类加载器完成。

链接:链接分为3个小部分,验证、准备、解析。

验证:验证的目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。
主要包括四种验证
文件格式验证:基于字节流验证。
元数据验证:基于方法区的存储结构验证
字节码验证:基于方法区的存储结构验证。
符号引用验证:基于方法区的存储结构验证。

准备:给静态方法和静态变量赋予初值,比如static int a;给其中的a赋予初值为0,但是这里不会给final修饰的静态变量赋予初值,因为被final修饰的静态变量在编译期间就已经被赋予初值了;

解析:主要将常量池中的符号引用替换为直接引用的过程。

初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化。

类加载器:

把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成。这样的好处在于,我们可以自行实现类加载器来加载其他格式的类,只要是二进制字节流就行,这就大大增强了加载器灵活性。系统自带的类加载器分为三种:
启动类加载器:它是由C++实现的本,没有父类。地方法,不属于Java类范畴,不能够被直接引用,主要被用于加载java所需的核心jar包,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,属于顶级类加载器。
扩展类加载器
扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,父类加载器为null,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

//ExtClassLoader类中获取路径的代码
private static File[] getExtDirs() {
     //加载<JAVA_HOME>/lib/ext目录中的类库
     String s = System.getProperty("java.ext.dirs");
     File[] dirs;
     if (s != null) {
         StringTokenizer st =
             new StringTokenizer(s, File.pathSeparator);
         int count = st.countTokens();
         dirs = new File[count];
         for (int i = 0; i < count; i++) {
             dirs[i] = new File(st.nextToken());
         }
     } else {
         dirs = new File[0];
     }
     return dirs;
 }

系统类加载器

它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

由Java语言实现,父类加载器为ExtClassLoader。
类加载器加载Class大致要经过如下8个步骤:

1.检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
2.如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
3.请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
4.请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
5.当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
6.从文件中载入Class,成功后跳至第8步。
7.抛出ClassNotFountException异常。
8.返回对应的java.lang.Class对象。

双亲委派机制:(来源:简书https://www.jianshu.com/p/3556a6cca7e5)

双亲委派机制工作过程
双亲委派机制工作过程:

1:双亲委派模式使得类的加载有了层级优先级,通过这种层级的优先级来保证加载过的类不会被重复加载,父类已经加载过的类,子类没有必要再去加载一次。

2:其次是为了安全,比如Bootstrap ClassLoader会加载JVM需要的核心java包,这时候网络上传来了一个名字是java.lang.Integer的类,Bootstrap ClassLoader检测到该类已经被加载过了,所以直接返回Class,而不是重新加载,便可以防止核心API库被随意篡改。可能你会想到自己在classpath路径下自定义一个java.lang.myInteger类,这并不属于java核心包中,父类加载器找不到该类,所以最后交由系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常

java.lang.SecurityException: Prohibited package name: java.lang

loadClass方法:

  protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 先从缓存查找该class对象,找到就不用重新加载
            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
                    // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                    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;
        }
    }

ClassLoader类中的loadClass方法显示出了双亲委派模式,在类进行加载之前首先在缓存中查询是否该类已经被加载过了,如果加载过了则直接返回class对象,如果没有被加载过,则首先查看本类加载器的父类加载器是否存在,如果存在则委托父类加载器进行类的加载,如果父类加载器为null,代表子类加载器是扩展类加载器,而其父类是启动类加载器,使用委托给启动类加载器进行加载。若以上的父类没有加载成功,最后使用自定义的类加载器进行加载。

findClass方法默认抛出ClassNotFoundException,子类可以通过重写findClass方法来调用自定义类加载器。

类与类加载机制

两个class对象是否为同一个类对象存在两个必要条件:

  1. 类的完整类名必须一致,包括包名。
  2. 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

在JVM中,即使这个两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的,这是因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间,所以加载的class对象也会存在不同的类名空间中,但前提是覆写loadclass方法,从前面双亲委派模式对loadClass()方法的源码分析中可以知,在方法第一步会通过Class<?> c = findLoadedClass(name);从缓存查找,类名完整名称相同则不会再次被加载,因此我们必须绕过缓存查询才能重新加载class对象。当然也可直接调用findClass()方法,这样也避免从缓存查找,如下:

public class Test{
	public static void main(String[] args){
	String root="/Users/java";
//创建两个不同的自定义类加载器实例
FileClassLoader loader1 = new FileClassLoader(root);
FileClassLoader loader2 = new FileClassLoader(root);
//通过findClass创建类的Class对象
Class<?> object1=loader1.findClass("com.zejian.classloader.DemoObj");
Class<?> object2=loader2.findClass("com.zejian.classloader.DemoObj");
 
System.out.println("findClass->obj1:"+object1.hashCode());
System.out.println("findClass->obj2:"+object2.hashCode());
}
}

垃圾回收机制

四个问题分别进行讲解
垃圾回收简介
圾回收是如何工作的?
垃圾回收的类别
垃圾回收监视和分析

Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对JVM(Java Virtual Machine)中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,防止出现内存泄露和溢出问题。

Java GC机制主要完成3件事:确定哪些内存需要回收,确定什么时候需要执行GC,如何执行GC。经过这么长时间的发展(事实上,在Java语言出现之前,就有GC机制的存在,如Lisp语言),Java GC机制已经日臻完善,几乎可以自动的为我们做绝大多数的事情。然而,如果我们从事较大型的应用软件开发,曾经出现过内存优化的需求,就必定要研究Java GC机制

启动Java垃圾回收
作为一个自动的过程,程序员不需要在代码中显示地启动垃圾回收过程。System.gc()和Runtime.gc()用来请求JVM启动垃圾回收。
这个请求机制提供给程序员一个启动 GC 过程的机会,但是启动由 JVM负责。JVM可以拒绝这个请求,所以并不保证这些调用都将执行垃圾回收。启动时机的选择由JVM决定,并且取决于堆内存中Eden区是否可用。JVM将这个选择留给了Java规范的实现,不同实现具体使用的算法不尽相同。

毋庸置疑,我们知道垃圾回收过程是不能被强制执行的。我刚刚发现了一个调用System.gc()有意义的场景。通过这篇文章了解一下适合调用System.gc() 这种极端情况。
标记
垃圾回收的第一步是标记。垃圾回收器此时会找出哪些内存在使用中,还有哪些不是。
在这里插入图片描述

上图中,蓝色表示已引用对象,橙色表示未引用对象。垃圾回收器要检查完所有的对象,才能知道哪些有被引用,哪些没。如果系统里所有的对象都要检查,那这一步可能会相当耗时间。
清除

这一步会删掉标记出的未引用对象。
image

内存分配器会保留指向可用内存的引用,以供分配新对象。
压缩
在这里插入图片描述
为了提升性能,删除了未引用对象后,还可以将剩下的已引用对象放在一起(压缩),这样就能更简单快捷地分配新对象了。
image
为什么需要分代垃圾收集?

之前说过,逐一标记和压缩 Java 虚拟机里的所有对象非常低效:分配的对象越多,垃圾回收需时就越久。不过,根据统计,大部分的对象,其实用没多久就不用了。
来看个例子吧。(下图中,竖轴代表已分配的字节,而横轴代表程序运行时间)
![image](https://img-blog.csdnimg.cn/20200129193734676.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RhZWdldW0=,size_16,color_FFFFFF,t_70

上图可见,存活(没被释放)的对象随运行时间越来越少。而图中左侧的那些峰值,也表明了大部分对象其实都挺短命的。
JVM 分代

根据之前的规律,就可以用来提升 JVM 的效率了。方法是,把堆分成几个部分(就是所谓的分代),分别是新生代、老年代,以及永生代。
image

新对象会被分配在新生代内存。一旦新生代内存满了,就会开始对死掉的对象,进行所谓的小型垃圾回收过程。一片新生代内存里,死掉的越多,回收过程就越快;至于那些还活着的对象,此时就会老化,并最终老到进入老年代内存。
Stop the World 事件 —— 小型垃圾回收属于一种叫 “Stop the World” 的事件。在这种事件发生时,所有的程序线程都要暂停,直到事件完成(比如这里就是完成了所有回收工作)为止。
老年代用来保存长时间存活的对象。通常,设置一个阈值,当达到该年龄时,年轻代对象会被移动到老年代。最终老年代也会被回收。这个事件成为 Major GC。
Major GC 也会触发STW(Stop the World)。通常,Major GC会慢很多,因为它涉及到所有存活对象。所以,对于响应性的应用程序,应该尽量避免Major GC。还要注意,Major GC的STW的时长受年老代垃圾回收器类型的影响。
永久代包含JVM用于描述应用程序中类和方法的元数据。永久代是由JVM在运行时根据应用程序使用的类来填充的。此外,Java SE类库和方法也存储在这里。
如果JVM发现某些类不再需要,并且其他类可能需要空间,则这些类可能会被回收。

世代垃圾收集过程
Eden 区:当一个实例被创建了,首先会被存储在堆内存年轻代的 Eden 区中。

注意:如果你不能理解这些词汇,我建议你阅读这篇 垃圾回收介绍 ,这篇教程详细地介绍了内存模型、JVM 架构以及这些术语。

Survivor 区(S0 和 S1):作为年轻代 GC(Minor GC)周期的一部分,存活的对象(仍然被引用的)从 Eden 区被移动到 Survivor 区的 S0 中。类似的,垃圾回收器会扫描 S0 然后将存活的实例移动到 S1 中。(图:JVM中的对象分配和老化过程。)
JVM中的对象分配和老化过程当 eden 空间填满时,会触发轻微的垃圾收集。
在这里插入图片描述引用的对象被移动到第一个 survivor 空间。清除 eden 空间时,将删除未引用的对象。
在这里插入图片描述在下一次Minor GC中,Eden区也会做同样的操作。删除未被引用的对象,并将被引用的对象移动到Survivor区。然而,这里,他们被移动到了第二个Survivor区(S1)。
此外,第一个Survivor区(S0)中,在上一次Minor GC幸存的对象,会增加年龄,并被移动到S1中。待所有幸存对象都被移动到S1后,S0和Eden区都会被清空。注意,Survivor区中有了不同年龄的对象。
在这里插入图片描述在下一次Minor GC中,会重复同样的操作。不过,这一次Survivor区会交换。被引用的对象移动到S0,。幸存的对象增加年龄。Eden区和S1被清空。
在这里插入图片描述此幻灯片演示了 promotion。在较小的GC之后,当老化的物体达到一定的年龄阈值(在该示例中为8)时,它们从年轻一代晋升到老一代。
在这里插入图片描述随着较小的GC持续发生,物体将继续被推广到老一代空间。
在这里插入图片描述所以这几乎涵盖了年轻一代的整个过程。最终,将主要对老一代进行GC,清理并最终压缩该空间。
在这里插入图片描述垃圾回收中实例的终结

在释放一个实例和回收内存空间之前,Java 垃圾回收器会调用实例各自的 finalize() 方法,从而该实例有机会释放所持有的资源。虽然可以保证 finalize() 会在回收内存空间之前被调用,但是没有指定的顺序和时间。多个实例间的顺序是无法被预知,甚至可能会并行发生。程序不应该预先调整实例之间的顺序并使用 finalize() 方法回收资源。

任何在 finalize过程中未被捕获的异常会自动被忽略,然后该实例的 finalize 过程被取消。
JVM 规范中并没有讨论关于弱引用的垃圾回收机制,也没有很明确的要求。具体的实现都由实现方决定。
垃圾回收是由一个守护线程完成的。

对象什么时候符合垃圾回收的条件?

所有实例都没有活动线程访问。
没有被其他任何实例访问的循环引用实例。

内存碎片:一旦实例从堆内存中被删除,其位置就会变空并且可用于未来实例的分配。这些空出的空间将会使整个内存区域碎片化。为了实例的快速分配,需要进行碎片整理。基于垃圾回收器的不同选择,回收的内存区域要么被不停地被整理,要么在一个单独的GC进程中完成。

在编译过程中作为一种优化技术,Java 编译器能选择给实例赋 null 值,从而标记实例为可回收。

class Animal {
    public static void main(String[] args) {
        Animal lion = new Animal();
        System.out.println("Main is completed.");
    }
 
    protected void finalize() {
        System.out.println("Rest in Peace!");
    }
}

上面的类中,lion 对象在实例化行后从未被使用过。因此 Java 编译器作为一种优化措施可以直接在实例化行后赋值lion = null。因此,即使在 SOP 输出之前, finalize 函数也能够打印出 ‘Rest in Peace!’。我们不能证明这确定会发生,因为它依赖JVM的实现方式和运行时使用的内存。然而,我们还能学习到一点:如果编译器看到该实例在未来再也不会被引用,能够选择并提早释放实例空间。

GC Scope 示例程序:

class GCScope {
    GCScope t;
    static int i = 1;
 
    public static void main(String args[]) {
        GCScope t1 = new GCScope();
        GCScope t2 = new GCScope();
        GCScope t3 = new GCScope();
 
        // 没有对象符合GC
        t1.t = t2; // 没有对象符合GC
        t2.t = t3; // 没有对象符合GC
        t3.t = t1; // 没有对象符合GC
 
        t1 = null;
        // 没有对象符合GC (t3.t 仍然有一个到 t1 的引用)
 
        t2 = null;
        // 没有对象符合GC (t3.t.t 仍然有一个到 t2 的引用)
 
        t3 = null;
        // 所有三个对象都符合GC (它们中没有一个拥有引用。
        // 只有各对象的变量 t 还指向了彼此,
        // 形成了一个由对象组成的环形的岛,而没有任何外部的引用。)
    }
 
    protected void finalize() {
        System.out.println("Garbage collected from object" + i);
        i++;
    }
   }

GC OutOfMemoryError 的示例程序

import java.util.LinkedList;
import java.util.List;
 
public class GC {
    public static void main(String[] main) {
        List l = new LinkedList();
        // Enter infinite loop which will add a String to the list: l on each
        // iteration.
        do {
            l.add(new String("Hello, World"));
        } while (true);
    }
}

成为JavaGC专家Part II — 如何监控Java垃圾回收机制, http://www.importnew.com/2057.html
JDK5.0垃圾收集优化之–Don’t Pause,http://calvin.iteye.com/blog/91905
【原】java内存区域理解-初步了解,http://iamzhongyong.iteye.com/blog/1333100
关于施用full gc频繁的分析及解决:http://www.07net01.com/zhishi/383213.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值