JVM
1. JVM内存模型:堆、栈、程序计数器、方法区
- 堆:线程公有。是JVM中最大的一部分,用于存放new创建的对象,GC回收主要回收的就是堆的内存,堆又可以分为老年代和年轻代
- 年轻代:用于存放新生对象和短期存活的对象,年轻代又可以分为eden区、FS区、TS区。其中大部分对象会直接进入eden区,当eden区满了之后,会执行Minorgc,Minorgc会发生STW,停止用户线程,回收年轻代的内存空间。如果经过多次gc后依旧存活的对象,JVM默认设定为15次,会移动到老年代
- 老年代用于存放大对象和长期存活的对象,大对象会直接进入老年代,当老年代满了之后,会执行Fullgc,回收堆和方法区的内存
- MinorGC:回收年轻代的空间
- MajorGC:回收老年代的空间
- Fullgc:回收堆 + 方法区的内存,堆进行垃圾回收,方法区进行类卸载。
- system.gc()执行时
- 老年代空间不足时
- 方法区空间不足时
- 空间担保机制
- 为什么分为老年代和年轻代:
- 针对不同对象的存活时间不同分别进行管理,比如年轻代大部分对象其实是短期存活的对象,频繁执行Minorgc,对象使用完之后能够及时被回收,释放内存。而老年代的对象是长期存活的或者是大对象,不需要经常进行gc回收。如果将全部对象放在一起,每次gc都需要遍历整个堆空间,回收效率低
- 堆会发生OOM
- 栈:线程私有。分为Java虚拟机栈和本地方法栈
- Java虚拟机栈:对应Java方法,每当有新线程创建时就会分配一个栈空间,线程结束后栈空间被回收,创建一个栈帧,存放局部变量表,局部变量表上存放8种基本数据类型和对象的引用指针。Java虚拟机栈会发生SOF和OOM
- 本地方法栈:对应system本地方法,同样会发生SOF和OOM
- 程序计数器:线程私有。每个线程都有一个程序计数器,是当前线程在字节码文件的行号指示器,单线程下可有可无,多线程下发生线程切换时,程序计数器会记录当前线程在字节码文件执行的行号位置,以便线程再次获得调度时能快速恢复状态并继续执行
- 方法区:线程公有。存放静态变量、静态方法、常量。运行时常量池也存放在方法区。1.8之前使用永久代实现,1.8之后使用元空间实现,元空间不属于JVM内存
- 方法区也会参与名为类卸载的gc回收,当堆中没有该类的实例、加载该类的类加载器已经被回收、类的class对象被回收后,可以进行类卸载
- 方法区也会发生OOM
2. 哪些区域可能发生OOM:堆、栈、方法区
- 堆
- 堆内存不足,通过jmap查看堆内存使用情况,使用-Xmx和-Xms调整堆内存大小
- 内存泄漏,存在未被回收的对象,使用可达性分析查找回收
- 栈
- 运行时大量创建线程,Java虚拟机栈内存不足以为线程分配栈空间导致OOM,可以减少使用线程或者-Xss调整线程大小
- 此外Java虚拟机栈还会发生SOF栈溢出,当递归深度超过JVM的规定时,发生SOF
- 方法区
- 运行时大量创建类,比如反射,类信息会存放在方法区,导致方法区内存不足
- 1.8之前使用永久代实现,通过-XX:MaxPermSize调整
- 1.8之后使用元空间实现,通过-XX:MetaSpaceSize调整
3. 类加载的时机:5个
- new实例化
- 反射获取class对象
- main方法的类最先加载
- 访问类的静态变量和静态方法时,类未实例化
- 初始化子类时,父类未被初始化
4. 类加载过程:加载、连接、初始化
- 加载:将字节码文件加载到JVM中,将静态变量、常量、静态方法加载到方法区,生产一个class对象
- 连接
- 验证:检查字节码文件是否符合JVM规范,防止出现错误
- 准备:为静态变量分配内存空间并赋初值,主要是分配空间
- 解析:将常量的符号引用转换为直接引用
- 初始化:真正为静态变量分配初值
5. 类加载器
- BootstrapClassLoader,启动类加载器,加载lib目录下rt.jar
- ExtensionClassLoader,拓展类加载器,加载lib目录ext文件夹
- ApplicationClassLoader,应用类加载器,加载用户类路径下的类
- 自定义的类加载
6. 双亲委派
- 过程:当一个类加载器接到加载任务时,先检查这个类是否已经被加载,如果是,返回class对象,否则将加载任务交给父加载器进行加载,当父加载器无法加载时,才会由子加载器进行加载
- 好处:
- 沙盒安全机制:防止用户自定义的类覆盖核心基础类
- 避免类的重复加载
- 破坏:重写loadclass方法。保护:添加findclass方法
7. GC回收
1. 标记阶段
- 引用计数法:每个对象都有一个引用计数器,有引用计数+1,释放引用计数-1,0表示可以回收。
- 优点:实时计算,效率高
- 缺点:实时计算,时间开销大,需要一个计数器,有额外的空间开销。无法解决循环引用问题
- 可达性分析:从GCRoots开始,当一个对象进入到GCRoots中没有任何一条引用链与之相连时,表明该对象可以被回收
- 优点:解决了循环引用问题
- 缺点:效率比引用计数法低
- GCRoots:Java虚拟机栈和本地方法栈的对象、方法区的静态变量和常量
2. finalized()方法
- 对象会经历两次标记过程才能被认定为可回收,第一次由GCRoots进行标记,第二次在finalized()方法中进行,如果执行finalized()之后对象还是没有任何一条引用链与GCRoots相连,就是可以被回收
3. 对象的引用类型
- 强引用:通过new创建的对象,不会被回收
- 软引用:非必须的对象,内存不足时被回收
- 弱引用:非必须的对象,下一次GC时被回收
- 虚引用:可有可无的对象,随时都会被回收
4. 清除阶段
- 标记清除法:先对需要存活的对象进行标记,标记完成后清除没有被标记的对象
- 缺点:如果堆中存在大量的对象,需要遍历整个堆,标记和清除的效率低,清除过程会产生空间碎片
- 复制法:将内存分成大小相等的两块,先使用其中一块,当这一块的内存满了之后,对需要存活的对象进行标记,清除没有标记的对象,然后将存活的对象移动到另一块内存
- 优点:不会产生空间碎片,复制法用于年轻代,需要标记的对象少,执行效率高
- 缺点:内存的利用率低
- 标记整理法:先对需要存活的对象进行标记,标记完成后将他们移动到一边,回收边界外的内存
- 优点:不会产生空间碎片
- 缺点:用于老年代,需要标记的对象多并且存在大对象,效率低
- 分代收集法:hotspot虚拟机针对对象的不同存活时间采用不同的回收策略
- 年轻代的对象生命周期短,回收频繁,使用复制算法
- 老年代的对象生命周期长并且存在大对象,回收不频繁,采用标记清除和标记整理
4. GC收集器
- CMS并发标记清除
- 四个阶段:初始标记、并发标记、重新标记、并发清除
- 其中初始标记和重新标记会STW
- 优点:并发标记清除,效率高
- 缺点:基于标记清除算***产生空间碎片
- G1并行并发不分代,基于标记整理法,将堆内存分成大小相等的区域,进行筛选回收
- 四个阶段:初始标记、并发标记、最终标记、筛选清除
- 其中初始标记和最终标记会STW
- 优点:并行并发效率高,基于标记整理法,不会产生空间碎片问题
5. 为什么会STW
- 确保引用的地址能正确更新,因此GC时可能会设计对象的移动,这时需要先将用户线程停止,等待GC完成之后再重新指向
- 防止一直处于标记阶段,如果一边标记一边产生垃圾,GC线程无法完成标记工作,不能进行清除
6. System.gc()一定会执行吗
- System.gc()会显示触发FullGC,但是不能保证一定会执行
- 一般情况下不需要手动调用System.gc()进行GC回收
- 可以调用System.runFinalization()方法强制执行对象的第二次标记,然后再执行一次System.gc()就可以
7. 内存分配原则和空间担保机制
- 内存分配原则:大部分对象会进入年轻代的eden区、大对象直接进入老年代、长期存活的对象进入老年代
- 空间担保机制:在年轻代进行MinorGC之前,先检查老年代的内存是否足够存放年轻代所有的对象,如果不够,先进行FullGC
8. JVM参数
- -Xms:堆初始大小
- -Xmx:堆最大大小
- -XX:NewRatio:年轻代和老年代的比例,默认是1:2
- -XX:SurvivorRatio:eden区、FS区、TS区的比值,默认是8:1:1
- -Xss:调整线程大小
- -XX:MetaSpaceSize
- -XX:PermSize
- -XX:MaxTeruningThreshold
8. jvm故障处理工具:jps、jmap、jstack
- jps:查看虚拟机进程,显示当前正在运行的虚拟机进程和进程ID
- jmap:查看堆内存使用情况和堆所使用的gc收集器。可用于解决堆的oom问题
- jstack:查看进程的线程快照,可以定位死锁问题
查看1道真题和解析