jvm概念和运行过程
jvm是java的虚拟机位于操作系统层之上,应用程序层之下,所以才具有跨平台能力,JAVA文件需要通过JVM转译成字节码或通过javac命令编译为.class文件后才能运行JAVA程序,运行时必须要有JRE(运行环境),JDK是开发包,其中包含有JRE。
jvm组成
JVM 结构主要分为三个部分:类加载器,运行时数据区,执行引擎
类加载器(Class Loader):
类加载器负责将类文件加载到 JVM 中运行时数据区中的方法区/元空间。JVM 内置了三个主要的类加载器:
启动类加载器:负责加载 Java 核心类库,是 JVM 自带的类加载器,其中启动类加载器BootstrapClassLoader是native方法,C语言实现的
扩展类加载器:负责加载 JVM 扩展目录中的 JAR 文件。
应用程序类加载器:负责加载应用程序类路径(Classpath)上的类。
执行引擎(Execution Engine):
执行引擎负责执行字节码。常见的执行引擎有两种:解释器,即时编译器
解释器:逐行解释执行字节码,执行效率较低。
即时编译器:将字节码一次性编译成本地机器码指令,生成可执行文件,执行效率更高。
运行时数据区(Runtime Data Area):方法区,堆,栈,程序计数器
方法区/元空间
方法区/元空间/非堆区/堆外内存都是一个意思,主要存储类信息、类的静态变量,常量,运行时常量池,他是jdk8开始用来代替之前堆里的永久代,元空间使用的是本地内存,可以动态调整内存大小,避免永久代内存溢出问题,
方法区相关总结
1.类的信息以及类的静态变量和常量直接存储在方法区的,没有存储在常量池
2.类信息包括类的名称、访问修饰符、字段、方法、父类、接口等信息。
3.方法区信息是各个线程共享的。
4.方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类也会导致方法区溢出。关闭 JVM 就会释放这个区域的内存。
拓展
1.可以通过String类的intern()方法将字符串放入字符串常量池中。
2.字面量
int a=1; // 这个1便是字面量
String b="iloveu"; // iloveu便是字面量
3.符号引用
定义:引用其他类的地址如:com.javabc.Quest就是符号引用
作用:由于在编译过程中并不知道每个类的地址,因为可能这个类还未加载,所以如果在一个类中引用了另一个类,被引用的类的全限定类名(com.javabc.Quest)会作为符号引用,在类加载完后用这个符号引用去获取它的内存地址
4.运行时常量池:是所有常量池的运行时表示形式,包括类常量池(主要存放字面量和符号引用)、方法常量池,字符串常量池等,
5.常量池作用:为了提高运行效率,减少内存的重复占用,并且为了方便JVM对程序的解释和执行。
堆:
一个JVM只有一个堆,用于存储对象实例。堆内存的大小可以通过参数调整,垃圾收集器扫描的主要区域就是中存储对象的实例,如果堆内存满了,并且垃圾收集器释放不了任何对象,就会OOM
jdk8后堆结构
jdk1.7及之前堆结构分为新生代(Eden,s0,s1),老年代,永久代,
jdk8后永久代被数据区中的元空间代替,就只有新生代(Eden,s0,s1)和老年代
新生代Eden:被创建的对象实例一开始都在Eden,Eden与S0,S1的默认比例是 8 :1 : 1
新生代S0(from区):如果某个对象活过了第一次垃圾收集,那么就会加入S0
新生代S1(To区):GC收集垃圾的复制算法时,会复制S0存活的对象实例到S1,然后在清除整个S0
老年代:
对象每活过一次GC,年龄都会+1,一旦年龄达到了老年代的门槛(15次GC没被回收),就会加入老年代
当老年代满了之后,JVM就会出发FullGC,扫描整个堆进行垃圾回收,一般情况下只会进行轻量级GC,minor GC
拓展
永久代:
JDK8之前叫永久代,JDK8之后叫元空间,他们的区别就是元空间在直接内存中,不在JVM运行时内存中
JDK8后常量池转移到了元空间中
JDK1.7开始常量池已经在堆中了
主要存储JDK自身携带的class对象,interface元数据,运行时环境
当出现OOM时导出DUMP文件,Dump文件可以在项目路径下看到,后缀为.hprof使用工具分析
栈(先进后出):
栈是先进后出的,并且是线程私有的,每个线程执行时都会创建一个栈,每个方法执行时都会在栈里创建一个栈帧,用来存储局部变量、方法调用和方法返回地址。
虚拟机栈:
虚拟机栈执行的是 java 方法,
本地方法栈:
本地方法栈执行的是 native 方法,是用其他语言(如 C、C++)编写的,通过 Java Native Interface(JNI)调用
栈相关问题:
1.如果当前线程栈中有方法递归调用或方法调用链过长,超过了栈的深度限制可能会抛出栈溢出 (StackOverFlowError)异常,
2.如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存也会报OOM异常
2.虚拟机栈和本地方法栈都是栈,具有相同的特性,只是执行的方法类型不同
3.栈的大小可以在启动时固定,也可以动态地分配和扩展。
4.栈不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。
程序计数器:
每个线程都有自己的程序计数器,用于记录当前线程正在执行的 Java 字节码指令的地址。他可以确保线程切换后能够继续执行原来的代码,也用于记录方法之间的跳转信息,例如方法调用和返回地址。
类加载过程
JVM 的类加载过程包括加载,验证、准备、解析和初始化五个阶段。
加载:
加载阶段是由类加载器将类的字节码文件(.class文件)从磁盘加载到内存中,
验证:
验证阶段主要保证安全问题。
主要验证文件格式是否符合Class格式规范,字节码中的(类、字段、方法)是否符合JVM规范,以及方法调用参数和返回类型是否正确,还有符号引用验证是否规范
准备:
准备阶段jvm会给类的静态变量分配内存并设置初始值(默认零值),静态变量会分配在方法区中。
解析:
符号引用是一种符号来描述所引用的目标,解析阶段将类、接口、字段和方法的符号引用解析为直接引用,是可以直接定位到目标的指针、句柄或偏移量,也就是和实际的内存地址关联起来。
初始化:
初始化阶段是对类的静态变量进行初始化,以及执行静态代码块。表示类准备好被使用了。
双亲委派
双亲委派是Java类加载机制中的一种机制,用于保证类的唯一性和安全性。确保在不同类加载器中加载的类不会互相冲突。避免恶意同名类的加载
过程:
当一个类加载器(称为子类加载器)需要加载一个类时,它首先不会自己尝试加载,而是将请求委托给其父加载器(称为父类加载器)去完成。
父类加载器也会按照相同的方式,首先委托给它的父加载器,以此类推,一直到顶层的启动类加载器(Bootstrap ClassLoader)。
如果父加载器无法找到所需的类,子类加载器才会尝试加载。
垃圾回收算法:
JVM 自动管理垃圾回收,主要回收不再被引用的对象,释放内存空间。
复制算法 (新生代):
主要用于新生代的垃圾回收,因为生命周期较短,适合用复制算法来回收,将堆分为两个区域,每次只使用其中一个区域。垃圾回收时,将存活的对象复制到另一个区域,然后清空原区域。
新生代GC触发机制:当新生代满(Survivor 满不会引发 GC)时就会触发新生代GC(通过复制算法回收垃圾)
标记-整理算法 (一般是老年代):
主要用于老年代的垃圾回收。
在标记阶段,标记存活对象。然后将存活对象向一端移动,最后清除边界以外的内存,保持存活对象的连续性。
标记-清除算法 (一般是老年代):
这个算法是基本的垃圾回收算法,但不常用于新生代和老年代的垃圾回收。
在标记阶段,它会标记所有存活的对象。在清除阶段,它会清除所有未标记的对象。
老年代GC触发机制:一般在进行老年代GC 前都先进行了一次 新生代GC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。或者当空间不够分配给新创建的对象时也会触发老年代GC 进行垃圾回收
老年代的对象生命周期较长, MajorGC比较费时,所以也不会频繁执行,也可以通过手动调用System.gc()来触发。
JVM调优
堆内存调优:
-Xms: 设置堆的初始大小。
-Xmx: 设置堆的最大大小。
-XX:NewRatio: 设置新生代(Young Generation)与老年代(Old Generation)的大小比例。
-XX:MaxNewSize :设置新生代的最大大小和。
-XX:MaxTenuringThreshold:设置升级到老年代的门槛次数
垃圾回收调优:
-XX:+UseSerialGC, -XX:+UseParallelGC, -XX:+UseConcMarkSweepGC, -XX:+UseG1GC: 选择不同的垃圾回收器。
-XX:SurvivorRatio: 设置新生代 Eden 区和 Survivor 区的比例。
-XX:MaxGCPauseMillis: 设置垃圾回收最大暂停时间目标。
栈和线程调优:
-Xss: 设置线程栈的大小。
-XX:ThreadStackSize: 设置线程栈大小的通用参数。
JIT 编译器调优:
-XX:+TieredCompilation: 启用分层编译。
-XX:CompileThreshold: 设置方法被 JIT 编译的调用次数阈值。
使用工具分析应用程序性能:
使用 Java 性能分析工具(如 VisualVM、JProfiler、YourKit)来分析应用程序的性能瓶颈和资源利用情况。
GC 日志与分析:
使用 -XX:+PrintGC、-XX:+PrintGCDetails 等参数打印 GC 日志,以便分析垃圾回收情况。