JVM

java内存模型

在这里插入图片描述

JVM由类的加载子系统,执行引擎,运行时数据区,本地库接口组成。

  • 类的加载子系统:将硬盘上的字节码文件加载到内存中,存放在
    运行时数据区的方法区
  • 执行引擎:执行class文件中的指令
  • 运行时数据区:平常所说java内存,包括程序计数器,java虚拟机栈,本地方法栈,堆,方法区
  • 本地库接口:与其它编程语言交互的接口

执行过程:java源程序先被编译成.class文件存储在硬盘上,类的加载子系统去硬盘上将.class文件加载到内存中,加载到运行时数据区的方法区,现在还只是字节码文件,不能直接交给机器去执行,就需要执行引擎去解释编译,翻译成机器能看懂的机器语言,在这过程中就会用到其它编程语言的本地库接口。

运行时数据区

每个线程都有各自的程序计数器,java虚拟机栈,本地方法栈;所有线程共享堆和方法区。

  • 程序计数器:存放下一条要运行的指令
  • java虚拟机栈:存储局部变量表,操作数栈,动态链接,方法出口等一些信息
  • 本地方法栈:为native方法服务,存储native修饰的方法
  • 堆:内存中最大的一块,几乎所有对象的创建都是在堆内存上分配的
  • 方法区:存储类的相关信息,常量和静态变量,即时编译后的代码等

堆和栈的区别

  • 堆是内存中最大的一块,几乎所有对象的创建都是在堆内存分配的;栈存储的是局部变量表,操作数栈,动态链接,方法出口等一些信息
  • 堆内存的分配是运行时才确定的,是动态的,大小不固定;栈分配的内存在编译时就已经确定了,是固定的。
  • 堆对所有线程都是共享的,每个线程都有自己的栈

类加载的时机

  • 创建某个类的实例
  • 访问静态属性
  • 调用静态方法
  • 初始化某个类的子类,其父类也会被初始化
  • 反射
  • jvm启动时被表明是启动类的类,直接用java.exe来运行主类(包含main函数的类)

类加载过程

在这里插入图片描述

  • 加载:通过类的全路径名获取定义该类的二进制字节流,将字节流所代表的的静态存储结构转化为方法区的运行时数据结构,然后在内存中生成一个代表该类的java.lang.class对象,也就是在堆中,作为方法区该类的各种数据的访问入口
  • 验证:确定.class文件的字节流包含的信息符合当前虚拟机的要求,而且确定不会危害虚拟机
  • 准备:开始为类对象分配内存,并设置初始值
  • 解析:将符号引用替换为直接引用。符号引用跟内存布局无关,符号引用目标并不一定存储在内存中;直接引用跟内存布局有关,直接引用目标一定已经被加载到内存中
  • 初始化:实际上就是执行类构造器方法(clinit())的过程。先将所有的静态变量和静态代码块按照它们出现的顺序收集起来,静态代码块只能访问定义在它前面的变量,定义在它后面的变量只能赋值不能访问;虚拟机会保证在子类的clinit方法执行前先执行父类的clinit方法;如果没有那就不用收集;接口中的变量只有在使用时才会被初始化。

类的加载器

在这里插入图片描述

  • BootStrap ClassLoader:启动类加载器,负责加载JVM基础核心类库(<JAVA_HOME>/jre/lib/rt),是用c++实现的。
  • Extension ClassLoader:扩展类加载器,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
  • Application ClassLoader:应用类加载器,用来加载classPath指定的类库

双亲委派机制

当某个特定的类收到类加载的请求时,并不会直接去加载,而是先将加载任务交给自己的父类加载器,父类加载器也是这样,如果父类能成功加载就直接返回,如果不能父类不能加载,再由自己去尝试加载。

双亲委派模型中的代码逻辑很清晰:先查看是否加载过该类,如果没有加载过就交给父类,如果父类加载器为空,就让BootStrap 去加载,如果父类加载失败抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
// 父类加载器
private final ClassLoader parent;

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 {
                // 父类加载器加载,如果父类加载器为空,使用最顶级的Bootstrap类加载器来加载
                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.
                // 如果以上步骤还没找到类,就按照自己定义的类加载器中重写的findClass方法加载
                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;
    }
}

重写loadClass():不遵循双亲委派机制,按照自己的方式加载
重写findClass():遵循双亲委派机制,只有父类加载器不能加载的时候才按照自己的方式加载

双亲委派机制的好处

  • 类的加载代码也是java类,类的加载器也需要加载,所以必须要有一个非java类,也就是BootStrap,是由c++实现的;
  • 虽然是父子类加载器,但并不是继承关系,而是组合关系
  • 在一定程度上具备了优先级的层次关系,越是基础的类越是由上层加载器加载,像java自带的基础的类肯定是由上层加载的,自己编写的就在最下层;
  • 保证了java体系的稳定性,如果用户也写了一个Object类,不同的类加载器加载各自的,当用的时候,那到底该用哪个,肯定会出问题。

对象的创建

当new对象的时候,首先看是否已经加载过该类,如果没有加载过该类,需要先进行类的加载;类的加载通过后,需要给对象分配内存,分配内存有两种方法:指针碰撞和空闲列表,如果内存绝对规整的话就用指针碰撞,否则就采用空闲列表;由于内存的分配非常的频繁,需要考虑线程安全,有两种方式来确保线程安全:本地线程分配缓冲和CAS同步处理;内存分配完后,就初始化内存,设置一些初始信息,然后调用初始化方法。

  • 指针碰撞:用过的内存全在一端,空闲的内存全在另一端,一个指针在两者的分界线上,另一个指针向空闲的一方移动,一直移动到内存等于要分配的内存大小。
  • 空闲列表:用一张表来记录哪些内存是空闲的,当需要分配内存时,就选出一块足够大的内存,然后更新表的记录。
  • CAS同步:CAS同步+失败重试来确保操作的原子性。
  • 本地线程分配缓冲:把内存的分配划分到不同线程中,先在堆中给每一个线程分配一块内存,也就是本地线程分配缓冲,当某个线程需要分配内存时,先在自己的本地线程分配缓冲中分配内存,如果内存不够需要重新分配时,才会同步加锁。

java中的内存泄漏

内存泄漏是指已经不被使用的对象或变量仍然占着内存。理论上来说,java有GC垃圾回收机制,已经不被使用的对象就会被回收,但即使这样也还是会有内存泄漏问题。原因是:长生命周期对象持有短生命周期对象的引用,即使短生命周期已经不被使用了,但由于长生命周期对象还持有该对象的引用,就导致GC无法回收。

为什么会有GC机制

  • 安全性
  • 减少内存泄漏
  • 降低程序员的工作量,不需要程序员手动释放内存。

哪些内存需要GC

程序计数器,java虚拟机栈,本地方法栈都是跟线程绑定在一起的,随着线程的创建而创建,随着线程的销毁而销毁,比如栈中的一个栈帧分配多大的内存在确定类结构的时候就已经确定了,内存的分配与回收都是固定的,所以不需要考虑内存回收问题;而堆和方法区的内存分配都是在运行期动态分配的,是不固定的,所以GC需要关注这部分内存。

java中的GC在什么时候回收

  • 当JVM确定某个对象没有引用指向它的时候,就会调用该对象的finalize方法,类似于生命周期临终的方法,调用该方法后,该对象就会被回收。
  • 对于方法区中的常量和类,如果一个常量没有对象引用的时候就会被回收,如果一个类是无用类就会被回收。
  • 无用类:该类的所有实例都已经被回收;加载该类的类加载器也已经被回收;该类的java.lang.class对象没有被引用

如何判断没有引用指向对象

  • 引用计数算法:每增加一个引用就加1,每减少一个引用就减1,但是无法处理循环引用的情况,比如A引用B,B引用A,这两者互相引用无法被回收,就会造成内存泄漏。
  • 可达性分析算法:有一系列的GC roots,从这些roots开始向下探索,走过的路径称为引用链,如果引用链可达就不能被回收,如果引用链不可达就说明可以被回收。java中就是采用的可达性分析算法。

引用类型

  • 强引用:只要有强引用,该对象就不会被回收。
  • 软引用:如果GC一次,内存还不够,就会再GC一次,这次GC就会把软引用的对象给回收掉
  • 弱引用:在第一次GC的时候就会把若引用对象回收掉
  • 虚引用:并不会改变对象的生命周期,也无法通过虚引用获取对象实例,虚引用只是在GC时会收到一个通知

垃圾回收算法

  • 标记—清除算法:老年代回收算法,最基本的垃圾回收算法。先标记要清除的对象,然后再清除标记的对象,标记和清除都比较耗时,而且会产生大量的不连续空间碎片,导致再来大对象时没有足够的连续空间,又会触发另一次的GC
  • 复制算法:新生代收集算法。将内存分成两块,一块留空,一块使用,当发生GC时,将使用的一块中的存活对象复制到留空的一块中,再把剩下的对象都回收掉,这两块内存来回互换着使用。只使用一半,利用率不高。
  • 标记—整理算法:老年代垃圾回收算法。跟标记—清除算法很类似,只是标记完后,将存活对象移到一端,从边界开始清除所有标记对象。
  • 分代收集算法:将内存分为新生代和老年代,由于新生代的存活较低老年代的存活率较高,所以新生代采用复制算法,老年代采用标记—清除/整理算法,新生代中Eden:S0:S1=8:1:1,这样就降低了“浪费”。

内存分配与回收策略

  • 创建对象时,优先在Eden区分配内存,如果Eden区内存不够时,就会触发Minor GC
  • 大对象直接进入老年代
  • 长期存活对象进入老年代:新生区的对象每熬过一次Minor GC,年龄就会加1,如果对象的年龄超过年龄阈值(默认为15)就会进入老年代,JVM会动态设置年龄,如果某个年龄的对象加起来超过Survivor区的一半,那大于等于这个年龄的对象就都会进入老年代
  • 空间分配担保机制:Minor GC后如果新生区内存还是不够,还存活的对象就会通过内存担保机制进入到老年代,可能会触发Major GC:不允许空间分配担保;允许担保但是老年代最大连续空间小于历次晋升到老年代的对象的平均大小等。

垃圾收集器

新生代收集器:Serial,ParNew,Parallel Scavenge
老年代收集器:Serial Old,Parallel Old,CMS
全堆收集器:G1

  • Serial:新生代收集器,复制算法,串行收集器。client模式下默认的新生代收集器,没有线程的交互,专心的做垃圾收集可以取得很高的效率。
  • ParNew:新生代收集器,复制算法,并行收集器。和CMS搭配适合于用户体验优先的情况下。
  • Parallel Scavenge:新生代收集器,复制算法,并行收集器。吞吐量可以调节,所以适用于吞吐量优先;有自适应调节策略,当这个参数打开后,JVM会自动监控性能,会自动设置一些参数,比如:新生代内存的大小,新生代进入老年代的年龄阈值,新生代Eden,S0,S1区的比例等。
  • Serial Old:老年代收集器,标记—整理算法,串行收集器。给client模式下的虚拟机使用,在server模式下可以和Parallel Scavenge搭配使用,还作为CMS发生Concurrent Modile Failure时的后备方案。
  • Parallel Old:老年代收集器,标记—整理算法,并行收集器。和Parallel Savengue用在吞吐量优先中。

CMS收集器

特点:

  • :高并发,低停顿,标记—清除算法

过程:

  • 初始标记:标记GC roots能直接关联的对象,虽然会暂停用户线程,但是速度很快,时间很短
  • 并发标记:进行 GC roots Trancing的过程
  • 重新标记:修正在并发标记过程中由于用户线程持续运作导致已经被标记的对象的标记发生改变,会暂停用户线程,相比于并发标记时间还是很短
  • 并发清除:并发清除对象
    尽管1,3阶段会暂停用户线程,但速度非常快,时间非常短,最耗时的2,4阶段中回收线程跟用户线程并发执行的,所以整体上可以看做是并发GC。

缺陷:

  • CMS会占用CPU资源,导致CPU分出精力去处理垃圾回收线程,使用户线程的执行速度下降。

  • 无法处理浮动垃圾:最后清除的阶段是并发清除的,在清除过程中,用户线程会不断的产生新垃圾,都是没有被标记的,这就导致无法被回收,只能在下一次的垃圾回收中收集,所以不能在内存快要满的时候在进行垃圾收集,每次都要预留出空间。

  • CMS在回收期间,如果内存不能满足程序的需要,就会发生Concurrent Modile Failure ,就会用Serial Old来进行垃圾收集,这样CMS本来降低停顿时间的目的没有达成,相比于直接用Serial Old还增加了前几个阶段的停顿,得不偿失。

  • 使用标记清除算法本身的缺陷:会产生大量的空间碎片,导致来大对象时,本来有很多内存,但是没有足够大的连续内存而不得不再次触发GC。

G1收集器

  • 全堆收集器,用户体验优先
  • 适用于Heap Memory很大的情况下,把堆内存分成很多个Region块,每个Region块都是随机分配的,既可能是Eden区,Survivor区,还有可能是老年区,除此之外,还多了一个H区,用去存放大对象,当某个对象超过Region块的一半时就会被放到H区
  • G1在回收Region块期间,一般不会暂停用户线程,是基于Most garbage优先回收,整体上来看是基于标记—整理算法,局部上来看是两个Region之间的复制算法
  • G1在清除实例所占用的内存后,还会进行内存压缩。
  • 年轻代使用的是复制算法,将Eden区和Survivor区还存活的对象复制到新的Survivor区

老年代收集分4步,跟CMS步骤差不多,略有不同

  • 初始标记:标记GC roots能直接关联的对象,但这阶段并不会特意暂停用户线程去 进行标记,而是在Minor GC的时候一并把这阶段的事情给做了。
  • 并发标记:进行 GC roots Trancing 的过程,同时还会统计每个Region块内的存活率,如果老年代Region中的存活率很低或者基本上没有存活的,那就会直接回收掉,不用等到后续,这也是G1名字的由来。
  • 最终标记:修正由于并发标记阶段用户线程持续运作导致已经被标记的对象的标记发生改变的对象,但是跟CMS使用的算法不一样,速度更快
  • 筛选回收:回收存活率低的Region块,这个阶段也是跟Minor GC一同发生的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值