JVM垃圾回收及双亲委派原则

类的加载过程

加载 链接(验证、准备、解析) 初始化

  1. 加载:就是把class字节码文件加载到内存中,创建一个class对象
    链接:分为三个阶段
  2. 验证阶段:确保被加载的类(.class文件的字节流)满足java虚拟机的规范,不会造成安全错误
  • 对文件格式的验证,是否有不支持的常量类型,是否不符合UTF-8编码,文件中是否有不规范或者附加的其他信息
  • 对元数据的验证:这个类是否有父类,是否继承了不允许被继承的类,是否出现不合理的重载
    对字节码的验证:保证程序语义的合理性,保证类型转换的合理性
  • 对字符引用的验证:校验符号因子中通过全限定名是否能找到对应的类。校验符号引用中的访问性是否可被当前类访问
  1. 准备阶段:负责为类的静态成员分配内存,并设置默认初始值

  2. 解析阶段:将类的二进制数据中的符号引用替换为直接引用。将符号替换为具体的内存地址或者偏移量

  3. 初始化:常量字段复制的过程。只对static修饰的变量或者语句块进行初始化。如果初始化一个类的时候,其父类尚未初始化,也优先初始化父类;如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

垃圾回收

判断对象是否存活

引用计数器算法:给对象中添加一个引用计数器,每当有一个对象引用它时,计数器就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。缺点:很难解决对象间相互引用的问题。

可达性分析算法:通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(对象不可达时),证明此对象不可用。
以下对象会被认为是root对象:

  • 虚拟机栈(栈帧中本地变量表)中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中Native方法引用的对象

对象被判定可被回收,需要经历两个阶段:

  1. 第一阶段是可达性分析,分析该对象是否可达
  2. 第二阶段是当对象没有重写finalize()方法或者finalize()方法已经被调用过,虚拟机认为该对象不可以被救活,因为回收该对象。finalize()方法在垃圾回收中的作用是,给对象一个救活的机会。

垃圾收集算法

  1. 标记-清除算法
  2. 复制算法
  3. 标记-整理算法
  4. 分代收集算法

标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
两个不足:一个效率问题,标记和清除两个过程效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够连续的内存而不得不提前触发另一次GC

复制算法
为了解决效率问题。它将内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区就行内存回收。代价是降内存缩小了原来的一半。现在商业虚拟机都采用这种方法回收新生代。分为Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被浪费。

标记-整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率变低。为了应对所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。标记-整理算法,标记过程依然和标记-清除算法一样,但后续不走不是直接对可回收的对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法
根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代。在新生代,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对它进行担保,就必须采用标记-清理,标记-整理算法进行回收

Minor GC 和 Full GC 有什么不同

  • 新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收速度也比较快。
  • 老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程。MajorGC 的速度一般会比 Minor GC 慢 10 倍以上。

双亲委派原则

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,这里的类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因为所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
双亲委派模型保证了java程序的稳定运作,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法中。先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

    public Class<?> loadClass(String name) 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
                    // 说明父类加载器无法完成加载请求

                if (c == null) {
                    //在父类加载器无法加载的时候
                    // 再调用本身的findClass方法来进行类加载
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // 这是定义类加载器,记录统计数据
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

用户自定义类加载器:

  1. Java.lang.ClassLoader类的子类
  2. 用户可以定制类的加载方式
  3. 父类加载器是系统加载器
  4. 编写步骤:(1)继承ClassLoader,(2)重写findClass方法。从特定位置脚在class文件,得到字节数组,然后利用defineClass把字节数组转化为Class对象。

为什么要自定义类加载器?

  1. 可以从指定位置加载class文件,比如数据库、云端加载class文件
  2. 加密:Java代码可以被轻易的反编译,因为,如果需要对代码进行加密,那么加密以后的代码,就不能使用Java字典的ClassLoader来加载这个类了,需要自定义ClassLoader,对这个进行解密,然后加载。

java虚拟机运行时的数据区

程序计数器
是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器,线程私有。

java虚拟机栈
线程私有的,每次方法执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法执行完成过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。局部变量存放到栈中。各种基本数据类型(boolean,byte,char,short,int,float,double,long、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的指针)。

本地方法栈
与虚拟机栈所发挥的作用相似,区别是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务,线程独享。

java堆
java虚拟机内存最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。所有的对象实例以及数组都在堆上分配。堆是垃圾收集器管理的主要区域,GC堆。现在收集器都采用分代收集算法。java堆细分为:新生代和老年代,再细致一点有Eden空间,From Survivor空间,To Survivor空间。可通过-Xmx和-Xms控制。

方法区
用于存储已被虚拟机加载的类信息,常量,静态变量、即时编译后的代码等数据。别名“非堆”
线程共享的

JMM Java内存模型

  1. Java的并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通讯,多个线程之间是不能通过直接传递数据交互的,它们之间交互只能通过共享变量实现。
  2. 主要目的是定义程序中各个变量的访问规则。
  3. Java内存模型规定所有变量都存储在主内存中,每个线程还有自己的工作内存
  4. 线程的工作内中保存了被该线程使用到的变量的拷贝(从主内存中拷贝过来),线程对变量的操作都必须在工作内存中执行,而不能直接访问主内存中的变量
  5. 不同线程之间无法直接访问对象工作内存的变量,线程间变量值的传递都通过主内存来完成。
  6. 主内存主要对应Java堆中实例数据部分。工作内存对应于虚拟机中部门区域

java内存分配

java内存模型规定所有的变量都是存在主存中,每个线程都有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不是直接再主存中进行。并且每个线程不能访问其他线程的工作内存。原子性:在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

对象优先在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次MinorGC

大对象直接进入老年代。大对象指需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组。

长期存活的对象将进入老年代。虚拟机给每个对象定义一个对象Age计数器,如果对象在Eden出生并经历过一次Minor GC后仍存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,对象在Survivor每熬过一次Minor GC年龄就+1,当它的年龄到达一定程度(默认15岁)就晋升到老年代。晋升老年代阈值,通过参数 -XX:MaxTenuringThreshold设置

动态对象年龄判定。如果Survivor空间相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或者等于该年龄的对象就直接进入老年代,无需达到阈值。

空间动态担保。取之前每一次晋升到老年代对象容量的平均大小作为依据,与老年代剩余空间进行比较,决定是否进行FULL GC来让老年代腾出空间。 HandlePromotionFailure设置开关。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值