内存结构

image-20251105160618906

1.程序计数器

作用:记录下一条指令的执行地址

特点:

  • 线程私有
  • 不会内存泄漏

2.Java虚拟机栈(存局部变量,方法参数)

1.定义:

image-20250629171743329

  • 每个线程运行时需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(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() 方法
  • 具体步骤:
    1. 应用程序类加载器收到加载请求,先不自己加载,委托给父类(扩展类加载器);
    2. 扩展类加载器收到请求,也委托给父类(启动类加载器);
    3. 启动类加载器在自己的加载范围(lib 目录)查找,找到则加载,返回 Class 对象;
    4. 若启动类加载器找不到,扩展类加载器在自己的范围(ext 目录)查找,找到则加载;
    5. 若扩展类加载器也找不到,应用程序类加载器在 classpath 下查找,找到则加载;
    6. 若所有父加载器都找不到,抛出 ClassNotFoundException

3.核心作用

  1. 沙箱安全:防止核心类被篡改(如自己写一个 java.lang.String,双亲委派会优先加载 JVM 的核心 String 类,而非自定义类,避免恶意替换);
  2. 类唯一性:同一类的全限定名,只会被一个类加载器加载一次(父加载器加载过,子加载器无需重复加载),保证类的一致性(如 Object 类在整个 JVM 中只有一个 Class 对象)。

4. 打破双亲委派的场景

双亲委派是默认规则,但可主动打破,常见场景:

  1. Tomcat 等 Web 服务器:每个 Web 应用需要隔离(如不同应用的 com.test.User 是不同类),Tomcat 的 WebAppClassLoader 会先加载自己的类,再委托父加载器(反向委派);
  2. SPI 机制(服务发现):如 JDBC(java.sql.Driver),核心接口在 rt.jar(启动类加载器加载),但驱动实现类在第三方 Jar(classpath),需应用程序类加载器加载,通过 Thread.currentThread().setContextClassLoader() 打破委派;
  3. OSGi 框架:模块间热部署、类隔离,支持同一类被不同类加载器加载(按需委派);
  4. 自定义类加载器:重写 loadClass 方法,改变委托顺序(如先自己加载,再委托父加载器)。