内存结构
1.程序计数器
作用:记录下一条指令的执行地址
特点:
- 线程私有
- 不会内存泄漏
2.Java虚拟机栈(存局部变量,方法参数)
1.定义:
- 每个线程运行时需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存(参数、局部变量、返回地址)
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
2.问题辨析:
1.垃圾回收是否涉及栈内存?
不会,也不需要。
2.栈内存分配越大越好吗?
-Xss1024k
不一定,栈内存越大,线程数就越小。
3.方法内的局部变量是否线程安全?
需要判断变量是不是共享变量,私有变量是线程安全的。共享变量需要考虑线程安全问题。
局部变量是否逃离了方法的作用范围,通过返回值返回了引用地址,可能会被其他线程访问,也需要注意线程安全问题。
3.栈内存溢出
栈帧过多(如递归过深)
栈帧过大()
4.线程运行诊断
1.线程占用cpu过高
定位
- 用top命令定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu | grep 进程id(用ps命令进一定位哪个线程引起的cpu占用过高)
- jstact 进程id—》可以根据线程id找到有问题的线程,进一步定位到问题代码的行数。
2.程序运行很长时间没有结果
可能是有死锁,通过jstact id 查看。
3.本地方法栈
给本地方法运行提供内存空间。
object类里面的clone(),wait(),notify()等方法都是本地方法,是通过 JNI(Java Native Interface)调用c或c++代码实现操作系统的相关api调用。
4.堆(存对象)
1.定义
- 通过new关键字,创建出来的对象都会使用堆内存
特点
- 它是线程共享的,堆中对象都需要考虑线程安全问题
- 有垃圾回收机制
2.堆内存溢出
如果不断新建对象且一直在使用,就会爆堆内存。
通过-Xmx8m可以设置堆内存大小,这里的8m比较小了
3.堆内存诊断
1.jps工具,查看当前运行的java进程。
2.jmap工具,查看堆内存占用情况 jmap -heap +进程id
3.jconsole工具, 图形界面的,多功能的监测工具,可以连续监测
案例:
多次垃圾回收之后堆内存占用依然很高
jvisualvm工具可视化的虚拟机。
通过堆转储 堆 dump来保存快照查看情况。
方法区(存储类元数据、方法字节码、即时编译器需要的信息)
方法区是JVM规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
永久代是Hotspot虚拟机对jvm、规范的实现(1.8之前)
元空间是HotSpot虚拟机对jvm规范的实现(1.8以后),使用本地内存作为这些信息的存储空间。
JVM垃圾回收算法
1.标记清除法(一般不用,cms用,已废弃)
标记:从根对象(一定不会回收的对象如局部变量,静态变量引用的对象)出发,看能不能被引用,找到了当前对象,加上标记,表示不会回收。
缺点:虽然释放内存多,但是比较分散,内存碎片多。
2.标记整理法(场景:老年代垃圾回收)
前面类似,后面再整理一下存活的对象,避免内存碎片,但是由于多了个步骤,造成效率低。
3.标记复制法(场景:新生代垃圾回收)
把内存分成两部分区域,第一个阶段标记,第二阶段把存活对象复制到空闲区域,复制完成后另一边区域对象直接清空。
缺点:内存占用大
说说GC和分代回收算法
GC的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度。
GC要点:
1.回收的区域是堆内存,不包括虚拟机栈,在方法调用结束时会自动释放占用内存。
2.判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象。
3.GC具体的实现称为垃圾回收器
4.GC大都采用了分代回收思想,理论依据是大部分对象朝生夕灭用完立刻可以回收,另有少部分对象会长期存活,每次很难回收,根据这两类对象的特性将回收区域分为新生代老年代,不同区域应用不同的回收策略
5.根据GC的规模可以分成Minor GC, Mixed GC, Full GC。
分代回收与GC规模
分代回收
1.伊甸园eden,最初对象都分配到这里,与幸存区合称新生代
2.幸存区survivor,当伊甸园内存不足,回收后的幸存对象到这里,分成from和to,采用标记复制算法。每次回收后from和to交换区域。
3.当幸存区对象熬过几次回收(不超过15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)。
GC规模:
1.Minor GC发生在新生代的垃圾回收,暂停时间短。
2.Mixed GC新生代+老年代部分区域的垃圾回收,G1收集器特有
3.Full GC 新时代+老年代完整垃圾回收,暂停时间长,应尽力避免
三色标记
黑色:已标记
灰色:标记中
白色:还未标记
并发漏标问题:
1.Incremental Update:只要赋值发生,被赋值的对象就会被记录
2.Snapshot At The Beginning, SATB: 1.新加对象会被记录2被删除引用关系的对象也被记录
垃圾回收器
1.Parallel GC(有暂停,会使用多个线程并行回收)
1.eden内存不足发生Minor GC,标记复制STW(停止所有工作线程)
2.old内存不足发生full GC ,标记整理STW
3.注重吞吐量
2.ConcurrentMarkSweep GC(暂停时间短)
1.old并发标记,重新标记时需要STW,并发清除
2.Failback Full GC
3.注重响应时间
3.G1(jdk9开始,默认的垃圾回收器)
1.响应时间与吞吐量兼顾
2.划分多个区域,每个区域都可以充当eden,survivor,old,humongous(用来存储大对象)
3.新生代回收:eden内存不足,标记复制STW
4.并发标记(老年代内存占比占堆内存45%以上时触发):old并发标记,快照法处理并发漏标问题时,重新标记时需要STW
5.混合收集:并发标记完成,开始混合收集,参与复制的有eden、survivor,old,其中old会根据暂停时间目标,选择部分回收价值高的区域,复制时STW
6.Failback Full GC
类加载
分五个阶段:加载,验证,准备,解析,初始化
1. 加载(Loading):找到并读取字节码,创建.Class对象
2. 验证(Verification):确保字节码安全
3. 准备(Preparation):为类变量(static 修饰的变量)分配内存 + 赋默认值
- 若类变量加
final(常量),准备阶段直接赋 代码中定义的初始值(如static final int a = 10,准备阶段 a=10,而非 0)。非final常量在初始化时才赋值
4. 解析(Resolution):符号引用转直接引用
符号引用:.class 字节码中用 “全限定名” 描述的引用(如 Ljava/lang/String; 表示 String 类),无具体内存地址;
直接引用:指向内存中实际对象的地址(如类的内存地址、方法的入口地址)。
5. 初始化(Initialization):执行类构造器 <clinit>()
< clinit >() 方法的构成:
类变量的显式赋值语句(如 static int a = 10);
静态代码块(static { … });
双亲委派机制(类加载的核心规则)
1. 定义
当一个类加载器需要加载某个类时,先委托给其父加载器加载,只有父加载器无法加载(找不到类)时,才由自身加载。核心是 “先找上级,上级不行自己来”。
2. 类加载器层次结构(从上到下)
| 类加载器 | 加载范围(核心职责) | 特点 |
|---|---|---|
| 启动类加载器(Bootstrap) | JVM 核心类(JAVA_HOME/lib 下的 Jar 包,如 rt.jar) |
1. 由 C/C++ 实现,无 Java 对象(getClassLoader() 返回 null);2. 仅加载特定文件名的 Jar 包 |
| 扩展类加载器(Extension) | JVM 扩展类(JAVA_HOME/lib/ext 下的 Jar 包) |
由 Java 实现(sun.misc.Launcher$ExtClassLoader) |
| 应用程序类加载器(Application) | 应用程序类(classpath 下的类,自己写的代码、第三方 Jar) | 由 Java 实现(sun.misc.Launcher$AppClassLoader);默认类加载器 |
| 自定义类加载器 | 自定义需求(如加密类、热部署类) | 继承 ClassLoader,重写 findClass() 方法 |
- 具体步骤:
- 应用程序类加载器收到加载请求,先不自己加载,委托给父类(扩展类加载器);
- 扩展类加载器收到请求,也委托给父类(启动类加载器);
- 启动类加载器在自己的加载范围(
lib目录)查找,找到则加载,返回Class对象; - 若启动类加载器找不到,扩展类加载器在自己的范围(
ext目录)查找,找到则加载; - 若扩展类加载器也找不到,应用程序类加载器在 classpath 下查找,找到则加载;
- 若所有父加载器都找不到,抛出
ClassNotFoundException。
3.核心作用
- 沙箱安全:防止核心类被篡改(如自己写一个
java.lang.String,双亲委派会优先加载 JVM 的核心String类,而非自定义类,避免恶意替换); - 类唯一性:同一类的全限定名,只会被一个类加载器加载一次(父加载器加载过,子加载器无需重复加载),保证类的一致性(如
Object类在整个 JVM 中只有一个Class对象)。
4. 打破双亲委派的场景
双亲委派是默认规则,但可主动打破,常见场景:
- Tomcat 等 Web 服务器:每个 Web 应用需要隔离(如不同应用的
com.test.User是不同类),Tomcat 的 WebAppClassLoader 会先加载自己的类,再委托父加载器(反向委派); - SPI 机制(服务发现):如 JDBC(
java.sql.Driver),核心接口在rt.jar(启动类加载器加载),但驱动实现类在第三方 Jar(classpath),需应用程序类加载器加载,通过Thread.currentThread().setContextClassLoader()打破委派; - OSGi 框架:模块间热部署、类隔离,支持同一类被不同类加载器加载(按需委派);
- 自定义类加载器:重写
loadClass方法,改变委托顺序(如先自己加载,再委托父加载器)。







