文章目录
- JVM组成
- JVM由哪些部分组成,运行流程是什么?
- 运行时数据区域
- 什么是程序计数器?
- 什么是Java堆?为什么使用元空间替换了永久代?
- 什么是虚拟机栈? 堆栈的区别是什么?
- 什么是本地内存?方法区? 常量池?
- 什么是直接内存?
- 运行时数据区 组成部分(jvm内存划分):堆、方法区、栈、本地方法栈、程序计数器分别是干什么的?
- java对象的分配(逃逸分析)
- 类加载器
- 什么是类加载器,类加载器有哪些?
- 什么是双亲委派模型?为什么JVM采用它?什么时候需要打破它?如何打破?
- 类加载的执行过程(类的生命周期)?
- Tomcat中为什么要使用自定义类加载器?
- 类加载器常见异常
- 垃圾回收
- GC是什么?为什么要GC?
- 对象什么时候可以被垃圾器回收? 引用计数法 可达性分析算法 三色标记法
- JVM 垃圾回收算法有哪些?标记清除算法 复制算法 标记整理算法
- 分代收集算法是什么?为什么8:1:1划分?分代年龄为什么是15次?
- Java堆为什么这样划分?选择不同的垃圾回收算法的目的是什么?
- 为什么对于年轻代采用复制算法 老年代采取标记整理法?
- JVM 有哪些垃圾回收器?
- 频繁Young GC(Minor GC)的原因和解决办法?Full GC呢?
- 强引用、软引用、弱引用、虚引用的区别?
- JVM调优
- JVM 调优的参数都有哪些?
- JVM 调优的参数可以在哪里设置参数值?
- JVM 调优的工具有哪些?
- java内存泄露的排查思路?
- CPU飙高排查方案与思路?
JVM组成
JVM由哪些部分组成,运行流程是什么?
JVM,Java Virtual Machine,Java程序的运行环境(java二进制字节码的运行环境)
好处: 一次编写,到处运行(跨系统);自动内存管理,垃圾回收机制
JVM 的主要组成部分:
- ClassLoader(类加载器)
- Runtime Data Area(运行时数据区,内存分区)
- Execution Engine(执行引擎)
- Native Method Library(本地库接口)
运行流程: - Java编译器(javac):Java编译器负责将Java源代码(.java文件)编译成字节码文件(.class文件)。
- 类加载器(ClassLoader):类加载器的任务是将这些字节码文件加载到JVM的运行时数据区。
- 运行时数据区(Runtime Data Area):运行时数据区是JVM的一部分,它负责在内存中存储加载进来的字节码文件。
- 执行引擎(Execution Engine):执行引擎负责解释或编译字节码为底层系统指令,然后交由CPU执行。在需要的时候,执行引擎会调用本地方法库(Native Method Library)来实现某些功能。
运行时数据区域
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区 (jdk1.8改为了元空间)
- 直接内存 (非运行时数据区的一部分)
什么是程序计数器?
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
什么是Java堆?为什么使用元空间替换了永久代?
它是线程共享的区域(会有线程安全问题):主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError(OOM)异常。
下面以JVM的运行数据区来介绍:
- 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区(幸存者区),根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。
- 老年代主要保存生命周期长的对象,一般是一些老的对象
- 元空间保存的类信息、静态变量、常量、即时编译后的代码
为了避免方法区出现OOM,所以在java8中将堆上的方法区【永久代】给移动到了本地内存上,重新开辟了一块空间,叫做元空间(详细的回答下面有写)。那么现在就可以避免掉OOM的出现了。
为什么使用元空间替换了永久代?
主要有三个方面的原因:
- 在1.7版本里的永久代内存是有上限的,虽然可以通过-XX:PermSize参数来设置,但是JVM加载的类总数大小是很难确定的,所以很容易出现OOM的问题。但是元空间是存储在本地内存里,内存上限比较大,可以很好避免这个问题;
- 永久代的对象是通过Full GC(下面会介绍)和老年代同时进行垃圾回收的,替换成元空间以后简化了Full GC的过程,可以在不进行暂停的情况下去并发的释放类的数据,同时也提升了GC的性能;
- Oracle要合并HotSpot JVM和JRockit VM的代码,而 JRockit VM里是没有永久代的
什么是虚拟机栈? 堆栈的区别是什么?
Java Virtual machine Stacks (java 虚拟机栈)
每个线程运行时所需要的内存(线程安全),称为虚拟机栈,先进后出
每个栈由多个栈帧(frame)组成,因为方法可能调用多个其他方法,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
1. 垃圾回收是否涉及栈内存?
垃圾回收主要是回收堆内存,当方法调用完毕栈帧弹栈以后,内存就会自动释放
2. 栈内存分配越大越好吗?
未必,默认的栈内存通常为1024k,栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程,数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半
3. 方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的;如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
4. 什么情况会导致栈内存溢出?
- 栈帧过多导致栈内存溢出,典型问题:递归调用
- 栈帧过大导致栈内存溢出
5. 堆和栈的区别是什么?
- 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收,而栈不会。
- 栈内存是线程私有的,而堆内存是线程共有的。
- 两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常。栈空间不足:java.lang.StackOverFlowError。堆空间不足:java.lang.OutOfMemoryError。
什么是本地内存?方法区? 常量池?
本地内存并不是JVM运行时数据区的一部分,它主要指的是JVM在本地机器上的内存分配,包括直接内存的分配等。本地内存的管理是不受JVM控制的。
**方法区(Method Area)**是各个线程共享的内存区域;主要存储类的信息、运行时常量池;虚拟机启动的时候创建,关闭虚拟机时释放;如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError:Metaspace
常量池可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
什么是直接内存?
不受 JVM 内存回收管理,是虚拟机的系统内存,常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JVM 内存回收管理。
运行时数据区 组成部分(jvm内存划分):堆、方法区、栈、本地方法栈、程序计数器分别是干什么的?
1、堆解决的是对象实例存储的问题,垃圾回收器管理的主要区域。是Java运行时数据区中最大的一块。它是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
2、方法区可以认为是堆的一部分,也是是各个线程共享的内存区域,用于存储已被虚拟机加载的信息,常量、静态变量、即时编译器编译后的代码。
3、栈解决的是程序运行的问题,栈里面存的是栈帧,栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息。
4、本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
5、程序计数器(PC寄存器)程序计数器中存放的是当前线程所执行的字节码的行数。JVM工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。
java对象的分配(逃逸分析)
在Java中,对象的分配是指在程序运行时为对象实例分配内存空间的过程。Java的对象分配通常包括两个阶段:栈上分配和堆上分配。而逃逸分析则是Java虚拟机的一项优化技术,用于分析对象的作用域,以决定对象是在栈上分配还是堆上分配。
栈上分配
栈上分配是指将对象分配在线程的栈上,而不是在堆上。这种分配方式通常发生在对象的作用域被限制在方法内部且不会逃逸出方法的情况下。
栈上分配的好处是对象的生命周期与方法的生命周期相同,当方法退出时,对象会自动被销毁,不需要进行垃圾回收,从而提高了性能。
栈上分配的对象不能被多个线程共享,因为每个线程都有自己的栈空间。
堆上分配
堆上分配是指将对象分配在堆内存中,这是最常见的对象分配方式。当对象的作用域超出了方法的范围,或者对象需要在多个方法之间共享时,通常会在堆上分配对象。
堆上分配的对象由垃圾回收器来管理其生命周期,当对象不再被引用时,会被自动回收。
堆上分配的对象可以被多个线程共享,因为堆是线程共享的内存区域。
逃逸分析
逃逸分析是Java虚拟机的一项优化技术,用于分析对象的作用域,以确定对象是否会逃逸出方法的范围。如果对象不会逃逸出方法,那么可以将其分配在栈上,否则将其分配在堆上。
对于不逃逸的对象,可以通过栈上分配来避免垃圾回收的开销,从而提高程序的性能。
对于逃逸的对象,虚拟机会将其分配在堆上,以保证对象在方法外部仍然可以被访问到。
逃逸分析可以帮助Java虚拟机进行更精细的内存分配,提高程序的性能和资源利用率。
类加载器
还是这张图,首先要知道对于一个Java文件,它从编译到执行的整个过程。
类加载器:用于装载字节码文件(.class文件)
运行时数据区:用于分配存储空间
执行引擎:执行字节码文件或本地方法
垃圾回收器:用于对JVM中的垃圾内容进行回收
什么是类加载器,类加载器有哪些?
JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。现有的类加载器基本上都是java.lang.ClassLoader的子类,该类的主要职责就是用于将class文件内容加载到JVM中生成类对象。
类加载器根据各自加载范围的不同,划分为四种类加载器:
- 启动类加载器(BootStrap ClassLoader):该类并不继承ClassLoader类,其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。
- 扩展类加载器(ExtClassLoader):该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。
- 应用类加载器(AppClassLoader):该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。
- 自定义类加载器:开发者自定义类继承ClassLoader,实现自定义类加载规则。
上述三种类加载器的层次结构如下如下:
类加载器的体系并不是“继承”体系,而是委派体系,类加载器首先会到自己的parent中查找类或者资源,如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次。
什么是双亲委派模型?为什么JVM采用它?什么时候需要打破它?如何打破?
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载。
为什么使用双亲委派?
(1)通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
(2)为了安全,保证类库API不会被修改,下面举例说明
在工程中新建java.lang包,接着在该包下新建String类,并定义main函数
public class String {public static void main(Stri