类的加载
类的加载是指将类的class文件中的二进制数据读入到内存中,并将其放在运行时数据区的方法区内,然后在堆中创建一个class对象,这个class对象就是类加载的最终产品。
类加载过程
加载-》校验-》准备-》解析-》初始化-》使用-》卸载
- 加载,通过IO读入字节码文件;
- 执行校验、准备、解析步骤;
- 初始化,对类的静态变量初始化为指定的值,以及执行静态代码块;
- 使用;
- 卸载;
类加载器种类
- 启动类加载器:加载支撑JVM运行的位于JRE的lib目录下的核心类库。
- 拓展类加载器:加载支撑JVM运行的位于JRE的lib目录下的 xt拓展目录中的JAR包。
- 应用程序类加载器:加载ClassPath路径下的类包,主要就是加载你自己写的那些类。
- 自定义加载器:加载用户自定义路径下的类包。
java虚拟机运行时内存区域划分
按照线程是否私有划分:
私有区域:
- 程序计数器:记录字节码执行的行号,也是所有内存区域汇总唯一不会产生内存溢出的地方。
- java虚拟机栈:当Java中的方法被执行时,会形成栈帧放入到栈内存当中。栈帧又细分:局部变量表、操作数栈、操作出口等一系列内存区域。
- 本地方法栈:和java虚拟机栈相似,调用的是native方法。
共有区域:
- 堆内存:分配所有对象实例的地方,垃圾回收工作的主要区域。
- 元数据区(方法区):记录了所有虚拟机加载的类信息、常量以及静态变量。
例外:
严格意义上来说,还有块直接内存,他不属于JVM的内存区域,属于堆外内存,java中的NIO(非阻塞IO)会直接操作这块内存,提升读写数据的效率。
JVM内存分配与回收策略
众所周知,new出来的对象都放在堆内存。
堆内存分为:年轻代(新生代)、老年代。
年轻代分为:Eden区、Survivor区。
Survivor区分为:From区、To区。
1、对象优先在Eden区分配
大多数情况下,对象在年轻代的Eden区分配,当Eden区没有足够的空间进行分配时,虚拟机将发生一次minor GC。
Minor GC与Major GC区别:
- Minor GC / Young GC:指发生在新生代的垃圾收集动作,Minor GC非常的频繁,并且回收速度一般也比较快。
- Major GC / Full GC:一般回收老年代,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC慢很多倍。
2、大对象直接进入老年代
大对象就是需要大量连续内存空间的对象,比如字符串、数组等。
大对象直接进入老年代的目的:为了避免大对象分配内存时的复制操作而降低效率。
JVM可以通过参数设置大对象的大小:MaxTenuringThreshold 。如果对象超过设置值的大小,就会直接进入老年代,不会直接进入年轻代。这个参数只在 Serial 和 parNew 两个设计器下有效。
3、长期存活的对象将进入老年代
虚拟机给每个对象一个年龄计数器,如果对象在Eden区出生,经过第一次 minor GC 后能够存活,并且能被 survivor 容纳的话,将被移动到 survivor 空间中,并且对象年龄设为1。
对象在 survivor 中每熬过一次 minor GC ,年龄就会增加一岁,当他的年龄增加到一定程度,默认是到15岁就会被晋升到老年代。
4、Minor GC后存活的对象Survivor区放不下
如果对象经过第一次 minor GC 后能够存活,但是 survivor 空间放不下的话,会把存活的对象部分挪到老年代,部分放入 survivor 区。
5、Eden与Survivor区默认比例8:1:1
大量的对象被分配在Eden区,Eden区满了会触发 minor GC ,可能99%的对象都会成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区。
因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让Eden区尽量的大,survivor区够用就行。
6、对象动态年龄判断
主要目的是为了避免MaxTenuringThreshold 设置过大导致大量对象无法晋升。
具体计算方式,年龄从小到大对象(年龄1~年龄N)的占据空间累加起来的空间,大于survivor区域的一半,就会让年龄N和年龄N以上的对象进入老年代。
7、老年代空间分配担保机制
- 年轻代每次 Minor GC前,都会计算老年代剩余可用空间。
- 如果这个可用空间小于年轻代现有的所有对象大小之和(包括垃圾对象),就会看是否设置HandlePromotionFailure 这个参数,如果是Java1.8则默认开启。
- 如果有这个参数,则会判断老年代的可用内存大小是否大于之前每次 Minor GC后进入老年代的对象平均大小。
- 如果没有设置HandlePromotionFailure 参数或者 3 中为小于,则会触发一次Full GC。
如果Full GC后依旧没有足够的空间存放新的对象,就会发生OOM。
Java虚拟机如何判断对象是否存活
1、引用计数法
当对象引用计数器为0时,即无其他对象引用,判定为死亡,可被回收,但是存在循环引用问题,导致对象一直存活。
2、可达性分析法
从GC Root根开始向下搜索,直接或间接可达的对象,即存活对象。
可以作为GC Root的对象:
- 虚拟机栈引用对象
- 本地方法栈引用对象
- 静态属性引用对象
- 常量应用对象
对象的引用分类
- 强引用:程序代码中普遍存在的,类似“Object obj = new Object()”这类的引用,主要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用:SoftReference实现,内存溢出之前回收。
- 弱引用:weakReference实现,下一次垃圾回收时回收。
- 虚引用:PhantomReference实现,最弱的一种引用关系,正如他的名字,完全形同虚设的一种引用,唯一的目的就是希望在这个对象被回收时收到一个系统通知。
垃圾收集算法
标记清除算法
首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。
缺点:
- 执行会随着对象数量增长而下降。
- 内存空间碎片化问题。
复制算法
为了解决标记清除算法在面对大量可回收对象时执行效率低的问题,就设计出了复制算法。它将可用内存划分为两块,每次只使用其中的一块,当这一块用完了就将存活的对象复制到另一块上,再把已使用的内存空间一次性清理掉。
缺点:
- 只能使用一半内存,造成空间浪费问题。
- 如果存活对象较多,复制操作的开销较大,所以不适合老年代。
- 需要停顿用户线程。
标记整理算法
由于复制算法会浪费一半的空间使用,并且老年代大部分对象都是不会被回收,会造成很大的复制开销,所以老年代一般不会选复制算法。
针对老年代对象的特征,提出了标记-整理算法,其中的标记过程与“标记-清除”算法的标记过程是相同的,但后续步骤不是直接将可回收对象回收,而是将存活对象都向内存空间另一端移动,然后直接清理掉边界以外的内存。
分代收集算法
当前的商业虚拟机的垃圾收集都是采用分代收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记-整理算法来进行回收
垃圾收集器
如果说垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
Serial收集器
- 新生代:复制算法
- 老年代:标记整理算法
单线程的收集器,单线程一方面意味着它只会使用一个cpu或者一条线程去完成垃圾收集工作,另一方面也意味着它进行垃圾收集时必须暂停其他线程的所有工作(stop the word),直到他结束为止。
- 优点:简单高效,高效相对于收集效率来说,因为其没有线程交互的开销。
Serial Old收集器
serial收集器的老年代版本。
两个用途:
- jdk1.5以及以前版本中与Parallel Scavenge收集器搭配使用
- CMS收集器的后备方案
ParNew收集器
serial收集器的多线程版本。
ParNew收集器默认的收集线程数跟cpu核数相同(可以通过参数修改)。
ParNew收集器是许多运行在server模式下的虚拟机的首要选择。
除了serial收集器,只有它能与CMS收集器配合工作。
Parallel Scavenge收集器
Parallel Scavenge收集器关注吞吐量(高效率的利用cpu)。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
该收集器提供了很多参数,供用户选择最合适的停顿时间或最大吞吐量。
在优化比较困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,吧内存管理的调优任务交给虚拟机去完成,是个很不错的选择。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。
CMS收集器
全称:Concurrent Mark Sweep,从名称可以看出是一种标记清除算法。
CMS收集器以获取最短回收停顿时间为目标,非常符合在注重用户体验的应用上使用。
收集过程:
- 初始标记:暂停其他所有线程,标记GCRoots能直接关联到的对象(时间很短)
- 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象;跟踪记录发生引用更新的地方。(这个闭包结构不能包含当前所有的可达对象,因为用户线程会不停的更新引用域,所以gc线程无法保证可达性分析的实时性,所以需要跟踪记录发生引用更新的地方。占用整个GC百分之七八十的时间)
- 重新标记:修正并发标记期间因用户程序继续运作而导致产生变动的那一部分对象的标记记录(时间比初始标记阶段稍长)
- 并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫
优缺点:
优点:并发收集、低停顿
缺点:
- 对cpu资源敏感,会和服务器抢资源。
- 无法处理浮动垃圾,在执行并发清理步骤时,用户线程也会同时产生一部分可回收对象,但是这部分可回收对象只能在下次执行清理是才会被回收。如果在清理过程中预留给用户线程的内存不足,便会切换到SerialOld收集方式。
- 使用标记清除算法,会有大量空间碎片产生。
- 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况。
G1收集器
全称:Garbage-First。
一款面对服务器的垃圾收集器,主要针对配备多颗处理器以及大容量内存的机器。用来高概率满足GC停顿时间要求的同事,还具备高吞吐量性能特征。
特性:
- 将堆划分成多个大小相等的独立区域(region),jvm最多可以有2048个region,这么做的目的是在进行收集时不必在全堆范围内进行。
- 一般region大小等于堆大小除以2048。
- 保留了年轻代和老年代的概念,但是不再是物理隔阂了,他们都是(可以不连续的)region的集合。
- 默认年轻代堆内存的占比是5%。
- region的区域功能可能会动态变化。
G1对大对象的处理:
G1有专门分配大对象的region教humongous区。一个对象超过了一个region大小的50%,就被成为大对象。
收集过程:
- 初始标记:暂停其他所有线程,标记GCRoots能直接关联到的对象。
- 并发标记:用一个闭包结构去记录可达对象;跟踪记录发生引用更新的地方。
- 最终标记:修正并发标记期间因用户程序继续运作而导致产生变动的那一部分对象的标记记录。
- 筛选回收:首先对各个region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划,这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
优缺点:
优点:
- 并行与并发:G1充分发挥多核性能,使用多CPU来缩短Stop-The-world的时间。
- 分代收集:G1能够自己管理不同分代内已创建对象和新对象的收集。
- 空间整合:G1从整体上来看是基于标记整理算法实现,从局部(相关的两块Region)上来看是基于复制算法实现,这两种算法都不会产生内存空间碎片。
- 可预测的停顿:它可以自定义停顿时间模型,可以指定一段时间内消耗在垃圾回收商的时间不大于预期设定值。