主要包括:
内存结构、判断对象存活、垃圾回收算法、垃圾收集器、GC日志、类加载机制、基本参数调优、基本性能检测工具。
一、内存结构
1. 堆
重要,存放对象实例、数组,垃圾收集的目标,垃圾回收机制算法和JVM参数调优都是主要针对堆的。
多线程共享。
堆内存的划分:新生代 eden s0 s1、老年代,默认比例是 1:2。
2. 栈
线程私有。
存放基本数据类型,局部变量。
3. 本地方法栈
Java语音调用外部语言(C语言)方法使用 native 修饰,通过本地方法接口调用本地方法库,这就需要使用本地方法栈。
4. 方法区
存放永久数据,例如类的信息、常量、静态常量,也叫永久区。
full gc 时可以回收。
多线程共享。
5. 直接内存
NIO 会用到
6. 运行常量池
7. 程序计数器
当前线程执行的字节码指示器
二、垃圾回收机制
JVM不定时的回收不可达的对象。
什么是不可达?就是对象没有被引用。
finalize()
方法是垃圾回收前执行的。
System.gc()
提示gc进行垃圾回收,但不代表马上进行回收。
gc 和主线程一样是守护线程。
三、判断对象存活
使用可达性分析来找到存活的对象。
从一系列 gc root 对象开始搜索,被引用到的对象就标记为存放对象,如果一个对象没有和 gc root 相连,就说明是可回收对象。
什么是 gc root?包括:
- 栈(局部变量表)中引用的对象。
- 方法区中静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中引用的对象。
四、垃圾回收机制
1. 标记-清除
先标记,再清除。
根据可达性分析方法标记需要回收的对象,然后统一回收。
缺点是效率低、产生碎片。
应用在老年代,因为老年代回收次数较少,对效率不是很敏感。
2. 复制
堆中分为新生代、老年代,新生代中分为 eden s0 s1 3个区,s0 s1 大小相等,每次使用一个,另一个用于复制,把可用的对象复制到另一个中,然后把复制源那个区清空。
没有碎片、快,就是比较浪费空间。
应用在新生代。
3. 标记-整理
和“标记-清除”的思路差不多,先标记,再清除,为了解决碎片问题,增加了整理的过程,把可用的对象移到到空间的一段,然后把其余空间删除。
4. 分代
堆中分为新生代、老年代,把上面3个算法整合,新生代使用复制算法,老年代使用标记清除、整理算法。
minor gc 是对新生代的回收。
major gc 回收老年代。
垃圾回收不会发生在永久代,但如果永久代满了,或者超过了临界值,就会触发完全回收(full gc)。
五、JVM参数配置
常见参数:
- -XX:+PrintGC
每次触发GC时打印信息。
- -XX:+UseSerialGC
使用串行回收。
- -XX:+PrintGCDetails
打印更详细的GC日志。
- -Xms
堆初始值,一定要设置为和最大值相等,可以减少垃圾回收的次数。
JVM调优的主要目标就是减少垃圾回收次数,初始值的大小直接决定了回收次数。
- -Xmx
堆最大的值,默认4G。
- -Xmn
新生代堆最大的值,满后触发 minor gc。
- -XX:SurvivorRatio
设置新生代中 eden 和 s0 s1 空间的比例。
- -XX:NewRatio
设置新生代和老年代的比例。
配置堆内存大小
查看堆内存使用情况:
public class Test {
public static void main(String[] args) {
// 测试空间占用
// byte[] b = new byte[25 * 1024 * 1024];
// System.out.println("分配了25M空间给数组");
// 当前已分配空间和剩余空间的总量
long totalMemory = Runtime.getRuntime().totalMemory();// 字节
// 最大堆空间,虚拟机将尝试使用的最大内存量(默认为物理内存的1/4)
long maxMemory = Runtime.getRuntime().maxMemory();
// 剩余空间,初始堆空间大小 减去 已经分配的空间大小
long freeMemory = Runtime.getRuntime().freeMemory();
// 以8G内存为例,输出结果大概为:
// totalMemory:123MB
System.out.println("totalMemory=" + totalMemory / 1024 / 1024 + "MB");
// maxMemory:1820MB
System.out.println("maxMemory=" + maxMemory / 1024 / 1024 + "MB");
// freeMemory: 96MB
System.out.println("freeMemory=" + freeMemory / 1024 / 1024 + "MB");
}
}
设置参数示例:
// 最大20m,初始20m,新生代最大值为1m,eden和from to的比例为2:1:1(比如 eden 为512k,那么from为256k,to为256k),新生代和老年代的比例为1:2(比如新生代为1m,那么老年代为2m)
-Xmx20m -Xms20m -Xmn1m -XX:SurvivorRatio=2 -XX:NewRatio=2
- 初始值默认为物理内存的1/64。
- 最大值默认为物理内存的1/4。
totalMemory
需要注意一下,他的含义是:当前已分配空间和剩余空间的总量。
刚开始的时候,对象占用空间很小,其值就是堆空间的初始值。
![1527923-836c8b425e33e142.jpg](https://upload-images.jianshu.io/upload_images/1527923-836c8b425e33e142.jpg)
但随着创建的对象越来越多,如果空闲空间小于了临界值,为新对象分配空间时就不放入空闲空间了,而是放在初始空间之外,这时 totalMemory 的值就是:初始空间值 + 之外占用空间的大小。
![1527923-060143a9ab67f732.jpg](https://upload-images.jianshu.io/upload_images/1527923-060143a9ab67f732.jpg)
六、溢出问题
- 栈溢出
递归时容易发生栈溢出,因为会超出最大深度。例如:
public class Test {
private static int count;
public static void count() {
try {
count++;
count();
} catch (Throwable e) {
System.out.println("最大深度:" + count);
e.printStackTrace();
}
}
public static void main(String[] args) {
count();
}
}
报错:java.lang.StackOverflowError
,输出最大深度大概为10185。
设置最大深度参数:-Xss5m
,5m支持的深度大概为:263323。
内存溢出和内存泄漏的区别
内存溢出:在申请内存空间时,超出最大堆空间。
内存泄漏:内存空间没有及时释放,长时间占用内存,导致内存溢出。
七、垃圾回收器
类型:
- 串行回收
只有一个线程执行回收,用户线程等待。
- 并行回收
多个垃圾回收线程并行工作,但此时用户线程仍然处于等待状态,单核时间片轮换处理。
- 并发回收
用户线程和垃圾收集线程同时执行,多核处理器同时执行。
回收器:
- serial 收集器
串行,用户线程等待,stop th world。
- parNew 收集器
serial 的多线程版本。
新生代并行,老年代串行。新生代使用复制算法,老年代使用标记算法。
- parallel 收集器
类似 parNew,更关注吞吐量,并行。
- cms 收集器
以最短停顿时间为目标。
- g1 收集器
支持很大的堆,在主线程暂停的情况下,使用并行收集,在主线程运行时,使用并发收集。
八、字节码
通过字节码可以对类的基本信息进行操作,例如新增或删除属性、方法,也可以新增类。比如 lombok 就是使用字节码来动态添加 set/get。
常用框架:
ASM、BCEL、CGLB、javassist
九、类加载器
典型场景:通过classloader热部署。
java文件 -> class文件 -> 加载到虚拟机。
加载过程:
- 加载,将class文件字节码内容加载到内存,将这些静态数据放在方法区,并在堆中生成一个代表这个类的Class对象。
- 链接(验证、准备、解析),验证用来确保类信息符合规范,没有安全问题,准备用来做内存分配,解析用来把符号引用替换为字节引用。
- 初始化,执行类的静态代码块、构造函数。
![1527923-af56dde6b30d3249.jpg](https://upload-images.jianshu.io/upload_images/1527923-af56dde6b30d3249.jpg)
热部署
原理:把之前的class文件删掉,使用 classloader 重新加载一下新的 class 文件。
代码思路:
- 读取class文件
- 转为 byte[]
- 使用 defineClass() 加载