1. 运行时区域划分
- 方法区:静态变量static、常量final、字符串常量池、类信息
- 虚拟机栈:方法被调用时,会入栈。含有局部变量、方法入口、操作数栈
- 本地方法栈:与虚拟机栈类似,本地方法(native)被调用时,会入栈。
- 堆:堆被划分为新生代、老年代和永久代。其中jdk8之后,元空间取代永久代实现方法区。
- 程序计数器:指向下一条要执行的指令
2. 类加载过程
Java源代码经过编译得到二进制字节码文件,类加载器再将字节码文件加载到JVM内存中,然后经过连接、初始化等操作,完成类的加载过程。
- 加载: 类加载器将字节码文件加载到JVM内存中,在方法区创建对应的 Class 对象,作为 .class 文件进入内存区域后的数据访问入口。
- 验证: 验证字节码文件是否符合JVM规范,不会对JVM有安全性问题。对元数据的验证,比如检查类是否继承了final修饰的类;对符号引用的验证,比如检查符号引用是否能通过全限定名找到以及相关语法。
- 准备: 为类变量(static)开辟内存空间并赋默认值,八大基本类型(byte、short、char、int、long、float、double、boolean)默认值为0/false;对应包装类(Byte、Short、Integer、Long、Float、Double、Character、Boolean)和引用类型默认是null。静态常量默认值为声明时指定的值。若显示指定了变量的值,那么就赋指定的值。比如public static int c=10,在准备阶段,c的值就是10。
- 解析: 将类、接口、方法和字段的符号引用转为直接引用。符号引用是类文件中的一个名字和描述符,而直接引用是指向内存中的地址或常量池中的引用。这里指静态的方法和属性。
- 初始化: 执行类的构造器方法。JVM会执行类变量的赋值操作和静态代码块(static block)中的代码。若类中有父类,父类的初始化会先于子类的初始化执行。若静态变量在声明时已经被赋值(public static int value=123),即准备阶段就已经赋值了,此时如果有额外的赋值操作或静态代码块中的代码,会覆盖或基于准备阶段的值进一步初始化。
2.1 类加载的时机
类首次使用时才会进行类加载,对于加载过的类,会将其缓存起来。每次要加载类时,会先去缓存中找是否加载过,如果找到了,就直接拿来用,没找到才会去加载。
类的使用分为主动使用和被动使用。
- 主动使用:new一个类的实例、调用类变量或给类变量赋值、运行main方法,main方法所属的类会被加载
- 被动使用:作为父类,其子类被实例化时,会先加载父类。
Class.forName和loadClass方法都是用来加载类的
当使用ClassLoader类的loadClass()方法来加载类时,该类只进行加载阶段,不会经历初始化阶段。
利用反射Class.forName(String name,boolean initialize,ClassLoader loader):根据initialize来决定会不会初始化该类,若为true则JVM会初始化该类,不传该参数默认强制初始化。String name是类的全名(包括包名),loader是用于加载类的类加载器。
2.2 new一个对象的过程
3. 双亲委派机制
JVM是按需动态加载类,采用双亲委派机制,当一个类加载器需要加载类时,会先委托给其父类加载器尝试加载,若父类加载器无法加载,才会交给当前的类加载器尝试加载。
优点: ①保证Java核心库的安全性,防止恶意篡改核心类 ②避免重复加载。
首先要了解到,类加载器有哪些?
- 启动类加载器(Bootstrap ClassLoader): 负责加载Java的核心类库,如rt.jar(Java运行时库。java.util.String等)。该类加载器由C++编写,直接由JVM实现,不继承java.lang.ClassLoader类,是其他所有类加载器的父类。
- 扩展类加载器(Extension ClassLoader): ClassLoader的子类,负责从java.ext.dirs系统属性指定的目录中加载类库(如jar文件),这些文件通常位于jre/lib/ext目录下。
- 应用程序类加载器(Application ClassLoader): ClassLoader的子类,也叫系统类加载器(System ClassLoader),若没有自定义类加载器,则默认这个类加载器,负责加载用户类路径(Classpath)上的类库。
- 自定义类加载器(Custom ClassLoader): 开发者可以根据需要自定义类加载器,加载自己的类库。
接下来看个示例
运行一下自己写的String类的main方法,报错:找不到main方法??
我不是写了main方法吗?为什么会找不到呢
由于Java的核心类中也有String类,加载String类的时候会先去启动类加载器尝试加载,如果启动类加载器无法加载该类,才会去扩展类加载器尝试加载..应用程序类加载器..
启动类加载器负责加载Java的核心类库,可以加载String类,所以会选择该类加载器负责加载,加载的是Java核心类库中的String类,而String官方的源码中并没有main方法,所以会报错。
这就是双亲委派机制的作用,防止篡改Java的核心类库,保证类加载的一致性。
3.1 那么又是如何防止类重复加载和类加载一致性呢?
只有父类加载器无法加载类时,才会让子类去加载,避免了多个类加载器去加载同一个类或多个类加载器加载不同的类。
4. 垃圾回收
4.1 垃圾回收算法
常见的垃圾回收算法有三种:标记-清除算法、标记-整理算法和复制算法。
- 标记-清除: 从GC Root出发,标记所有可达对象(即活跃对象),然后再把堆中所有对象都遍历一遍,删除掉未标记的对象。
缺点:容易导致大量内存碎片,从而提前触发垃圾回收。比如当内存碎片总和大于要开辟的空间,但每个碎片又小于要开辟的空间,这时候就内存不足了。并且要扫描两次对象,一次是可达对象,一次是所有对象。
优点:适用于活跃对象较多的老年代,内存碎片化就不严重,并且老年代内存更大。
- 复制: 将内存分为两块区域,从GC Root出发,将所有可达对象(即活跃对象)全部复制一份到另一个区域,然后清除掉原来区域的所有对象。
缺点:需要两块一样大小的内存,将活跃对象重新复制一份,对内存要求较高。
优点:适用于活跃对象较少的新生代,98%都是要被回收的对象。只需要扫描一次空间(标记活跃对象并移动)
- 标记-整理: 从GC Root出发,将所有可达对象(即活跃对象)全部压缩到内存的一端,然后将边界外的对象全部清除。
优点:适用于活跃对象多的老年代。老年代中的对象往往具有较好的空间局部性,即对象之间相互引用较多。通过移动存活对象来减少内存碎片,有助于保持这种局部性,而且只需要扫描一次。
新生代中的对象生命周期通常较短,很多对象很快就会被回收。使用标记整理算法可能会因为移动对象而增加不必要的开销。
GC Roots可达对象包括:
- 当前线程栈中引用的对象(如局部变量、操作数栈)
- 方法区中静态属性和常量引用的对象
- 本地方法栈中JNI(Java Native Interface)引用的对象
4.2 三种垃圾回收算法对比
时间和空间不可兼得。
收集速度:复制 > 标记-清除 > 标记-整理
内存利用率:标记-整理 > 标记清除 > 复制
4.3 垃圾回收过程
要了解垃圾回收的过程,首先要知道对象在堆中是如何分配内存的
Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存。
对象的内存分配,从概念上讲基本上都是在堆上分配。新生对象通常会分配到新生代中,少数情况下(例如对象大小超过一定阈值)也可能会直接分配在老年代。
对象的分配规则并不是固定的,取决于虚拟机当前使用的是哪一种垃圾收集器,以及虚拟机中内存相关的参数设定。
- 对象优先在新生代的伊甸园分配。
- 大对象(比如很长的字符串或元素数量庞大的数组)需要大量连续的内存空间,分配在老年代。所以要避免使用大对象,分配空间时容易导致内存还很多的时候提前触发垃圾回收。HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,避免在伊甸区和幸存区来回复制,造成不必要的开销。
- 长期存活的对象进入老年代,虚拟机给每个对象定义了一个对象年龄(Age)计数器,对象通常在伊甸区诞生,若经过一次Minor GC后存活,且能被幸存区容纳,该对象就会被移动到幸存区,并且将对象年龄设为1,对象在幸存区每熬过一次Minor GC,年龄就+1,当增长到一定程度(默认15),就会晋升到老年代。年龄阈值可以通过-XX:MAxTenuringThreshold设置。
- 动态对象年龄判定:为更好适应不同程序的内存状况,HotSpot虚拟机并不永远要求对象的年龄达到阈值才能晋升老年代,若幸存区中相同年龄的对象占内存的总和大于幸存区内存的一半,那么年龄>=该年龄的对象就可以直接进入老年代。
- 新生对象优先分配在新生代的伊甸区,当伊甸区没有足够的内存时,就会触发一次Minor GC,清理掉伊甸区和幸存1区中【死去的对象】,将活跃的对象移动到幸存0区。经过多次GC依旧存活的对象,会被移动到老年代。
- 当幸存0区满了,就会触发Major GC,清理老年代中【死去的对象】,为新生代腾空间。
- 当Major GC触发之后依旧内存不足,则会触发Full GC,对新生代和老年代进行垃圾回收。
- 若Major GC后依旧内存不足,则报OOM异常(内存溢出)。
4.4 垃圾收集器
- Serial 收集器: 新生代收集器,基于复制算法实现的单线程工作收集器,只会使用一个处理器,一条线程去完成垃圾收集工作,并且在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。但它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,简单高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的;对于单核处理器或处理器核心数较少的环境,Serial 收集器由于没有线程交互的开销,可以有更高的收集效率。
- ParNew 收集器: 实质上是 Serial 收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括 Serial 收集器可用的所有控制参数、收集算法(复制算法)、回收策略等都与 Serial 收集器完全一致。虽然除了支持多线程外,和 Serial 收集器相比没有太多创新,但却是不少运行在服务端模式下的HotSpot虚拟机,因为除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。
- Parallel 收集器:基于复制算法实现的新生代收集器,也是能够并行收集的多线程收集器。特点是它的关注点与其他收集器不同,CMS等收集器关注点是尽可能缩短垃圾回收时用户线程的停顿时间,而 Parallel 收集器的目标则是达到一个可控制的吞吐量。 吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾回收时间)。
- Serial Old 收集器:Serial 收集器的老年代版本,同样是单线程,使用标记整理算法。
- Parallel Old 收集器:Parallel 收集器的老年代版本,多线程,标记-整理算法实现。
- CMS 收集器: 一种以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现。运作过程分为四个步骤:初始标记-->并发标记-->重新标记-->并发清除
初始标记:标记GC Roots的所有可达对象,速度很快,该阶段是停顿的
并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,耗时长但不需要停顿用户线程,可以与垃圾收集器线程一起并发执行。
重新标记:修正并发标记期间,因用户程序继续运作而导致标记产生变动的那部分对象。该阶段停顿时间通常会比初始标记阶段稍长,但远比并发标记阶段时间短。
并发清除:清理标记阶段判断死亡的对象,由于无需移动存活对象,该阶段也可以与用户线程并发执行。
- Garbage First (G1)收集器: 是一款主要面向服务端应用的垃圾收集器。基于标记-整理算法实现,支持多线程并行。可以预测下一次垃圾回收的停顿时间,尝试在不超过这个时间的前提下进行垃圾回收。G1 收集器将整个Java堆划分为多个小的、固定大小的区域,而非传统的将堆分为新生代和老年代,可以更加灵活地管理内存。
初始标记:标记GC Roots的所有可达对象,速度很快,该阶段是停顿的
并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,耗时长但不需要停顿用户线程,可以与垃圾收集器线程一起并发执行。
最终标记:修正并发标记期间,因用户程序继续运作而导致标记产生变动的那部分对象。该阶段停顿时间通常会比初始标记阶段稍长,但远比并发标记阶段时间短。
筛选回收:计算每个区域的存活对象,选择收益最大的区域进行回收。
CMS和G1主要区别
- 停顿时间:CMS 主要关注减少停顿时间,而 G1 则提供可预测的停顿时间。
- 内存碎片:CMS 使用标记-清除算法容易产生内存碎片,G1 使用标记-整理算法,有效解决该问题。
- 适用区域:G1 更适合大堆内存的垃圾回收。
- 并发执行:CMS 在标记和清除阶段与其他工作线程并发执行,G1 在标记和选择回收区域时与其他工作线程并发执行。
- 配置复杂度:G1 的配置通常比 CMS 更复杂。
JVM调优
使用 jconsole 或 jprofiler 工具查看堆内存使用情况,可以知道是哪些数据占用了大量的内存,对代码进行优化。如果发现内存溢出问题是由于堆内存不足导致的,可以考虑调整JVM的启动参数,如增加堆内存大小(-Xms和-Xmx)。
常见问题
强、软、弱、虚引用
- 强引用:就是平常用到的普通对象,内存不足时,JVM对内存进行垃圾回收,即使内存溢出,也不会将强引用对象回收掉。所以强引用对象是造成内存溢出的主要原因之一。
- 软引用:相对强引用更弱一点的引用,用来描述一些有用但非必须的对象。内存充足时不会被回收,内存不足时会被列入回收范围。通常用在对内存敏感的程序当中,比如高速缓存。
- 弱引用:也是用来描述非必须的对象,但比软引用更弱一些。只能存活到下一次垃圾回收,无论内存是否充足,都会被回收掉。
- 虚引用:最弱的引用,相当于没有引用,随时可能被回收。主要作用是跟踪对象的垃圾回收状态。
一个系统多久触发一次Full GC ?
Full GC(全局垃圾回收)的触发频率完全取决于应用程序的行为和垃圾回收器的配置。
- 堆内存大小:若堆内存较小,可能很快就会填满,导致Full GC频繁发生。
- 老年代内存使用:老年代内存被填满时会触发Full GC。
- 新生代内存使用:若新生代内存频繁填满且大量对象晋升到老年代,也可能导致Full GC。
- 应用程序行为:应用程序创建对象的速度和模式会影响Full GC的频率。
- 内存泄漏:内存泄漏会导致内存使用不断增加,从而增加Full GC的频率。
- 系统负载:系统负载高时,可能无法及时回收垃圾,导致Full GC频率增加。
要减少Full GC的发生频率,可以采取以下措施:
- 优化应用程序,减少内存泄漏。
- 调整JVM堆内存大小,确保有足够的空间供应用程序使用。
- 选择合适的垃圾回收器,并调整其参数以适应应用程序的需求。
- 使用性能分析工具监控内存使用情况,并根据需要进行优化。
常用命令
启动JVM
- java [options] classname:启动JVM并运行指定类
- java [options] -jar filename.jar:启动JVM并运行指定的jar文件
查看JVM版本信息
- java -version
基本配置选项
- -Xms:设置JVM初始堆内存大小
- -Xmx:设置JVM可以使用的最大堆内存大小
- -Xss:设置每个线程的堆栈大小
- -XX:MetaspaceSize=:设置元空间初始大小
- -XX:MexMetaspaceSize=:设置元空间的最大大小
JVM性能调优选项
- -XX:NewRatio=值:设置新生代和老年代的大小比值
- -XX:SurvivorRatio=值:设置Eden区和Survivor区的大小比值
- -XX:MaxTenuringThreshold=值:设置对象晋升到老年代的年龄阈值
JVM日志和监控
- -XX:+PrintGCDetails:打印详细的垃圾回收日志
- -Xdebug:启动JVM时开启调试支持