JVM与GC

一.jvm概念

jvm(java virtual machine)即java虚拟机,工作在操作系统之上。java编译器只面向jvm,java源代码通过java编译器编译成jvm能理解的代码或字节码文件,然后通过jvm编翻译成各种机器的机器码,这就是java的跨平台和可移植性核心所在。

二.jvm的组成部分

Jvm由五大模块组成:类加载器子系统、运行时数据区(jvm内存模型)、执行引擎、本地方法接口和垃圾回收模块。 

java代码运行的完整流程: 

1.编译:java源代码通过java编译器(javac.exe)编译成jvm所能理解的字节码文件.class文件;

2.加载:编译好的字节码.class文件通过类加载器classLoader加载器加载进内存;

3.解释:加载进内存后由执行引擎Execution Engine在调用本地库接口的配合下负责解释为相应操作系统的机器码;

4.运行:在获取运行所需的数据后,程序在运行时数据区Runtime Date Area开始真正的运行。

三. 类加载器子系统

1.概念

class文件加载进jvm通过类加载classLoader实现,类加载器也是一个类,作用就是加载class文件进内存。

2.加载方式

  • 隐式加载:代码中new出来的类对象程序会自动调用对应的类加载器进行加载
  • 显式加载:代码中通过class.forname()等方法显式加载指定类

3.类加载器ClassLoader种类

  • Bootstrap ClassLoader(启动类加载器):加载java核心类库,C++编写实现,涉及到jvm实现细节,无法直接引用使用,加载位于/JAVA_HOME/lib/下的jar文件;
  • Extension ClassLoader(扩展类加载器):加载java扩展类库,java编写实现,能直接引用使用,加载位于/JAVA_HOME/lib/ext/下的jar文件;
  • Application ClassLoader(系统类加载器):加载用户所编写的类,java编写实现,加载位与/classPath/下的jar文件;
  • UserDefined ClassLoader(用户自定义加载器):用户根据业务需求,通过继承classLoader自定义编写的类加载器来实现定制化加载,加载位置自定义。

4.加载思想------懒加载

类加载器加载类基于懒加载思想,即不用到的时候不加载这个类,用到了再去加载。每个加载器加载类的时候,首先第一步都是查询此类是否加载过,加载过则直接返回结果;没加载过再进行加载

5.加载原理------双亲委派机制

 

当某个类加载器收到加载类的请求时,首先查询自己是否有加载过此类,有则直接返回,没有的话它不会自己去加载这个类,而是把这个类的加载任务递交给上一层的类加载器的,上一级依旧首先查询自己是否有加载过此类,有则直接返回,没有的话继续将这个加载任务递交给上一层类加载器,以此类推,最后直到到达顶层类加载器Boostrap ClassLoader,顶层加载即依旧首先查询自己是否有加载过此类,有则直接返回,没有的话顶层加载器开始尝试自己去加载此类,即在自己的加载路径去寻找此类,找到了则将此类加载进内存,找不到则无法加载。当顶层类加载器也无法加载此类的时候,就会将加载任务下放下一层类加载器,此时下层类加载器都纷纷尝试加载此类,在各自的加载路径搜索此类,找到了则加载进内存,找不到则继续将加载任务下放到下一层类加载器,以此类推,最后直到到达最底层的类加载器,当最底层的类加载器也无法加载此类的时候,则会抛出ClassNotFoundException。

双亲委托加载机制的优点

  • 防止类的重复加载:懒加载思想
  • 避免核心类被替换和篡改:当传播过来一个核心类时,该类的加载任务会通过双亲委托机制递交到启动类加载器,但由于启动类加载器发现已加过此类,就不会再去加载这个类了,因此保证了安全

6.类的加载流程

  • 加载:由双亲委托机制选出的加载器把class文件加载进内存;
  • 连接

       (1)检查:检查导入的class文件数据和格式的正确性

       (2)准备:为类的的静态变量分配空间并初始化默认值(如static int num = 3,准备阶段num初始化为0)

       (3)解析:将符号引用转为直接引用

  • 初始化:第一次使用到某个类的时候才会初始化,没用到不会初始化(上面的num初始化后才赋值为3)

7.loadClass()与class.forName的区别

loadClass()获得的对象只完成了类加载过程的加载部分,其余都还未进行;class.forName()获得的对象已经初始化完成了。

//上图是百度的,有个错误,顶层加载器bootClassLoader它写成了ExtClassLoader,暂时不想改。。。 

四.运行时数据区(jvm内存模型)

1.jvm内存模型

  • 程序计数器(Program Counter Register):线程私有,可看做当前线程所执行的字节码的行号指示器。字节码解释器通过改变程序计数器的值来选取下一条所要执行的字节码指令,实现代码的流程控制;多线程情况下,程序计数器用于记录当前线程执行的位置,用于线程切换回来时找到位置继续执行,这片内存区域不会出现OutOfMemoryError的情况;
  • 虚拟机栈(Java Virtual Machine Stacks):线程私有,为每个线程所执行的方法的内存模型。每个方法的执行都会在虚拟机栈中创建一个栈帧Stack Frame,栈帧中存储着局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法的调用到执行结束都对应了一个栈帧在虚拟机栈的入栈与出栈的过程,这片内存区域在一定条件下会抛出两种错误:StackOverflowError(当前线程请求的栈深度超过jvm所允许的最大深度),OutOfMemoryError(当可动态扩展的虚拟机在扩展时无法申请到足够的内存);
  • 本地方法栈(Native Method Stack):功能与虚拟机栈一致,区别是虚拟机栈为jvm执行java方法服务,而本地方法栈是为jvm使用到的本地方法服务;
  • 堆(Heap):线程共享,jvm中内存最大的一块区域,jdk1.7+还存放了常量池。大部分对象的创建与保存都是在堆上完成的。在堆上保存的对象实例,实际只保存了对象的属性值,属性类型和对象自身的类型标记等,而对象的方法是存储在虚拟机栈上的。在完成对象实例在堆上的分配后,虚拟机栈会保存该实例对象在堆中的的地址。这片内存区域由于涉及到对象的分配与销毁,一定条件下会出现OutOfMemoryError,也是垃圾回收器GC的主要内存区域;
  • 方法区(Method Area):线程共享,存放静态变量,JIT编译后的代码,符号引用等,jdk1.7之前包含运行时常量池,jdk1.7就把常量池移除了,而是在堆中开辟了空间存放常量池,一定条件下会出现OutOfMemoryError,运行时常量池是方法区的一部分,这片内存区域垃圾回收效果差,因此几乎不进行GC。

2.堆Head的内存划分

堆上的内存主要分为年轻代(1/3),年老代(2/3)

3.直接内存

直接内存不是jvm所管辖的区域,可看做jvm以外的机器内存。比如你的电脑有4G内存,java虚拟机占了1G,那么直接内存大小就是3G。JDK中有一种基于通道(Channel)和缓冲区 (Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。 由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

4.jvm常见配置参数

  • -Xms:初始堆的大小
  • -Xmx:最大堆的大小,超出了会报OOM(OutOfMemory)
  • -Xmn:年轻代的大小
  • -Xss:栈的大小,决定了线程进行函数调用的深度,超出后会报StackOverflowError
  • -XX:NewSize:设置年轻代的大小
  • -XX:MaxNewSize:年轻代的最大值
  • -XX:PermSize:设置持久代的初始大小
  • -XX:MaxPermSize:设置持久代的最大值
  • -XX:NewRatio:年轻代与年老代大小的比值
  • -XX:SurvivorRatio:Eden区与Survivor区大小的比值
  • -XX:MaxTenuringThreshold:设置垃圾的最大年龄,即对象在两个Survivor区来回复制的次数

PS:可以通过Java自带的可视化软件Jconsole来查看jvm内存情况(cmd键入jconsole)

五.执行引擎

这一块主要是描述执行引擎是如何让class字节码如何在jvm运行起来.

六.本地方法接口

所谓的本地方法就是方法的实现不是有Java代码来实现的,可能是C或C++。通过提供本地方法接口来实现本地方法调用。

七.垃圾搜集模块

java与其他语言(如C,C++)不同的一大特点就是在于其具有垃圾回收机制,在我们创建对象的时候我们不必特别在意对象的销毁与内存的释放,jvm自带的垃圾回收机制能自动帮我们进行垃圾处理,gc过程主要发生在堆上。

1.堆的内存区域

堆上的内存主要分为年轻代(1/3),年老代(2/3) 

年轻代又分为Eden区(占8/10),两个Survivor区(From Survivor区(占1/10)和To Survivor区(占1/10))

2.GC详细过程

年轻代GC:在堆中,每一个新创建的对象都在年轻代的Eden区完成,到Eden区满了的时候,触发MinorGC,将Eden区中存活的对象复制到From Survivor区,并清空Eden区,重复此过程,当From Survivor区也满了的时候,则将Eden区中存活的对象与From Survivor区中的对象复制To Survivor区,并清空Eden区与From Survivor区;此时From Survivor区变成了To Survivor区,To Survivor区变成了From Survivor区;每次MinorGC时总是将Eden区存活的对象复制到From Survivor区;两个Survivor区不停切换,直到切换次数达15次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制),Survivor区中存活的对象会被复制到年老代,然后清空两个Survivor区。年轻代上的GC也叫YoungGC。(两个Survivor区总是有一个是空的,以配合其它区域GC,minor:较小的,major:成年的)

年老代GC:年老代比年轻代有更大的内存空间,能存放更多或更大的对象,因此当创建的对象较大时(长字符串或数组),同时Eden区的空间空间不足时,大对象的分配会在年老代上进行,由于大对象可能触发提前GC,所以应尽量避免使用短命的大对象。在年老代上的GC过程称为MajorGC,MajorGC通常伴随着最少一次MinorGC(因为一般当MinorGC往年老代复制对象而年老代空间不足时就会触发MajorGC)。

永久代:由于HotSpot(jvm)设计团队把分代搜集延伸至方法区,所以用永久代实现了方法区,但在jdk1.8移除了永久代,改为元空间。元空间存储类的元信息,使用的是本地内存,设置好相应参数可以很好避免OOM的情况。

FullGC:同时对年轻代,年老代,永久代进行垃圾回收叫FullGC。

FullGC触发条件:

             (1)当年轻代晋升到⽼年代的对象⼤⼩,并⽐⽬前⽼年代剩余的空间⼤⼩还要⼤时,会触发Full GC;

             (2)当⽼年代的空间使⽤率超过某阈值时,会触发Full GC;

             (3)当元空间不⾜时(JDK1.7永久代不足),也会触发Full GC;

             (4)当调⽤System.gc()也会安排⼀次Full GC。

3.GC算法

GC的过程主要包含对象的标记与清除两个步骤,具体算法如下:

  • 标记算法

       引用计数法:即每个对象创建时绑定一个计数器,每次被引用时+1,被删除引用时-1,当计数器为0时,标记为垃圾对象。缺点:无法解决相互引用的问题

       GC Roots法(可达性分析法):以一系列“GC Roots”为起点,向下搜索,所走过的路径叫引用链,当一个对象到GC root没有任何引用链时,标记为垃圾对象。缺点:实现复杂,需要分析大量数据,效率相对较低

       可作为GC Roots的有:

            (1)虚拟机栈(栈帧中的本地变量表)中引用的对象;

            (2)方法区中的静态成员引用的对象;

            (3)方法区中的常量引用的对象;

            (4)本地方法栈中JNI(一般说的Native方法)引用的对象

  • 清除算法

       标记-清除算法:直接将标记的对象清除,是最基础的GC算法

       标记-复制算法:将内存分为大小相等的两块,每次只使用其中一块,当其中一块满了的时候复制存活的对象到另一块内存,清除满了的那块内存

       标记-整理算法:将未被标记的存活对象向内存一端移动,并更新对象引用,清除边界以外的内存

4. 分代搜集算法

分代搜集算法:按对象生命周期把堆内存空间分为各种代空间,根据不同空间选择不同垃圾回收算法,叫做分代搜集算法。由于引用计数法的局限性,主流的jvm标记算法用的都是GC Roots(可达性分析法)

       年轻代:标记-复制算法

       年老代:标记-整理算法

5.GC搜集器

年轻代搜集器:

  • Serial串行收集器:标记-复制算法,单线程,进行工作时需要暂停其它工作线程,简单高效,客户端模式下默认的年轻代收集器
  • ParNew搜集器:标记-复制算法,可看做Serial搜集器的多线程版本,多核下比Serial有更好表现
  • Parallel Scavenge并行清除收集器:标记-复制算法,多线程,关注点在系统吞吐量,服务端式下默认的年轻代收集器

年老代搜集器:

  • Serial Old收集器:使用标记-整理算法,单线程,进行工作时需要暂停其它工作线程,客户端模式下默认的年老代收集器
  • Parallel Old搜集器:标记-整理算法,多线程,关注点在系统吞吐量
  • CMS(Concurrent Mark Sweep)并发标记扫描搜集器:标记-清除算法,关注“最短停顿时间”。工作流程主要分为:(1)初始标记(2)并发标记(3)重新标记(4)并发清除

G1(Garbage-First)搜集器:使用标记-整理算法,多线程,作用于整个堆区。工作流程主要分为:

(1)初始标记(2)并发标记(3)最终标记(4)筛选回收

八.其它常见知识点 

1.父类与子类初始化问题

/**
 * @Description
 * @Author YMJ
 * @DateTime 2020-07-26 16:24
 * @Version V1.0.0
 */
public class ParentAndSonInitializeProblem {
    public static class Parent{
        static String staticParentFiled = showStaticParentFiled();
        String filed = showParentFiled();

        Parent(){
            System.out.println("父类构造函数");
        }

        static {
            System.out.println("父类静态代码块");
        }

        {
            System.out.println("父类非静态代码块");
        }

        private static String showStaticParentFiled(){
            String str = "父类静态成员变量";
            System.out.println(str);
            return str;
        }
        private static String showParentFiled(){
            String str = "父类成员变量";
            System.out.println(str);
            return str;
        }

    }

    public static class Son extends Parent{
        Son(){
            System.out.println("子类构造函数");
        }

        static {
            System.out.println("子类静态代码块");
        }

        {
            System.out.println("子类非静态代码块");
        }

        private static String showStaticSonFiled(){
            String str = "子类静态成员变量";
            System.out.println(str);
            return str;
        }
        private static String showSonFiled(){
            String str = "子类静态成员变量";
            System.out.println(str);
            return str;
        }

        static String staticSonFiled = showStaticSonFiled();
        String filed = showSonFiled();
    }

    public static void main(String[] args) {
        Son son = new Son();
    }
}

输出结果: 

父类静态成员变量
父类静态代码块
子类静态代码块
子类静态成员变量
父类成员变量
父类非静态代码块
父类构造函数
子类非静态代码块
子类静态成员变量
子类构造函数

 总结:在父类与子类的初始化过程中,执行顺序如下:

  • 父类的静态部分,包括父类的静态成员变量和静态代码块,两者按先后顺序执行
  • 子类的静态部分,包括子类的静态成员变量和静态代码块,两者按先后顺序执行
  • 父类的非静态部分,包括父类的成员变量和非静态代码块,两者按先后顺序执行
  • 父类的构造函数
  • 子类的非静态部分,包括子类的成员变量和非静态代码块,两者按先后顺序执行
  • 子类的构造函数

2.内存溢出与内存泄漏

内存溢出(OutOfMemory):系统不能够再为程序分配所需的内存空间;

内存泄漏(MemoryLeak):无法释放已经申请得到的内存空间。

PS:因为内存泄漏加之有可能使用者把地址弄丢了导致自己也不能访问,系统也无法将这块内存分配给其它程序使用,由于找不到这块内存的任何信息,垃圾收集器也无法对这块内存进行回收;所以,能分配的内存空间少了一部分,这就是内存泄漏,少数几次内存泄漏并不会影响程序运行,但经过每一次的内存泄漏导致的内存叠加起来,就会使得系统无法在位程序分配所需的内存空间,而发生内存溢出

 

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值