JVM分为三个主要的子系统
(1)类加载器子系统(2)运行时数据区(3)执行引擎
1.类加载器子系统
Java的动态类加载功能是由类加载器子系统处理。当它在运行时(不是编译时)首次引用一个类时,它加载、链接并初始化该类文件。
1.1加载
类由此组件加载,遵循委托层次算法加载类文件。
- 启动类加载器:负责从启动类路径中加载类,rt.jar,优先级比较高
- 扩展类加载器:负责加载ext目录(jre/lib)内的类
- 应用程序类加载器:负责加载应用程序级别类路径,涉及到路径的环境变量等
1.2链接
- 校验 – 字节码校验器会校验生成的字节码是否正确,如果校验失败,我们会得到校验错误。
- 准备 – 分配内存并初始化默认值给所有的静态变量。
- 解析 – 所有符号内存引用被方法区(Method Area)的原始引用所替代。
1.3初始化
这是类加载的最后阶段,是所有的静态变量被赋初始值,并且静态块将被执行
1.4什么是类加载机制
Class文件描述的各种信息,都需要加载到虚拟机后才能运行。虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
1.5双亲委派模型
双亲委派模型(Parents Delegation Model)要求除了顶层的启动类加载器外,其余加载器都应当有自己的父类加载器。类加载器之间的父子关系,通过组合关系复用。
工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有到父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去加载。
2.运行时的数据区(Runtime Data Area)
线程共享的数据区是堆和方法区,其他的都是线程私有的数据区
2.1方法区(Method Area)
- 所有类级别的数据将被存储在这里,在class被加载后的一些信息 如常量,静态常量这些被放在这里,在Hotspot里面我们将它称之为永生代。
- 每个JVM只有一个方法区,它是一个共享的资源
2.2 堆区(Heap Area)
- 他是最大的一块区域,用于存放对象实例和数组,是全局共享的,每个JVM只有一个堆区,由于其内存由多个线程共享,所以不是线程安全的
2.3 栈区(Stack Area)
对每个线程会单独创建一个运行时栈。不是共享资源,线程安全。对每个函数呼叫会在栈内存生成一个栈帧(Stack Frame)
-
局部变量数组 – 包含多少个与方法相关的局部变量并且相应的值将被存储在这里。
-
操作数栈 – 如果需要执行任何中间操作,操作数栈作为运行时工作区去执行指令。
-
帧数据 – 方法的所有符号都保存在这里。在任意异常的情况下,catch块的信息将会被保存在帧数据里面。
2.4 PC寄存器
每个线程都有一个单独的PC寄存器来保存当前执行指令的地址,一旦该指令被执行,pc寄存器会被更新至下条指令的地址。
2.5 本地方法栈
本地方法栈保存本地方法信息。对每一个线程,将创建一个单独的本地方法栈。
3.垃圾回收机制
3.1什么是垃圾?
java中的垃圾是指已经分配内存但不再有任何引用的对象。
3.2两种垃圾判断算法
3.2.1引用计数法
a.算法分析
- 当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。
- 当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1)
- 当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。
- 任何引用计数器为0的对象实例可以被当作垃圾收集。
b.优缺点:
- 优点:引用计数器可以很快的执行,对程序需要不被长时间打断的实时环境比较有利;判断效率高
- 缺点:对循环引用的对象无法进行回收
3.2.2可达性分析法
a.算法分析
可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
b.可作为GC ROOTS的对象:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法中JNI引用的对象
3.3四种垃圾回收算法
3.3.1 标记-清除算法
a.算法分析:
- 标记:采用跟集合(GC Roots)进行扫描,对存活的对象进行标记
- 清除:扫描整个空间中未被标记的对象,进行回收
b.缺点:由于直接回收不存活的对象,会造成内存碎片;会停止整个程序运行,递归效率性能低
3.3.2复制算法
a.算法分析:
- 把内存分为两块区域:空闲区域和活动区域,第一还是标记(标记可达对象)
- 标记之后将可达对象复制到空闲区域,将空闲区变成活动区,同时把以前的活动区对象清除掉,编程空闲区
b.缺点:速度快但是耗费空间,若活动区全部是活动对象,这个时候进行交换的话多占用一倍空间也没有什么用处
3.3.3标记-整理算法
a.算法分析:
- 标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。
b.缺点:在标记-清除的基础上还需进行对象的移动,成本相对较高,好处则是不会产生内存碎片。
3.3.4分代收集算法
分代收集算法是大部分JVM的垃圾收集器采用的算法。
根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外(方法区)还有一个代就是永久代(Permanet Generation)。
- 新生代的特点是每次垃圾收集时只有少量对象需要被回收,采用复制算法
- 老年代的特点是每次垃圾回收时都有大量的对象需要被回收,采用标记-整理算法或者标记清理
3.4七个垃圾收集器
3.4.1垃圾收集器
- Serial收集器(复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过
-XX:+UseSerialGC
来强制指定。 - Serial Old收集器(标记-整理算法):老年代单线程收集器,Serial收集器的老年代版本。
- ParNew收集器(复制算法):是Serial收集器的多线程版本。
- Parallel Scavenge收集器(复制算法):新生代收集器,并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用
-XX:+UseParallelGC
来强制指定,用-XX:ParallelGCThreads=4
来指定线程数。 - Parallel Old收集器(标记-整理算法):是Parallel Scavenge收集器的老年代版本
- CMS(Concurrent Mark Sweep)收集器(标记-清理算法)高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。
- G1收集器:(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
3.4.2G1
优点:
- 整体是基于
标记-整理
,局部使用复制
算法,不会产生碎片空间 - 可以独立管理整个堆(使用分代算法)
- 利用多CPU来缩短STW的时间
- 可以预测停顿:G1把整个堆分成多个Region,然后计算每个Region里面的垃圾大小(根据回收所获得的空间大小和回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
与其他比较:
- G1的设计原则是”首先收集尽可能多的垃圾(Garbage First)“,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。
- G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。
- G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,逻辑上的
- G1的收集都是STW停顿的(STW,Stop-The-Word)的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。
3.5.内存分配策略
- 对象优先分配到Eden区(新生代)
- 大对象直接进入老年代
- 年龄大的对象进入老年代(经过15次Minor GC依然存活)
- 动态对象年龄判定,年龄大于等于Survivor空间相同年龄的一半,就进入老年代
- 空间分配担保(Minor GC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果是,GC是确保安全的。)
4.finalize()方法
- 用途:java垃圾回收只是释放那些经过new分配的内存,回收无用对象占据的内存资源。但如果不是new得到的内存区域,就不能使用垃圾回收该内存,所以需要使用finalize()
- 工作原理:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
- 少使用的原因:finalize函数调用时,有可能导致对象复活;finalize函数执行的时间没有保证;坏的finalize会影响gc性能,死锁和线程挂起,还可能导致内存泄漏。
5.内存泄漏
内存泄漏就是存在一些被分配的对象,这些对象有两个特点:1.对象是可达的,2.对象是无用的;这些对象不会被GC回收,但是占用内存
引起的原因:
- 静态集合类:HashMap、Vector等的使用最容易出现内存泄漏,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着
- 当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
- 监听器:在释放对象的时候却没有删除这些监听器
- 各种连接:数据库连接、网络连接、io连接,除非显示的调用了close()方法将其连接关闭,否则不会被GC回收
- 内部类和外部模块的引用
- 单例模式:不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏。
6.java的四种引用
6.1强引用 Strong Reference
Object obj = new Object();
代码中普遍存在的,像上述的引用。只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
6.2软引用 Soft Reference
如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;
如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
好处:
- 软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。
- 使用软引用能防止内存泄露,增强程序的健壮性。
- SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。
6.3弱引用 Weak Reference
只能生存到下一次垃圾收集发生前。弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
6.4虚引用 Phantom Reference
- 不影响对象的生命周期
- 无法通过虚引用来取得一个对象实例
- 为一个对象关联虚引用的唯一目的,就是希望在这个对象被收集器回收时,收到一个系统通知
小结:对象可能不被垃圾回收;垃圾回收只与内存有关
参考:
- https://blog.youkuaiyun.com/aijiudu/article/details/72991993
- https://www.cnblogs.com/1024Community/p/honery.html#%E4%B8%80----%E6%8A%80%E6%9C%AF%E8%83%8C%E6%99%AF%E4%BD%A0%E8%A6%81%E4%BA%86%E8%A7%A3%E5%90%A7
- https://blog.youkuaiyun.com/napoay/article/details/76360006