一、jvm内存模型
1、内存结构
五大部分:堆、虚拟机栈、本地方法栈、方法区、程序计数器
执行引擎、本地接口
(1)程序计数器(Program Counter Register)
-
当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,
-
每个线程都有一个独立的程序计数器,程序计数器线程私有。
-
如果线程执行 Java 方法,则记录正在执行的虚拟机字节码指令的地址;执行Native 方法(C++代码)时,计数器值为空Undefined。
-
程序计数器不会发生内存溢出(
OutOfMemoryError
即OOM
)问题。 -
是一个非常小的内存空间,几乎可以忽略不计
(2)栈(jvm
)
JVM
中的栈包括 虚拟机栈和本地方法栈,两者的区别就是,Java 虚拟机栈为 JVM
执行 Java 方法服务,本地方法栈则为 JVM
使用到的 Native 方法(java
调用非java
代码的接口)服务。两者作用是极其相似的
堆中对象的一个方法对应一个栈帧。每个栈帧包括局部变量、操作数栈、方法返回地址、动态链接
- 栈是用完了就弹出,所以不产生垃圾
- 栈是私有的
- 放局部变量
- 动态链接里面放 方法 对应的 它在方法区里面解析后入口的地址,通过这个找到方法的具体代码,可以理解为方法索引
- 方法出口:返回调用自己的方法的那一行,不至于又从第一行代码开始执行
(3)方法区
- 用于存储类信息。
- 被所有线程共享
- 方法区是一个特殊的堆,
JVM
调优99%的情况都是对方法区和堆进行调优,而绝大部分都是对堆调优 - 所有字段、方法字节码、特殊方法(构造函数)、接口代码在此定义。所有定义的方法的信息都保存在该区域
- 静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是,和方法区无关static final, Class,常量池
(4)堆内存
- 是最大的一块内存。用于存储对象实例。
- 实例变量存在堆内存中
- 堆内存又细分为新生代、老年代。
- 年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为
8:1:1
①年轻代步入老年代过程
新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
- 15次存活进入老年代
- 大对象直接进入老年代
- 对象动态年龄判断:Eden过来的对象超过了幸存者区的1/2,直接进入老年代。
②Survivor存在的意义?
翻译:幸存者
意义:减少对象直接进入老年代的机会,减少Full GC的频率,从而尽可能减少GC停顿的发生;
③为什么需要划分两个Survivor出来?
由于新生代采用复制算法决定的。因为新生代一般采用复制算法来回收内存,每次内存回收总是需要另一块空的区域(s0或s1,s0、s1轮流被使用)来存放GC存活下来的对象。
二、类加载器
类是模板,对象是具体的
1、加载类
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
先在应用程序加载器找,再去扩展类加载器找,层层往上,当返回NULL的时候有两种情况:
- 不存在
- Java程序获取不到(c++写的程序Java当然找不到)
2、双亲委派机制
目的: 为了保证安全
查找顺序(从右至左开始找):
APP
(应用程序加载器)->EXC
(扩展类加载器)->BOOT
(根加载器,最终执行)
若Boot里面没有这个类->EXC找,EXC里面也没有这个类->APP里面找
3、沙箱安全机制
类似于linux
的权限保护
4、全盘负责委托机制
当个 ClassLoader
加载一个类的时候,除非显示的使用另一个 Classloader
,该类所依赖和引用的类也由这个 ClassLoader
载入
5、分配担保机制
就是大于新生代50%的对象直接进入老年代
6、Native
调用C++方法
三、GC
回收算法
1、引用计数法
给对象添加一个引用计数器,每当有一个地方引用,计数器就加1,当引用失效,计数器就减1。
为0就清理掉
这个方法实现简单、效率高,但是目前主流的虚拟机中没有选择这个算法来管理内存,因为如果两个对象互相引用对方,导致它们的引用计数器都不为0,就无法回收
2、复制算法
复制算法保证to永远是空的(谁空谁是To)
清理GC
的时候将Eden
里面存活的对象放入幸存者To
区,然后将from
区的对象也放入To
区,这时候To区就变成了From区,原来的From就变成了To区(原来的对象复制到了To区,自己就没有对象了,谁空谁是To)
好处
没有内存的碎片
坏处
浪费了内存空间:to永远是空。假设对象100%存活(极端情况)复制算法
最佳使用场景
对象存活度较低的时候;新生区;
3、标记清除算法
优点
不需要额外的空间
缺点
两次扫描,严重浪费时间,会产生内存碎片(那些缺口不好补)。
4、标记压缩算法
压缩实际上就是移动
先标记清除,再移动
5、可行性分析算法
通过一系列的称为GC Roots
的对象作为起点,从这些节点开始向下搜索,节点所走过的路当一个对象到==GC Roots
没有任何引用链相连的话==,则证明此对象时不可用的。
GC Roots
根节点:
类加载器、Thread、虚拟机栈的局部变量表、static成员、常量引用、本地方法栈的变量等等
四、+GC收集器
0、GC
收集器三指标:
- 占用的内存(Capacity)
- 延迟(Latency)
- 吞吐量(Throughput)
发展历程:
Serial
->Parallel
->CMS
->G1
目前
1、Serial收集器
- Serial 收集器是最基本、发展历史最悠久的收集器,
jkd1.3
之前的唯一收集器 - 单线程
- 重点:在它垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束
2、ParNew
收集器
-
新生代收集器,
-
CMS
默认搭配, -
默认开启的线程数等于
cpu
数。 -
Serial
的多线程版本。 -
==并行==收集:多条垃圾收集器同时工作,用户线程等待。
-
GC
复制算法。
3、吞吐量优先收集器
-
又称:
Prallel Scavenge
收集器 -
新生代收集器
-
JDK8
默认parallel Scavenge收集器 -
==并行==收集**:多条垃圾收集器同时工作,**用户线程等待。
-
GC
复制算法 -
特点:以吞吐量优先。
- 吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间)
-
高吞吐量(垃圾收集时间小)可以最高效率利用CPU时间,适合在后台运算,而不是太多交互的任务
-
(其不适合需要与用户交互的程序,良好的响应速度能提升用户的体验,此种场景
CMS
效果更好)。
4、Serial Old收集器
- 老年代收集器
- 单线程
- 标记清理算法
- 用途:
jdk1.5
及以前与Prallel Scavenge
搭配使用- 作为
CMS
备用方案 - 用于Client模式(客户机模式)
5、Parallel Old收集器
JDK1.6
出现- 老年代收集器
- 多线程;
- 标记清理算法
- 用途
- 代替
Serial Old
收集器; - Server模式,多CPU 的情况下
- 在注重吞吐量时采用**
Prallel Scavenge
**和它组合使用
- 代替
6、CMS
收集器
-
全称:Concurrent Mark Sweep 并发低停顿收集器
-
==并发==收集:用户线程与垃圾收集器线程同时工作
-
最低停顿时间【也就是指Stop The World的时间】为目的
- Stop The World:暂停所有线程
-
标记清理算法
-
缺点:
CMS
收集器对CPU资源非常敏感- 无法处理浮动垃圾(运行过程中产生的垃圾)
- 大量空间碎片产生
-
整个收集过程
- :【初始标记】只标记没有跟
GC ROOT
相连的对象 -->STW
- :【并发标记】
CMS
收集器一边收集一边标记 - :【并发预先清除】
- :【并发可能失败的预先清除】
- :【最终重新标记】将所有垃圾进行标记,确保万无一失 -->
STW
- :【并发清除】用户线程可以进行,同时清理垃圾
- :【并发重置】重新回到第一步
- :【初始标记】只标记没有跟
7、G1
收集器
- server模式( 面向服务器)
- 满足小停顿和大吞吐
- 官方推荐
- 引入分区的思路
- 标记清除算法
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
G1
执行过程
将内存平分成差不多大的多个区域,一个对象来的时候,如果有一个空白区域,直接把你的新对象放入进去,然后这一个区域变成了新生代,然后这个新生代满了以后也会进行回收,回收完后进行转移,这个对象大小超过了老年代的45%,它会把这个对象直接放入Humongous(巨大)区域,因为新生代装不下,在老年代满了之后通过fullGC
来释放
G1
的进化特征
(1)并发:
G1
能充分利用CPU多核
来缩短STW
时间,而且G1
并发(让java
程序继续执行)
(2)分代收集
虽然G1
不需要其他收集器配合就能独立管理整个GC
堆,但是还是保留了分代的概念。
(3)空间整合:
与CMS
的”标记-清理"算法不同,G1
从整体是"标记整理"算法,但是从局部看是基于"复制"算法
(4)可预测的停顿:
这是G1
相对于CMS
的另一个大优势,降低停顿时间是G1
和CMS
共同的关注点,但G1
除了这个,能让用户明确指定在一个长度为M毫秒的时间片段内。
如果只设置10ms
,那么当然10ms
是收集不完的,它会优先回收价值最大的Region(区域)
G1
收集器出现了GC
一般是Mix GC
,如果发生了FULL GC
,那么一般是你的项目出现了问题
缺点
垃圾回收的次数变多,硬件要求高
三、G1
回收步骤
(1)名词解析
Minor GC
从年轻代空间(包括Eden
和Survivor
区域)回收内存。Major GC
是清理老年代。Mixed GC
是在G1
收集器中独有的,用于收集整个young gen
以及部分old gen
的GC
。Full GC
是清理整个堆空间—包括年轻代、老年代及永久代(元数据空间)。
(2)回收步骤详解
G1 Young GC
(STW
)
- 当
eden
数据满了,则触发g1 YGC
- 并行的执行:
YGC
将 eden region 中存活的对象拷贝到survivor,或者直接晋升到Old Region中;将SurvivorRegin
中存活的对象拷贝到新的Survivor或者晋升old region。
- 计算下一次
YGC
eden
、Survivor
的尺寸
G1 Mix GC
在G1 GC
中,它主要是为Mixed GC
提供标记服务的,并不是一次GC
过程的一个必须环节。global concurrent marking
的执行过程分为五个步骤:
- 初始标记(
initial mark
,STW
)- 在此阶段,
G1 GC
对根进行标记。
- 在此阶段,
- 根区域扫描(
root region scan
)G1 GC
在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW
)同时运行,并且只有完成该阶段后,才能开始下一次STW
年轻代垃圾回收。
- 并发标记(
Concurrent Marking
)G1 GC
在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被STW
年轻代垃圾回收中断
- 最终标记(
Remark
,STW
)- 该阶段是
STW
回收,帮助完成标记周期。G1 GC
清空SATB
缓冲区,跟踪未被访问的存活对象,并执行引用处理。
- 该阶段是
- 清除垃圾(
Cleanup
,STW
)- 在这个最后阶段,
G1 GC
执行统计和RSet
净化的STW
操作。在统计期间,G1 GC
会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
- 在这个最后阶段,
8、怎么选择垃圾收集器
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于
100m
,使用串行收集器 - 如果是单核,并且没有停顿时间的要求,串行或MM自己选择
- 如果允许停顿时间超过1秒,选择并行或者MM自己选
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器
官方推荐G1
,性能高
五、STW
stop Thread Work,暂停一切线程;
轻GC和重GC的时候会暂停线程,防止在找垃圾的过程中产生新的垃圾(一开始找的时候不是垃圾,找到一半变成垃圾了,又不能重新从头找)
六、JVM调优
1、调优概念
JVM
调优主要是为了减少GC
,特别是full GC
(重GC
)
调整指标
- 内存大小
- 延迟:由于垃圾收集而引起的程序停顿时间。
- 吞吐量:=运行代码时间/(运行代码时间+垃圾收集时间))
2、JVM调优工具
1、JVM
自带的调优工具:Jvisualvm
2、阿里开发的调优工具:Arthas
七、类的生命周期
类中的静态加载顺序
静态变量/静态代码块(看谁写在前) > 构造方法之外的其他代码块 > 构造方法
1、加载
将.class
文件从磁盘读到内存【通过权限定名(包名+类名)读取的】
2、连接
(1)验证
验证字节码文件的正确性
比如说格式、元数据、字节码、符号引用验证
(2)准备
给类的静态变量分配内存,并赋予默认值
(3)解析
把常量池中的符号引用(class文件格式)替换成直接引用(指针指向地址)
3、初始化
为类的静态变量赋予正确的初始值
八、强/软/弱/虚引用
1、强引用:
只要能够通过GC Root
的引用链找到就不会被垃圾回收,对象未被强引用就被回收
2、软引用
跟缓存一样
java
中使用SoftRefence
来表示软引用,对象软引用时内存不足才释放
如果某个对象与软引用关联,那么JVM
只会在内存不足的情况下回收该对象
3、弱引用:
任何情况必被释放
4、虚引用:
就和没有引用一样,任何情况必被释放
创建的时候会关联一个引用队列,这样子用户就可以决定要不要释放它
九、判断常量是否废弃
常量池中字符串abc
,如果当前无任何引用,就说明常量abc
就是废弃常量,如果这时发生内存回收而且有必要的话,“abc
"会被系统清理出常量池
十、怎样让一个对象自救
就是说 释放后再复活
一个类在被回收之前,他是没有任何引用的,但是你想让他复活,就是GC
回收的时候又持有一个引用,就可以在类里面定义一个Finalize
方法,但是这个方法只能自救一次。