堆内存
类加载器
运行时根据需要动态加载类,将.class文件加载到内存中并转换为Class对象,类加载的过程:
- 加载:通过io流把字节码文件读入到jvm的元空间
- 连接:
- 验证:校验字节码文件头8位是否是
cafebabe - 准备:为类中静态部分开辟空间并赋初始值
- 解析:将符号引用转换为直接引用
- 验证:校验字节码文件头8位是否是
- 初始化:为类中的静态部分赋指定值并执行静态代码
jdk提供了下面的三个类加载器:
BootStrapClassLoader:根类加载器,加载jre/lib下面的类ExtClassLoader:扩展类加载器,加载jre/lib/ext下面的类AppClassLoader:应用类加载器,加载classpath下面的类
双亲委派:
- 类加载器加载一个类时,首先请求它的父类加载器,如果父类加载过则返回,如果没有则继续向上请求,一直到根类加载器
BootStrapClassLoader,如果启动类加载器也没有找到,则从上到下依次由子加载器加载 - jvm中判断一个类是不是已经被加载过的逻辑是:类名+对应的类加载器实例
- 设计优点:避免核心类库被篡改
// 核心代码
protected Class<?> loadClass(String name, boolean resolve) // resolve默认是false
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
c = findClass(name);
}
}
return c;
}
}
堆空间
- 堆空间是线程共享的
- 堆的默认最大空间是物理内存的1/4
- 堆分为新生代和老年代,新生代:老年代=1:2
- 新生代又分为Eden区、s0区、s1区,Eden:s0:s1=8:1:1
对象进入老年代的方式:
- 年龄超过某个阈值,这个是动态调整的,区间在7-15
- 大对象可直接进入老年代,jdk8没有启用该配置
- 动态年龄判断:当Survivor区中年龄从1到n的对象大小之和累加超过Survivor区的50%时,年龄大于等于n的对象直接进入老年代
元空间
- jdk1.8叫元空间(之前叫永久代、方法区等),元空间存储类信息、字符串常量池等
- 元空间使用本地内存,大小受物理内存限制,不受堆内存控制
程序计数器
线程私有,每个线程都有自己的程序计数器,记录待执行的下一条指令的地址,jvm中唯一不会出现内存溢出的地方
本地方法栈
- 线程私有,本地方法:native修饰的方法,由c++等语言实现,用来调用操作系统的方法
- 虚拟机栈存的是java方法调用过程中的栈帧,本地方法栈存的是本地方法调用过程中的栈帧
- 也会出现
内存溢出和栈溢出
虚拟机栈
-
虚拟机栈是线程私有,每个线程对应一个栈,栈中放栈帧,通过
-Xss设置栈大小 -
一个方法开始执行栈帧入栈,方法执行完对应的栈帧出栈,所以虚拟机栈不需要进行垃圾回收
-
线程太多,没有足够的内存创建 虚拟机栈 会出现
内存溢出,方法调用层次太多可能会出现栈溢出 -
局部变量表保存这个方法执行过程中的局部变量,操作数栈用来辅助计算
垃圾回收算法
- 复制算法:把内存分成两部分,每次只用其中一部分,需要进行回收时,将已经使用的内存中的对象移动到另一块内存中,然后将之前已经用过的那块内存回收,就是
s0,s1区,高效且避免内存碎片 - 标记-清除算法:先标记垃圾对象,然后直接清除,这种方式会产生很多内存碎片
- 标记-整理算法:先标记垃圾对象,然后将存活的对象移动到一起,最后将其余对象回收
垃圾回收器
先用“可达性分析算法”找到没有被GC Roots引用的对象,然后使用“垃圾回收算法”回收对象,如果对象在 finalize 方法中不能拯救自己就会被回收(除非有性能衡量指标,否则尽量不要修改GC的配置参数,即改也必须基于大量的测试结果)
| 名称 | 适用代际 | 线程模式 | 备注 |
|---|---|---|---|
| Parallel | 新/老年代 | 并发 | jdk8默认 |
| ParNew | 新生代 | 并发 | 默认与CMS是一对 |
| CMS | 老年代 | 并发 | 通过并发标记和回收减少STW |
| G1 | 全堆 | 并发 | jdk9+默认,将内存分成小块,分新老年代,jdk9+默认 |
| ZGC | 全堆 | 并发 | jdk11推出,将内存分成小块,不分新老代 |
jvm调优
- 调整新生代大小,让更多对象在
Minor GC时被回收 - 调整对象晋升老年代时的年龄阈值
- 减少大对象的创建,可将大对象拆成小对象
- 选择合适的垃圾回收器,比如
G1、ZGC
参数总结
场景排查
jps -l:查看java进程jmap -heap pid:查看堆内存分布jmap -histo:live 进程id | more:查看对象数量jmap -dump:format=b,file=heapdump.hprof 进程id:手动生成堆快照-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/dump.hprof:堆内存溢出时自动生成快照-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log:保存gc日志
参数设置
-Xss:设置虚拟机栈大小-Xms:设置堆的初始大小-Xmx:设置堆的最大内存-XX:MetaspaceSize:设置元空间大小-XX:MaxMetaspaceSize:设置元空间最大大小-XX:+UseAdaptiveSizePolicy:自动调整堆空间大小,默认开启-XX:PretenureSizeThreshold:设置大对象进入老年代的阈值-XX:InitialTenuringThreshold、-XX:MaxTenuringThreshold:设置进入老年代的年龄阈值
面试题
-
对象都是在堆中创建吗? 有些对象不会被方法外部访问到,这样的对象会被创建在栈上,在栈上创建对象的时候 会把对象拆分成基本类型(叫标量替换)
-
逃逸分析是什么? jvm会分析 锁对象 是否只会被当前的线程访问,如果是的话 可以实现栈上分配对象,逃逸的类型包括:方法逃逸和线程逃逸
-
finalize方法的作用? 对象在被销毁前调用的方法,如果此对象可以和
gc roots上的对象建立链接 则不会被销毁