JVM内存模型
堆(线程共享):是整个JVM中占据空间最大的区域,new的对象,都是在堆区
方法区(线程共享):保存类对象,类加载的时候.class二进制字节码就是加载到内存中的方法区里,
①包括类的基本属性
②方法的具体指令也保存在方法区中,调用方法的时候就是在方法区里读取指令并执行
③静态成员
栈(线程私有):JVM栈和本地方法栈(jdk1.8以后不区分)
JVM栈:java代码的调用栈
本地方法栈:JVM内部的C++代码的调用栈
程序计数器(线程私有):
里面存放的就是一个地址,这个地址保存了程序执行指令,当CPU执行指令的时候球会根据程序计数器里的地址在内存中读取对应的指令,一条指令执行完毕后程序计数器里的值就会更新成下一条指令的地址,始终保存下一条指令的地址
垃圾回收 GC
垃圾回收机制并不能完全回收垃圾,大约可以回收70%
涉及到“内存管理”
1. 什么时候申请内存(时机是明确的)new对象
2.什么时候释放内存(时机是不确定的),什么时候这个对象不再被引用就可以释放了
如果不释放内存,就会出现“内存泄漏”的问题:内存一直在使用,随着时间的推移剩下的内存空间越来越少,如果再想申请新的内存,可能就申请不到了(申请不到内存可能导致程序崩溃)
解决方案:①GC一定程度上解决②例行重启
站在进程的角度来看,如果进程结束,该进程使用的内存也就随之释放了
所以JAVA里就引入了 垃圾回收 机制,目的就是为了能够“自动判定某个对象是否应该回收,若果可以回收,就进行回收
优点:开发人员不必考虑垃圾回收的问题
局限性:①内存的回收没那么及时②消耗一些额外资源③STW问题影想程序性能
GC具体回收什么东西?
栈和程序计数器是不需要GC的:
①栈上的内存是会被自动释放的,随着方法的结束,栈帧就销毁了,里面的内存就会自动释放
②程序计数器保存指令的地址,会自动刷新也不需要释放
主要回收堆和方法区:
申请内存和释放内存都是以“对象”为单位的而不是字节
GC主要做两件事:
①明确“那个对象是垃圾”②明确“如何回收垃圾”
垃圾:某个对象以后不再使用了(对代码没有贡献了)
两种方法:
1. 引用计数(在JAVA中没有用到)
创建一个对象的同时会搭配一个“计数器”,每当有一个引用指向这个对象,计数器+1,有一个亲友被销毁,计数器-1,当计数器为0的时候,这个对象就被认为是垃圾了
优点:引用计数下,内存回收更加及时
局限性:①计数器的数据操作得是“线程安全”的,效率较低 ②可能出现“循环利用”的问题(两个对象互相引用,计数器为2,但对象销毁后计数器为1,不能回收但也访问不到这两个对象了)
2. 可达性分析(JAVA采取的办法)
JAVA里的对象都是通过引用来获取到的,一个引用可以指向一个对象,一个对象里可能还包含若干个引用关系,这样就构成了一个“图状结构”
可达性分析:从GCRoots出发,能够被访问到的对象就是“可达的”,不能访问到的就是“不可达的”
一旦某个对象不可达了,此时这个对象持有的引用指向的对象也就是不可达的了
GCRoot(从哪里开始遍历)
①栈上的局部变量表中的引用
②常量池中的引用指向的对象
③方法区中的静态的引用类型的属性
(这三部分都是代码中直接使用的,所以一定是“可达的”)
JVM中有一个专门的线程,定期的扫描对象之间的引用关系,就可以识别出那些对象已经遍历不到了
JAVA中的四种引用:
①强引用(就是平时用的引用)既可以找到对象,也能决定对象的生死
②软引用:能找到对象,只能一定程度上决定对象的生死(延缓对象被回收的时机)
③弱引用:能找到对象,不能决定对象的生死
④虚引用:不能找到对象,也不能决定对象的生死(某些特殊情况使用,如进行善后工作)
四种垃圾回收算法:
①标记清除 ②复制算法 ③标记整理 ④分代回收
实际上JVM在GC的时候,不是只使用一种算法,而是会结合多种算法使用
1. 标记清除:先标记出垃圾,再清除垃圾
标记的方式就是使用可达性分析,除去可达的对象之后,剩下的就标记成垃圾
清除的含义就是释放对象对应的内存空间
优点:简单,容易实现
局限性:可能会产生很多的内存碎片,导致内存空间不连续,对后续申请空间造成影响
2. 复制算法:内存分成两份,只用其中一份
把不是垃圾的对象,复制到另一半内存中,然后把这一半内存整块回收
优点:很好的解决了内存碎片问题
局限性:①如果留下的对象比较多,就代表复制的占比达,效率比较低
②内存利用率低,只有一半内存可以利用
3. 标记整理:采用类似于“顺序表”删除中间元素的方式,搬运
先标记出垃圾的位置,再让后面的前移覆盖这个位置,再把最后位置的内存释放即可
优点:没有内存碎片问题,空间利用率也高了
局限性:内存搬运操作比较频繁,效率依旧不高
4. 分代回收: 把回收的过程分成几个场景,不同的场景下采取不同的回收方式
分代回收,主要基于一个经验规律:若果一个对象存活时间越久,就认为这个对象会继续更久的存活下去
如何衡量对象的“存活时间”:根据这个对象躲过的GC轮次(JVM进行周期性的可达性分析)
给对象引入了“年龄”,就是躲过的GC的轮次
①新的对象诞生于伊甸区(新对象的内存在伊甸区上分配)
②通过经验规律表明,在伊甸区中的新对象,绝大部分都活不过一轮GC
③成功通过第一轮GC的对象就进入“幸存区”,幸存区里的对象也会经受GC的考验,两个幸存区就会相互配合,使用“复制算法”,通过GC的对象就被复制到另一个幸存区,反复进行这个过程
④在幸存区中经历了重重GC之后,年龄积累到一定程度了,这时对象晋升为“老年代对象”,把这个对象从幸存区拷贝到了老年代里面(有一个特例:如果是一个很大的对象,就不适合在幸存区里不断复制,直接进入老年代)
⑤老年代的对象,被回收的频率就大大降低了
几个相关术语:
垃圾回收器:
CMS收集器:
G1收集器:
类加载
类加载:JVM把.class二进制文件加载到内存中(把程序加载到内存中是程序运行的大前提)
类加载的步骤:
①加载:找到.class文件,解析.class文件的格式,读取到内存中
②链接:类和类之间需要配合(一个类要用到另一个类),就需要把依赖的类也进行加载
③初始化:对类对象进行初始化(初始化静态成员,执行静态代码块)
类加载的结果:得到“类对象”,在代码中可以通过Test.class这样的方式获取到类对象
双亲委派模型/双亲委派模式
目的:为了防止出现用户写的类和标准库的类发生冲突(重名的情况),导致加载出错
负责类加载工作的模块称为“类加载器”
一个JVM工作过程中,会有很多个类加载器一起工作,这些类加载器,默认下有“父子”关系
一个类加载器只有一个父亲
自己写的类加载器,可以遵守这个模型,也可以不遵守
比如Tomcat,加载webapps中的Servlet就是有自己的类加载器,没有遵守双亲委派模型