Java程序是怎么运行的
类加载到JVM会经过: 类加载,验证,准备,解析,初始化,使用,卸载几个阶段.
准备阶段: 会为加载进来的类和类变量分配好内存.并设置默认值.
初始化阶段: 静态代码块会在这个阶段执行.会为类赋值.
什么时候初始化一个类:
- new 一个类的时候会触发类加载到初始化的全部过程
- 包含 main 方法的类
- 初始化一个类的时候发现父类没有初始化.此时会触发父类的加载到初始化全过程
类加载器
-
启动类加载器(Bootstrap ClassLoader)
负责加载 Java 目录下核心类. JDK 安装目录下 lib 目录内的核心类库.
-
扩展类加载器(Extension ClassLoader)
负责加载 Java 目录下扩展类.JDK 安装目录下 lib/ext 目录内的扩展类库.
-
应用程序类加载器(Application ClassLoader)
负责加载 ClassPath 环境变量所指定的路径中的类.就是自己写的代码.
-
自定义类加载器
根据自己的需求实现加载器.
双亲委派机制
- 自定义加载器 通知 应用程序类加载器 加载.
- 应用程序类加载器 通知 扩展类加载器 加载.
- 扩展类加载器 通知 启动类加载器 加载.
- 启动类加载器 通知 扩展类加载器 加载.
- 扩展类加载器 通知 应用程序类加载器 加载.
- 应用程序类加载器 通知 自定义加载器 加载.
由子到父在由父到子的加载模式.就是双亲委派.
可以避免多层级的加载器结构重复加载某些类.
JVM 的内存区域划分
- 包含 main() 方法的类被加载到 JVM 方法区.(类加载阶段.完成类的内存分配以及变量赋值)
- main 线程执行 main() 方法. main 线程有自己的(虚拟机栈,程序计数器,本地方法栈)
- 线程执行方法会为每个方法创建栈帧到线程自己的虚拟机栈中.
- 虚拟机栈中存放各种局部变量(值类型和引用类型的指针地址)
- 引用类型对象创建在堆内存中.
垃圾回收
一段代码执行过程中,线程虚拟机栈与堆内存的变化. 其中 load 方法执行完之后, 堆内存中的 User 对象实例就没有其他引用了. 此时 User 实例占用的内存就可以回收了.
堆内存的分代模型
public static void main(String[] args) {
int i = 1;
load();
}
public static void load(){
User user = new User();
user.sayHello();
}
load 方法执行完之后. 堆内存中的 User 对象实例就没有引用了. 占用的内存就可以回收了.
User 实例在堆内存中存活的周期是极短的.
public static User user = new User();
public static void main(String[] args) {
int i = 1;
load();
}
public static void load() throws InterruptedException {
while (true){
user.sayHello();
Thread.sleep(1000);
}
}
这种被类的静态变量长期引用的对象, 实例需要长期存活于堆内存中.
存活时间极短的对象就是年轻代. 长期存活的对象存放于老年代.
JVM里的永久代其实就是方法区
- 大部分的正常对象,都是优先在新生代分配内存
- 分配新的对象时,发现新生代空间不足此时会触发垃圾回收 Minor GC/Young GC
- 15次垃圾回收没有回收掉的对象.会放入老年代
- 老年代内存满了会触发 Old GC
哪些对象可以回收?哪些对象不能回收
可达性分析算法
说对每个对象,都分析一下有谁在引用他,然后一层一层往上去判断,看是否有一个GC Roots
- 虚拟机栈(局部变量表中引用的对象)
- 本地方法栈(本地方法引用的对象)
- 方法区中静态属性引用的对象
- 方法区中静态常量池中引用的对象
Java中对象不同的引用类型
强引用
// 一个变量引用一个对象就是强引用
// 只要存在强引用的对象,垃圾回收的时候一定不会回收
User user = new User();
软引用
// 正常情况是不会被垃圾回收的.
// 如果垃圾回收之后,内存仍然不够.此时就会回收掉这部分对象.
SoftReference<User> user = new SoftReference<>(new User());
弱引用
// 垃圾回收时一定会回收.
WeakReference<User> user = new WeakReference<User>(new User);
垃圾回收算法
标记清除算法
- 优点: 简单
- 缺点:
- 效率低,标记和清除效率都很低
- 产生大量的不连续的内存碎片,从而导致分配大对象时触发GC
复制算法
新生代划分为两块内存,一块儿使用一块不使用. 发生垃圾回收时就将存活对象复制到未使用的的内存. 两块内存交替使用.
- 优点: 实现简单,运行高效,不用考虑内存碎片问题
- 缺点: 浪费内存
复制算法优化
新生代划分为三块儿.1个Eden区,2个Survivor区
其中Eden区占80%内存空间,每一块Survivor区各占10%内存空间.
其中 Eden区 和 其中一个 Survivor区 是可用的. 垃圾回收时会将存活对象一次性复制到 空闲的Survivor区. 然后清理掉 Eden区和使用的Survivor区. 然后交替使用.
优化后的复制算法.有90%的新生代内存可用.
什么时候对象进入老年代
-
15次GC都没清理掉的对象,进入老年代. (15岁)
-XX:MaxTenuringThreshold 参数可以设置岁数.
-
动态对象年龄判断
当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代.
例: Survivor区 有五个对象.分别是 1岁,2岁,3岁,4岁,5岁. 此时五个对象的大小总和 大于 Survivor区内存大小的 50% . 此时 大于等于5岁的对象进入老年代.
-
大对象直接进入老年代
-XX:PretenureSizeThreshold 可以设置大对象大小.(字节数)
例: 设置为 1048576 字节(Byte) (1M) 大于 1M 的对象直接进入老年代.
-
Young GC 之后, 存活对象太多. 无法放入 Survivor 直接进入老年代.
Young GC 时. 空闲的 Survivor 区内存不够放存活对象?
存活对象太多无法放入空闲Survivor区时,会直接放入 老年代
Young GC 时. 空闲的 Survivor 区内存不够放存活对象. 老年代也不够?
老年代垃圾回收算法: 标记整理算法
- **标记:**标记垃圾对象.标记完之后的对象可能七零八落的.(GC Roots)
- **整理:**整理存活对象移动到一块儿连续的内存.
- **清除:**一次性清理垃圾对象.
无内存碎片.但是性能差.
Stop The World(STW)
垃圾回收进行时候,停止系统程序. 垃圾回收完毕后恢复程序运行.
STW 会使用系统,短暂的暂停运行.
垃圾回收器
**Serial和Serial Old垃圾回收器:**分别用来回收新生代和老年代的垃圾对象.
单线程.
**ParNew和CMS垃圾回收器:**ParNew现在一般都是用在新生代的垃圾回收器,CMS是用在老年代的垃圾回收器.
多线程.
**G1垃圾回收器:**统一收集新生代 和老年代,采用了更加优秀的算法和设计机制.
ParNew 垃圾回收器
新生代多线程垃圾回收器. 采用复制算法.
-XX:+UseParNewGC添加次参数.设置 JVM 新生代垃圾回收器.
默认线程数
ParNew 垃圾回收器运行时,默认线程数为 CPU 核心数是一致的.
-XX:ParallelGCThreads可以设置线程数.
CMS 垃圾回收器
老年代多线程垃圾回收器.采用标记整理算法.
如果老年代垃圾回收时,发生 STW .会导致系统长时间无法反映.
所以CMS采用的是
垃圾回收线程和系统工作线程尽量同时执行的模式来处理的.
默认线程数
CMS默认线程数是 (CPU核心数+3) / 4
CMS 如何实现系统一边工作的同时进行垃圾回收?
CMS 在执行一次垃圾回收的过程一共分为 4 个阶段:
-
初始标记
暂停一切工作线程(STW).
标记 GC Roots 直接引用的对象.(速度很快)
-
并发标记
恢复工作线程.
并发对老年代所有对象进行 GC Roots 追踪,追踪所有对象是否被 GC Roots 引用. (最耗时)
-
重新标记
暂停一切工作线程(STW).
第二阶段系统未停止运行.所以会有一些漏网之鱼.再次标记对系统运行变动过的少数对象进行标记.(速度很快)
-
并发清理
恢复工作线程.
整理清理垃圾对象.
问题
-
消耗 CPU 资源. CMS 执行期间垃圾回收线程和系统工作线程同时工作.会导致 CPU 资源被垃圾回收线程占用一部分.
-
浮动垃圾
CMS 垃圾回收线程运行期间,如果发生 Young GC 可能会产生新的对象进入老年代.然后又立刻变成垃圾对象.但是 CMS 仅仅清理标记后的垃圾对象.
如果不允许浮动垃圾存在则没办法进行 Young GC .所以 CMS 默认在老年代的内存占用 92% 时就会发生 Old GC. 预留 8% 的内存给 Young GC 放入新的对象.
-XX:CMSInitiatingOccupancyFaction可以设置百分比.如果 CMS 运行期间发生了 Young GC .且放入老年代的对象大小超过预留大小,则会垃圾回收失败 (Concurrent Mode Failure). 直接进入 STW 并使用 Serial Old 单线程垃圾回收器.重新进行 GC Roots 标记.然后清理掉所有垃圾对象. ==最严重的情况==
-
内存碎片
4个阶段执行完成后会产生大量的内存碎片.
CMS 垃圾回收完成之后, 默认会进行碎片整理.
-XX:+UseCMSCompactAtFullCollection设置开启整理. 默认开启.-XX:CMSFullGCsBeforeCompaction设置进行多少次 Full GC 后,进行一次内存碎片整理. 默认值为 0 .每次 Full GC 之后都会进行内存碎片整理
G1垃圾回收器
G1 内存模型
G1垃圾回收器是可以同时回收新生代和老年代的对象.
G1 把 Java 堆内存拆分为多个大小相等的Region.
物理上没有新生代老年代. 逻辑上存在新生代老年代.
特点
G1 最大的特点就是可以==设置 一个垃圾回收的预期停顿时间==
例: 可以设置 G1 在 1个小时内由G1垃圾回收器导致 STW 时间,不能超过 1 分钟.
G1是如何做到对垃圾回收导致的系统停顿可控的?
G1 将 堆内存 拆分为大量的 Region .追踪每个 Region 的回收价值(回收对象大小和回收时间). 然后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象.
G1 进行垃圾回收时会根据每个Region的垃圾对象大小,回收时间以及一段时间内垃圾回收STW的时间进行评估. 选择回收不同的Region达到设置的预期停顿时间等.
Region可能属于新生代也可能属于老年代
最开始 Region 是既不属于 新生代也不属于老年代. 但是随着系统的运行. Region 放入了许多对象此时 Region 成为了 新生代. 然后随着 G1 的垃圾回收. Region 内的对象躲过了许多次垃圾回收.此时 Region 就成为了老年代.
Region 属于新生代还是老年代是由 G1 控制的.
启动 G1 垃圾回收器
-XX:+UseG1GC设置使用 G1 垃圾回收器.
Region大小与个数
JVM 最多有 2048 个 Region. Region 的大小必须是 2 的倍数.
-XX:G1HeapRegionSize指定 Region 个数.例: 设置堆大小为 4G, 则默认有 2048 个 Region .每个 Region 大小为 2M.
新生代与老年代
JVM刚刚启动时,新生代默认占比 5%.
-XX:G1NewSizePercent可以设置. 使用默认值即可. 随着系统的运行 JVM 会加大 新生代占比.最大不能超过 60%.-XX:G1MaxNewSizePercent参数可以修改. 进行了垃圾回收 新生代占比会减少.
新生代的Eden和Survivor
-XX:SurvivorRatio=8表示 Eden 区占比 80%. 两个 Survivor 各占 10%.随着系统的运行Eden和Survivor大小会变化.
G1的新生代垃圾回收
- 系统的不断运行,不断的产生对象. 直到 新生代 达到了 堆内存的 60%. 此时触发 G1 的新生代垃圾回收.进入 STW .
- 把 Eden Region 和 使用 Survivor Region 中的存活对象复制到空白 Survivor Region 中.
- 回收掉 Eden Region 和 使用 Survivor Region.
-XX:MaxGCPauseMills可以设定目标GC停顿时间.默认是200ms.G1 会保证在停顿时间范围内尽可能的回收更多的垃圾对象.
对象什么时候进入老年代?
- 15次GC都没清理掉的对象,进入老年代. (15岁)
- 动态对象年龄判断
大对象 Region
之前其他垃圾回收器.会将 大对象 直接进入老年代.
G1 提供了专门的 大对象 Region.用来存放大对象.
对象大小超过了Region的 50%,则被判定为大对象. 如果一个对象特别的大.还有可能跨 Region 存放.
大对象的垃圾回收.会在新生代和老年代触发垃圾回收的时候.捎带手.回收.
JVM GC
Young GC 对系统的影响
普通的非超高并发的系统. 部署在 2C4G 2C8G 4C8G 之类的机器上. 新生代 Eden 区的内存也就1G左右. 复制算法回收相当快. 几毫秒或者几十毫秒. 毫秒用户几乎时无感知的.
新生代的调优方式就是评估好系统堆内存使用量. 分配好新生代内存. 减少 Young GC.
超高并发的系统.部署在大内存的机器上 32C64G 此时 Eden 区的内存可能 30~40G.
超过并发每秒几万请求.甚至更多.假设 1 分钟 新生代内存满了.此时会发生 Young GC.
一次回收 30~40G 的内存. 此时花费的时间就比较久了. 可能几秒钟.
G1 垃圾回收器. 很适合大内存机器上部署的高并发JVM.
G1 每次垃圾回收会评估在有限的时间内回收最大量的 Region .
频繁的 Old GC
-
15次GC都没清理掉的对象,进入老年代. (15岁)
-
动态对象年龄判断.
-
Young GC 之后, 存活对象太多. 无法放入 Survivor 直接进入老年代.
如果第二条和第三条频繁发生,导致大量的非长期存活对象进入老年代.从而导致 Old GC 也会频繁发生. 系统卡死也会频繁发生.
Survivor 区内存大小设置很关键. 太小会导致这两种情况频繁发生.
永久代GC
一般不需要垃圾回收. 永久代保存的都是类.常量池.
如果永久代真的放满了,回收之后发现没腾出来更多的地方,此时只能抛出
内存不够的异常了
Full GC
Young GC + Old GC + 永久代 GC = Full GC
一般 Old GC 触发会联动的触发 Young GC 和 永久代GC