JVM

161 阅读10分钟

zhuanlan.zhihu.com/p/34426768

类加载

mp.weixin.qq.com/s?__biz=MzI…

类加载是将.class文件加载到JVM内存的过程。.class文件的二进制数据流放入方法区,生成一个对应的java.lang.Class对象,该对象封装了类在方法区的数据结构。

一个类不会在被程序首次使用的时候才去加载,而是会预先加载,但在预先加载的过程发现.class文件损坏并不会抛出异常,而是在首次使用的时候才会抛出异常。

加载方式:
本地文件直接加载;从jar,zip等归档文件中加载;通过网络下载;将源文件动态编译为.class;从专有数据库中提取;

类加载包含了:加载,验证,准备,解析,初始化五个过程。

类加载器:启动类加载器,扩展类加载器,应用程序类加载器

JVM类加载机制:全盘负责,父类委托,缓存机制

类加载方式:JVM启动时加载;Class.forName;ClassLoader.loadClass

双亲委派模型:当一个类加载器收到类加载请求时,首先委派自己的父类加载器,一直向上委派,因此所有的类到最后都会被委托给启动类加载器,当父类加载器在其搜索范围内找不到该类时,子类加载器才会自己尝试加载。

内存结构

堆(线程共享):存放对象实例,几乎所有的对象实例都在这里分配内存(逃逸分析技术),垃圾收集器管理的主要区域,由于现在的收集器基本数都采用分代收集算法,所以java堆可以细分为新生代和老年代,新生代又分为Eden区,From Survivors,To Suivivors

方法区(线程共享):存储虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据;

运行时常量池:是方法区的一部分。Class文件中除了有类的版本、字段、接口等描述信息外,还有一项讲就是常量池,用于存储编译期(源文件生成字节码文件.class的过程)生成的各种字面量和符号引用,这部分内容在类加载之后进入方法区的运行时常量池存储。

栈(线程私有):描述的时Java方法执行的内存模型,每个方法执行的时候都会创建一个栈帧用于存储局部变量表(编译器可知的基本数据类型、对象引用)、操作数栈、动态链接、方法出口等信息。每个方法从调用到结束的过程就对应一个栈帧在虚拟机栈中入栈出栈的过程。

程序计数器(线程私有):记录当前线程所执行字节码的行号,字节码解析器工作时就是依靠改变这个计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复都需要依靠这个计数器来完成,每个线程都有独立的程序计数器,保证线程切换后能恢复到正确的位置,此区域时唯一不会出现OutOfMemory的区域。

垃圾收集

判断对象是否存活:
1、引用计数器:有一个地方引用计数加一,引用失效计数减一,为0可以回收,但是循环引用问题很难解决
2、可达性分析:通过一系列成为GC Roots的对象作为起始点,从这些节点向下搜索,搜索所走过的路径成为引用链,当一个对象不在任何引用链上时,就是GC Roots到这个对象不可达时,证明该对象不可用;可作为GC Roots的对象包括: 虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI(Native方法)引用的对象

finalize:回收对象之前调用该方法。

方法区垃圾回收:废弃的常量(不存在任何的引用)和无用的类(该类所有的实例被回收;加载该类的ClassLoader被回收;该类对应的java.lang.Class对象没有在任何地方被引用,没有在任何地方通过反射访问该类的方法)

垃圾收集算法:

1、标记清除算法:算法包括标记和清除两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
缺点:效率问题,标记和清除两个过程效率都不高;空间问题,清除后产生大量的不连续的内存碎片,空间碎片太多会导致在后边分配大对象时,找不到的足够的连续内存而提前触发另一次垃圾回收

2、复制算法:将内存划分为容量大小相等的两块,每次只使用其中一块,用完一块需要回收时,将存活的对象拷贝到另一块内存空间,再将已使用过的内存空间一次性处理掉。实现简单内存高效,但在使用时每次只能使用一半。
优化处理:新生代98%的对象都是朝生夕死的,不需要按1:1的比例划分内存,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和其中一块Survivor,当回收时,将Eden和Survivor中存活的对象拷贝到另一个Survivor空间中,最后清理Eden和刚才使用的Survivor,默认比例为8:1:1,当Survivor空间不足时,需要依赖其他内存空间(老年代)分配担保,即当Survivor没有足够的空间存储存活对象时,将存活对象放入老年代。

3、标记整理算法:标记对象是否存活,标记完成之后让所有存活的对象向一端移动,然后清理掉端边界意外的内存。 复制算法无法适用于存活率比较高的情况,存活率比较高时会进行较多的复制操作,效率变低,而且不想浪费50%的空间的话需要有额外的空间进行担保,所以老年代不适合使用复制算法。

4、分代收集算法:将内存分为新生代和老年代,新生代每次只有少量对象存活,使用复制算法,老年代存活率高而且没有额外的空间进行分配担保,使用标记清理或者标记整理算法回收。

垃圾收集器:

serial收集器:新生代,使用复制算法,单线程收集器,收集时要暂停其他所有的工作线程,直到它收集结束,运行在Client模式下的虚拟机的默认收集器,简单高效,没有线程交互的消耗

ParNew收集器:新生代,复制算法,Serial的多线程版本

Parallel Scavenge收集器:新生代收集器,复制算法,并行的多线程收集器

Serial Old:老年代,单线程,标记整理算法

Parallel Old收集器:Parallel Scavenge的老年代版本,使用多线程和标记整理算法

CMS收集器:以获取最短回收停顿时间为目标的收集器;标记清除算法,老年代,并发清除。缺点:面向并发设计的程序都对CPU资源比较敏感,在并发阶段,虽然不会导致用户线程停顿,但是会因为占用部分线程而导致应用程序变慢,总吞吐量下降;无法处理浮动垃圾,在并发清理阶段用户线程还在运行,还会产生新的垃圾,这部分垃圾在当次无法回收掉,所以没法像其他收集器那样可以等老年代几乎被填满再回收,需要预留部分空间放置这些浮动垃圾;标记清除后会产生大量的空间碎片,导致后边需要分配大对象时没有足够的连续空间从而再次触发FULL GC,为此CMS提供一个开发,清除完后进行合并整理,内存碎片没有了但会导致停顿时间变长,另外提供一个参数,设置每多少次Full Gc后进行一次压缩整理。

G1收集器:并行与并发,分代收集,空间整合,可预测的停顿,将堆划分为多个大小相等的独立区域(Region),虽然保留新生代和老年代的概念,但是新生代和老年代已经不再是物理隔离的,有计划的避免在整个堆中进行全区域的垃圾回收,而是跟踪各个Region,维护一个优先级队列,优先回收价值最大的region

并行:指多条垃圾收集线程并行工作,但此时用户线程处于等待状态。

并发:指用户线程与垃圾收集线程同时执行(并不一定时并行的,可能会交替执行)

内存分配与回收策略

对象优先在Eden区域分配
新生代GC(Minor GC)发生在新生代的垃圾回收,非常频繁,回收速度也非常快,大多数情况下对象在新生代Eden区分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次
老年代Gc(Major GC/Full GC)发生在老年代的GC,一般要比Minor GC慢10倍以上

大对象直接进入老年代
大对象是指需要大量连续内存空间的对象,通过设置参数可以超过一定大小的对象直接在老年代分配,这样做的目的是避免在Eden区以及两个Suivivor之间发生大量的内存复制。

长期存活的对象进入老年代
为每个对象定义了一个对象年龄(Age)计数器,未进入老年区的对象每熬过一次Minor GC年龄+1,年龄增加到一定程度进入老年代中(默认为15)

动态对象年龄判断
如果在Survivor空间中相同年龄所有对象的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象会直接进入老年区

空间分配担保
发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,可以确保这次Minor GC是安全的,如果不成立,查看参数设置是否允许担保失败,如果允许,继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均值,如果大于,会尝试进行一次Minor GC 尽管有风险,如果小于,或者参数设置不允许担保失败,进行一次Full GC。

JVM工具

jps:虚拟机进程状况工具 jps -lm输出全名和参数

jstat:虚拟机统计信息监控工具

jinfo:java配置信息工具

jmap:java内存映射工具,用于生成堆存储快照

jhat:堆转储快照分析工具

jstack:java堆栈跟踪工具

可视化工具:Jconsole,VisualVM