wcm 可以做网站吗,开发公司租赁机械车位价格,如乐网站,网络营销类网站1 JVM组成
1.1 JVM由那些部分组成#xff0c;运行流程是什么#xff1f; 难易程度#xff1a;☆☆☆ 出现频率#xff1a;☆☆☆☆ JVM是什么
Java Virtual Machine Java程序的运行环境#xff08;java二进制字节码的运行环境#xff09;
好处#xff1a; 一次编写运行流程是什么 难易程度☆☆☆ 出现频率☆☆☆☆ JVM是什么
Java Virtual Machine Java程序的运行环境java二进制字节码的运行环境
好处 一次编写到处运行 自动内存管理垃圾回收机制 JVM由哪些部分组成运行流程是什么 从图中可以看出 JVM 的主要组成部分
ClassLoader类加载器Runtime Data Area运行时数据区内存分区Execution Engine执行引擎Native Method Library本地库接口
运行流程
1类加载器ClassLoader把Java代码转换为字节码
2运行时数据区Runtime Data Area把字节码加载到内存中而字节码文件只是JVM的一套指令集规范并不能直接交给底层系统去执行而是有执行引擎运行
3执行引擎Execution Engine将字节码翻译为底层系统指令再交由CPU执行去执行此时需要调用其他语言的本地库接口Native Method Library来实现整个程序的功能。
1.2 什么是程序计数器 难易程度☆☆☆ 出现频率☆☆☆☆ 程序计数器线程私有的内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。 javap -verbose xx.class 打印堆栈大小局部变量的数量和方法的参数。 java虚拟机对于多线程是通过线程轮流切换并且分配线程执行时间。在任何的一个时间点上一个处理器只会处理执行一个线程如果当前被执行的这个线程它所分配的执行时间用完了【挂起】。处理器会切换到另外的一个线程上来进行执行。并且这个线程的执行时间用完了接着处理器就会又来执行被挂起的这个线程。
那么现在有一个问题就是当前处理器如何能够知道对于这个被挂起的线程它上一次执行到了哪里那么这时就需要从程序计数器中来回去到当前的这个线程他上一次执行的行号然后接着继续向下执行。
程序计数器是JVM规范中唯一一个没有规定出现OOM的区域所以这个空间也不会进行GC。
1.3 你能给我详细的介绍Java堆吗? 难易程度☆☆☆ 出现频率☆☆☆☆ 线程共享的区域主要用来保存对象实例数组等当堆中没有内存空间可分配给实例也无法再扩展时则抛出OutOfMemoryError异常。 年轻代被划分为三部分Eden区和两个大小严格相同的Survivor区根据JVM的策略在经过几次垃圾收集后任然存活于Survivor的对象将被移动到老年代区间。老年代主要保存生命周期长的对象一般是一些老的对象元空间保存的类信息、静态变量、常量、编译后的代码
为了避免方法区出现OOM所以在java8中将堆上的方法区【永久代】给移动到了本地内存上重新开辟了一块空间叫做元空间。那么现在就可以避免掉OOM的出现了。 元空间(MetaSpace)介绍
在 HotSpot JVM 中永久代 ≈ 方法区中用于存放类和方法的元数据以及常量池比如Class 和 Method。每当一个类初次被加载的时候它的元数据都会放到永久代中。
永久代是有大小限制的因此如果加载的类太多很有可能导致永久代内存溢出即OutOfMemoryError为此不得不对虚拟机做调优。
那么Java 8 中 PermGen 为什么被移出 HotSpot JVM 了
官网给出了解释http://openjdk.java.net/jeps/122
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力因为JRockit没有永久代不需要配置永久代。1由于 PermGen 内存经常会溢出引发OutOfMemoryError因此 JVM 的开发者希望这一块内存可以更灵活地被管理不要再经常出现这样的 OOM。
2移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合因为 JRockit 没有永久代。
准确来说Perm 区中的字符串常量池被移到了堆内存中是在 Java7 之后Java 8 时PermGen 被元空间代替其他内容比如类元信息、字段、静态属性、方法、常量等都移动到元空间区。比如 java/lang/Object 类元信息、静态属性 System.out、整型常量等。
元空间的本质和永久代类似都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于元空间并不在虚拟机中而是使用本地内存。因此默认情况下元空间的大小仅受本地内存限制。
1.4 什么是虚拟机栈 难易程度☆☆☆ 出现频率☆☆☆☆ Java Virtual machine Stacks (java 虚拟机栈) 每个线程运行时所需要的内存称为虚拟机栈先进后出 每个栈由多个栈帧frame组成对应着每次方法调用时所占用的内存 每个线程只能有一个活动栈帧对应着当前正在执行的那个方法 垃圾回收是否涉及栈内存 垃圾回收主要指就是堆内存当栈帧弹栈以后内存就会释放 栈内存分配越大越好吗 未必默认的栈内存通常为1024k 栈帧过大会导致线程数变少例如机器总内存为512m目前能活动的线程数则为512个如果把栈内存改为2048k那么能活动的栈帧就会减半 方法内的局部变量是否线程安全 如果方法内局部变量没有逃离方法的作用范围它是线程安全的 如果是局部变量引用了对象并逃离方法的作用范围需要考虑线程安全 比如以下代码
栈内存溢出情况 栈帧过多导致栈内存溢出典型问题递归调用 栈帧过大导致栈内存溢出 难易程度☆☆☆ 出现频率☆☆☆ 组成部分堆、方法区、栈、本地方法栈、程序计数器
1、堆解决的是对象实例存储的问题垃圾回收器管理的主要区域。 2、方法区可以认为是堆的一部分用于存储已被虚拟机加载的信息常量、静态变量、即时编译器编译后的代码。 3、栈解决的是程序运行的问题栈里面存的是栈帧栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息。 4、本地方法栈与栈功能相同本地方法栈执行的是本地方法一个Java调用非Java代码的接口。 5、程序计数器PC寄存器程序计数器中存放的是当前线程所执行的字节码的行数。JVM工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。
1.5 能不能解释一下方法区 难易程度☆☆☆ 出现频率☆☆☆ 1.5.1 概述 方法区(Method Area)是各个线程共享的内存区域 主要存储类的信息、运行时常量池 虚拟机启动的时候创建关闭虚拟机时释放 如果方法区域中的内存无法满足分配请求则会抛出OutOfMemoryError: Metaspace 1.5.2 常量池
可以看作是一张表虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
查看字节码结构类的基本信息、常量池、方法定义javap -v xx.class
比如下面是一个Application类的main方法执行源码如下
public class Application {public static void main(String[] args) {System.out.println(hello world);}
}找到类对应的class文件存放目录执行命令javap -v Application.class 查看字节码结构
D:\code\jvm-demo\target\classes\com\heima\jvmjavap -v Application.class
Classfile /D:/code/jvm-demo/target/classes/com/heima/jvm/Application.classLast modified 2023-05-07; size 564 bytes //最后修改的时间MD5 checksum c1b64ed6491b9a16c2baab5061c64f88 //签名Compiled from Application.java //从哪个源码编译
public class com.heima.jvm.Application //包名类名minor version: 0major version: 52 //jdk版本flags: ACC_PUBLIC, ACC_SUPER //修饰符
Constant pool: //常量池#1 Methodref #6.#20 // java/lang/Object.init:()V#2 Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;#3 String #23 // hello world#4 Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V#5 Class #26 // com/heima/jvm/Application#6 Class #27 // java/lang/Object#7 Utf8 init#8 Utf8 ()V#9 Utf8 Code#10 Utf8 LineNumberTable#11 Utf8 LocalVariableTable#12 Utf8 this#13 Utf8 Lcom/heima/jvm/Application;#14 Utf8 main#15 Utf8 ([Ljava/lang/String;)V#16 Utf8 args#17 Utf8 [Ljava/lang/String;#18 Utf8 SourceFile#19 Utf8 Application.java#20 NameAndType #7:#8 // init:()V#21 Class #28 // java/lang/System#22 NameAndType #29:#30 // out:Ljava/io/PrintStream;#23 Utf8 hello world#24 Class #31 // java/io/PrintStream#25 NameAndType #32:#33 // println:(Ljava/lang/String;)V#26 Utf8 com/heima/jvm/Application#27 Utf8 java/lang/Object#28 Utf8 java/lang/System#29 Utf8 out#30 Utf8 Ljava/io/PrintStream;#31 Utf8 java/io/PrintStream#32 Utf8 println#33 Utf8 (Ljava/lang/String;)V
{public com.heima.jvm.Application(); //构造方法descriptor: ()Vflags: ACC_PUBLICCode:stack1, locals1, args_size10: aload_01: invokespecial #1 // Method java/lang/Object.init:()V4: returnLineNumberTable:line 3: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/heima/jvm/Application;public static void main(java.lang.String[]); //main方法descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack2, locals1, args_size10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String hello world5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 7: 0line 8: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 args [Ljava/lang/String;
}
SourceFile: Application.java下图左侧是main方法的指令信息右侧constant pool 是常量池
main方法按照指令执行的时候需要到常量池中查表翻译找到具体的类和方法地址去执行 1.5.3 运行时常量池
常量池是 *.class 文件中的当该类被加载它的常量池信息就会放入运行时常量池并把里面的符号地址变为真实地址 1.6 你听过直接内存吗 难易程度☆☆☆ 出现频率☆☆☆ 不受 JVM 内存回收管理是虚拟机的系统内存常见于 NIO 操作时用于数据缓冲区分配回收成本较高但读写性能高不受 JVM 内存回收管理
举例
需求在本地电脑中的一个较大的文件超过100m从一个磁盘挪到另外一个磁盘 代码如下
/*** 演示 ByteBuffer 作用*/
public class Demo1_9 {static final String FROM E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4;static final String TO E:\\a.mp4;static final int _1Mb 1024 * 1024;public static void main(String[] args) {io(); // io 用时1535.586957 1766.963399 1359.240226directBuffer(); // directBuffer 用时479.295165 702.291454 562.56592}private static void directBuffer() {long start System.nanoTime();try (FileChannel from new FileInputStream(FROM).getChannel();FileChannel to new FileOutputStream(TO).getChannel();) {ByteBuffer bb ByteBuffer.allocateDirect(_1Mb);while (true) {int len from.read(bb);if (len -1) {break;}bb.flip();to.write(bb);bb.clear();}} catch (IOException e) {e.printStackTrace();}long end System.nanoTime();System.out.println(directBuffer 用时 (end - start) / 1000_000.0);}private static void io() {long start System.nanoTime();try (FileInputStream from new FileInputStream(FROM);FileOutputStream to new FileOutputStream(TO);) {byte[] buf new byte[_1Mb];while (true) {int len from.read(buf);if (len -1) {break;}to.write(buf, 0, len);}} catch (IOException e) {e.printStackTrace();}long end System.nanoTime();System.out.println(io 用时 (end - start) / 1000_000.0);}
}可以发现使用传统的IO的时间要比NIO操作的时间长了很多了也就说NIO的读性能更好。
这个是跟我们的JVM的直接内存是有一定关系如下图是传统阻塞IO的数据传输流程 下图是NIO传输数据的流程在这个里面主要使用到了一个直接内存不需要在堆中开辟空间进行数据的拷贝jvm可以直接操作直接内存从而使数据读写传输更快。 1.7 堆栈的区别是什么 难易程度☆☆☆ 出现频率☆☆☆☆ 1、栈内存一般会用来存储局部变量和方法调用但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收而栈不会。
2、栈内存是线程私有的而堆内存是线程共有的。
3,、两者异常错误不同但如果栈内存或者堆内存不足都会抛出异常。
栈空间不足java.lang.StackOverFlowError。
堆空间不足java.lang.OutOfMemoryError。
2 类加载器
2.1 什么是类加载器类加载器有哪些? 难易程度☆☆☆☆ 出现频率☆☆☆ 要想理解类加载器的话务必要先清楚对于一个Java文件它从编译到执行的整个过程。 类加载器用于装载字节码文件(.class文件)运行时数据区用于分配存储空间执行引擎执行字节码文件或本地方法垃圾回收器用于对JVM中的垃圾内容进行回收
类加载器
JVM只会运行二进制文件而类加载器ClassLoader的主要作用就是将字节码文件加载到JVM中从而让Java程序能够启动起来。现有的类加载器基本上都是java.lang.ClassLoader的子类该类的只要职责就是用于将指定的类找到或生成对应的字节码文件同时类加载器还会负责加载程序所需要的资源
类加载器种类
类加载器根据各自加载范围的不同划分为四种类加载器 启动类加载器(BootStrap ClassLoader) 该类并不继承ClassLoader类其是由C编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。 扩展类加载器(ExtClassLoader) 该类是ClassLoader的子类主要加载JAVA_HOME/jre/lib/ext目录中的类库。 应用类加载器(AppClassLoader) 该类是ClassLoader的子类主要用于加载classPath下的类也就是加载开发者自己编写的Java类。 自定义类加载器 开发者自定义类继承ClassLoader实现自定义类加载规则。
上述三种类加载器的层次结构如下如下 类加载器的体系并不是“继承”体系而是委派体系类加载器首先会到自己的parent中查找类或者资源如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次。
2.2 什么是双亲委派模型 难易程度☆☆☆☆ 出现频率☆☆☆☆ 如果一个类加载器在接到加载类的请求时它首先不会自己尝试去加载这个类而是把这个请求任务委托给父类加载器去完成依次递归如果父类加载器可以完成类加载任务就返回成功只有父类加载器无法完成此加载任务时才由下一级去加载。 2.3 JVM为什么采用双亲委派机制 难易程度☆☆☆ 出现频率☆☆☆ 1通过双亲委派机制可以避免某一个类被重复加载当父类已经加载后则无需重复加载保证唯一性。
2为了安全保证类库API不会被修改
在工程中新建java.lang包接着在该包下新建String类并定义main函数
public class String {public static void main(String[] args) {System.out.println(demo info);}
} 此时执行main函数会出现异常在类 java.lang.String 中找不到 main 方法 出现该信息是因为由双亲委派的机制java.lang.String的在启动类加载器(Bootstrap classLoader)得到加载因为在核心jre库中有其相同名字的类文件但该类中并没有main方法。这样就能防止恶意篡改核心API库。
2.4 说一下类装载的执行过程 难易程度☆☆☆☆☆ 出现频率☆☆☆ 类从加载到虚拟机中开始直到卸载为止它的整个生命周期包括了加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中验证、准备和解析这三个部分统称为连接linking。 类加载过程详解
1.加载 通过类的全名获取类的二进制数据流。 解析类的二进制数据流为方法区内的数据结构Java类模型 创建java.lang.Class类的实例表示该类型。作为方法区这个类的各种数据的访问入口 2.验证 验证类是否符合JVM规范安全性检查
(1)文件格式验证:是否符合Class文件的规范 (2)元数据验证 这个类是否有父类除了Object这个类之外其余的类都应该有父类 这个类是否继承extends了被final修饰过的类被final修饰过的类表示类不能被继承 类中的字段、方法是否与父类产生矛盾。被final修饰过的方法或字段是不能覆盖的 (3)字节码验证 主要的目的是通过对数据流和控制流的分析确定程序语义是合法的、符合逻辑的。 (4)符号引用验证符号引用以一组符号来描述所引用的目标符号可以是任何形式的字面量 比如int i 3; 字面量3 符号引用i 3.准备 为类变量分配内存并设置类变量初始值 static变量分配空间在准备阶段完成设置默认值赋值在初始化阶段完成 static变量是final的基本类型以及字符串常量值已确定赋值在准备阶段完成 static变量是final的引用类型那么赋值也会在初始化阶段完成 4.解析 把类中的符号引用转换为直接引用
比如方法中调用了其他方法方法名可以理解为符号引用而直接引用就是使用指针直接指向方法。 5.初始化 对类的静态变量静态代码块执行初始化操作 如果初始化一个类的时候其父类尚未初始化则优先初始化其父类。 如果同时包含多个静态变量和静态代码块则按照自上而下的顺序依次执行。
6.使用 JVM 开始从入口方法开始执行用户的程序代码 调用静态类成员信息比如静态字段、静态方法 使用new关键字为其创建对象实例
7.卸载
当用户程序代码执行完毕后JVM 便开始销毁创建的 Class 对象最后负责运行的 JVM 也退出内存
3 垃圾收回
3.1 简述Java垃圾回收机制GC是什么为什么要GC 难易程度☆☆☆ 出现频率☆☆☆ 为了让程序员更专注于代码的实现而不用过多的考虑内存释放的问题所以在Java语言中有了自动的垃圾回收机制也就是我们熟悉的GC(Garbage Collection)。
有了垃圾回收机制后程序员只需要关心内存的申请即可内存的释放由系统自动识别完成。
在进行垃圾回收时不同的对象引用类型GC会采用不同的回收时机
换句话说自动的垃圾回收的算法就会变得非常重要了如果因为算法的不合理导致内存资源一直没有释放同样也可能会导致内存溢出的。
当然除了Java语言C#、Python等语言也都有自动的垃圾回收机制。
3.2 对象什么时候可以被垃圾器回收 难易程度☆☆☆☆ 出现频率☆☆☆☆ 简单一句就是如果一个或多个对象没有任何的引用指向它了那么这个对象现在就是垃圾如果定位了垃圾则有可能会被垃圾回收器回收。
如果要定位什么是垃圾有两种方式来确定第一个是引用计数法第二个是可达性分析算法
3.2.1 引用计数法
一个对象被引用了一次在当前的对象头上递增一次引用次数如果这个对象的引用次数为0代表这个对象可回收
String demo new String(123);String demo null;当对象间出现了循环引用的话则引用计数法就会失效 先执行右侧代码的前4行代码 目前上方的引用关系和计数都是没问题的但是如果代码继续往下执行如下图 虽然a和b都为null但是由于a和b存在循环引用这样a和b永远都不会被回收。
优点
实时性较高无需等到内存不够的时候才开始回收运行时根据对象的计数器是否为0就可以直接回收。在垃圾回收过程中应用无需挂起。如果申请内存时内存不足则立刻报OOM错误。区域性更新对象的计数器时只是影响到该对象不会扫描全部对象。
缺点
每次对象被引用时都需要去更新计数器有一点时间开销。浪费CPU资源即使内存够用仍然在运行时进行计数器的统计。无法解决循环引用问题会引发内存泄露。最大的缺点
3.2.2 可达性分析算法
现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。
会存在一个根节点【GC Roots】引出它下面指向的下一个节点再以下一个节点节点开始找出它下面的节点依次往下类推。直到所有的节点全部遍历完毕。 根对象是那些肯定不能当做垃圾回收的对象就可以当做根对象 局部变量静态方法静态变量类信息 核心是判断某对象是否与根对象有直接或间接的引用如果没有被引用则可以当做垃圾回收 X,Y这两个节点是可回收的但是并不会马上的被回收 对象中存在一个方法【finalize】。当对象被标记为可回收后当发生GC时首先会判断这个对象是否执行了finalize方法如果这个方法还没有被执行的话那么就会先来执行这个方法接着在这个方法执行中可以设置当前这个对象与GC ROOTS产生关联那么这个方法执行完成之后GC会再次判断对象是否可达如果仍然不可达则会进行回收如果可达了则不会进行回收。
finalize方法对于每一个对象来说只会执行一次。如果第一次执行这个方法的时候设置了当前对象与RC ROOTS关联那么这一次不会进行回收。 那么等到这个对象第二次被标记为可回收时那么该对象的finalize方法就不会再次执行了。
GC ROOTS
虚拟机栈栈帧中的本地变量表中引用的对象
/*** demo是栈帧中的本地变量当 demo null 时由于此时 demo 充当了 GC Root 的作用demo与原来指向的实例 new Demo() 断开了连接对象被回收。*/
public class Demo {public static void main(String[] args) {Demo demo new Demo();demo null;}
}方法区中类静态属性引用的对象
/*** 当栈帧中的本地变量 b null 时由于 b 原来指向的对象与 GC Root (变量 b) 断开了连接所以 b 原来指向的对象会被回收而由于我们给 a 赋值了变量的引用a在此时是类静态属性引用充当了 GC Root 的作用它指向的对象依然存活!*/
public class Demo {public static Demo a;public static void main(String[] args) {Demo b new Demo();b.a new Demo();b null;}
}方法区中常量引用的对象
/*** 常量 a 指向的对象并不会因为 demo 指向的对象被回收而回收*/
public class Demo {public static final Demo a new Demo();public static void main(String[] args) {Demo demo new Demo();demo null;}
}本地方法栈中 JNI即一般说的 Native 方法引用的对象
3.3 JVM 垃圾回收算法有哪些 难易程度☆☆☆ 出现频率☆☆☆☆ 3.3.1 标记清除算法
标记清除算法是将垃圾回收分为2个阶段分别是标记和清除。
1.根据可达性分析算法得出的垃圾进行标记
2.对这些标记为可回收的内容进行垃圾回收 可以看到标记清除算法解决了引用计数算法中的循环引用的问题没有从root节点引用的对象都会被回收。
同样标记清除算法也是有缺点的
效率较低标记和清除两个动作都需要遍历所有的对象并且在GC时需要停止应用程序对于交互性要求比较高的应用而言这个体验是非常差的。重要通过标记清除算法清理出来的内存碎片化较为严重因为被回收的对象可能存在于内存的各个角落所以清理出来的内存是不连贯的。
3.3.2 复制算法
复制算法的核心就是将原有的内存空间一分为二每次只用其中的一块在垃圾回收时将正在使用的对象复制到另一个内存空间中然后将该内存空间清空交换两个内存的角色完成垃圾的回收。
如果内存中的垃圾对象较多需要复制的对象就较少这种情况下适合使用该方式并且效率比较高反之则不适合。 1将内存区域分成两部分每次操作其中一个。
2当进行垃圾回收时将正在使用的内存区域中的存活对象移动到未使用的内存区域。当移动完对这部分内存区域一次性清除。
3周而复始。
优点
在垃圾对象多的情况下效率较高清理后内存无碎片
缺点
分配的2块内存空间在同一个时刻只能使用一半内存使用率较低
3.3.3 标记整理算法
标记压缩算法是在标记清除算法的基础之上做了优化改进的算法。和标记清除算法一样也是从根节点开始对对象的引用进行标记在清理阶段并不是简单的直接清理可回收对象而是将存活对象都向内存另一端移动然后清理边界以外的垃圾从而解决了碎片化的问题。 1标记垃圾。
2需要清除向右边走不需要清除的向左边走。
3清除边界以外的垃圾。
优缺点同标记清除算法解决了标记清除算法的碎片化的问题同时标记压缩算法多了一步对象移动内存位置的步骤其效率也有有一定的影响。
与复制算法对比复制算法标记完就复制但标记整理算法得等把所有存活对象都标记完毕再进行整理
3.4 分代收集算法
3.4.1 概述
在java8时堆被分为了两份新生代和老年代【12】在java7时还存在一个永久代。 对于新生代内部又被分为了三个区域。Eden区S0区S1区【811】
当对新生代产生GCMinorGC【young GC】
当对老年代代产生GCMajor GC
当对新生代和老年代产生FullGC 新生代 老年代完整垃圾回收暂停时间长应尽力避免
3.4.2工作机制 新创建的对象都会先分配到eden区 当伊甸园内存不足标记伊甸园与 from现阶段没有的存活对象 将存活对象采用复制算法复制到 to 中复制完毕后伊甸园和 from 内存都得到释放 经过一段时间后伊甸园的内存又出现不足标记eden区域to区存活的对象将存活的对象复制到from区 当幸存区对象熬过几次回收最多15次晋升到老年代幸存区内存不足或大对象会导致提前晋升
MinorGC、 Mixed GC 、 FullGC的区别是什么 MinorGC【young GC】发生在新生代的垃圾回收暂停时间短STW Mixed GC 新生代 老年代部分区域的垃圾回收G1 收集器特有 FullGC 新生代 老年代完整垃圾回收暂停时间长STW应尽力避免 名词解释 STWStop-The-World暂停所有应用程序线程等待垃圾回收的完成 3.5 说一下 JVM 有哪些垃圾回收器 难易程度☆☆☆☆ 出现频率☆☆☆☆ 在jvm中实现了多种垃圾收集器包括 串行垃圾收集器 并行垃圾收集器 CMS并发垃圾收集器 G1垃圾收集器
3.5.1 串行垃圾收集器
Serial和Serial Old串行垃圾收集器是指使用单线程进行垃圾回收堆内存较小适合个人电脑 Serial 作用于新生代采用复制算法 Serial Old 作用于老年代采用标记-整理算法
垃圾回收时只有一个线程在工作并且java应用中的所有线程都要暂停STW等待垃圾回收的完成。 3.5.2 并行垃圾收集器
Parallel New和Parallel Old是一个并行垃圾回收器JDK8默认使用此垃圾回收器 Parallel New作用于新生代采用复制算法 Parallel Old作用于老年代采用标记-整理算法
垃圾回收时多个线程在工作并且java应用中的所有线程都要暂停STW等待垃圾回收的完成。 3.5.2 CMS并发垃圾收集器
CMS全称 Concurrent Mark Sweep是一款并发的、使用标记-清除算法的垃圾回收器该回收器是针对老年代垃圾回收的是一款以获取最短回收停顿时间为目标的收集器停顿时间短用户体验就好。其最大特点是在进行垃圾回收时应用仍然能正常运行。 3.6 详细聊一下G1垃圾回收器 难易程度☆☆☆☆ 出现频率☆☆☆☆ 3.6.1 概述 应用于新生代和老年代在JDK9之后默认使用****G1 划分成多个区域每个区域都可以充当 edensurvivorold humongous其中 humongous 专为大对象准备 采用复制算法 响应时间与吞吐量兼顾 分成三个阶段新生代回收、并发标记、混合收集 如果并发失败即回收速度赶不上创建新对象速度会触发 Full GC 3.6.2 Young Collection(年轻代垃圾回收) 初始时所有区域都处于空闲状态 创建了一些对象挑出一些空闲区域作为伊甸园区存储这些对象 当伊甸园需要垃圾回收时挑出一个空闲区域作为幸存区用复制算法复制存活对象需要暂停用户线程 随着时间流逝伊甸园的内存又有不足 将伊甸园以及之前幸存区中的存活对象采用复制算法复制到新的幸存区其中较老对象晋升至老年代
3.6.3 Young Collection Concurrent Mark (年轻代垃圾回收并发标记)
当老年代占用内存超过阈值(默认是45%)后触发并发标记这时无需暂停用户线程 并发标记之后会有重新标记阶段解决漏标问题此时需要暂停用户线程。 这些都完成后就知道了老年代有哪些存活对象随后进入混合收集阶段。此时不会对所有老年代区域进行回收而是根据暂停时间目标优先回收价值高存活对象少的区域这也是 Gabage First 名称的由来。
3.6.4 Mixed Collection (混合垃圾回收)
复制完成内存得到释放。进入下一轮的新生代回收、并发标记、混合收集 其中H叫做巨型对象如果对象非常大会开辟一块连续的空间存储巨型对象 3.7 强引用、软引用、弱引用、虚引用的区别 难易程度☆☆☆☆ 出现频率☆☆☆ 3.7.1 强引用
强引用只有所有 GC Roots 对象都不通过【强引用】引用该对象该对象才能被垃圾回收
User user new User();3.7.2 软引用
软引用仅有软引用引用该对象时在垃圾回收后内存仍不足时会再次出发垃圾回收
User user new User();
SoftReference softReference new SoftReference(user);3.7.3 弱引用
弱引用仅有弱引用引用该对象时在垃圾回收时无论内存是否充足都会回收弱引用对象
User user new User();
WeakReference weakReference new WeakReference(user);延伸话题ThreadLocal内存泄漏问题 ThreadLocal用的就是弱引用看以下源码
static class Entry extends WeakReferenceThreadLocal? {Object value;Entry(ThreadLocal? k, Object v) {super(k);value v; //强引用不会被回收}
}Entry的key是当前ThreadLocalvalue值是我们要设置的数据。
WeakReference表示的是弱引用当JVM进行GC时一旦发现了只具有弱引用的对象不管当前内存空间是否足够都会回收它的内存。但是value是强引用它不会被回收掉。 ThreadLocal使用建议使用完毕后注意调用清理方法。 3.7.4 虚引用
虚引用必须配合引用队列使用被引用对象回收时会将虚引用入队由 Reference Handler 线程调用虚引用相关方法释放直接内存 4 JVM实践调优
4.1 JVM 调优的参数可以在哪里设置参数值 难易程度☆☆ 出现频率☆☆☆ 4.1.1 tomcat的设置vm参数
修改TOMCAT_HOME/bin/catalina.sh文件如下图
JAVA_OPTS-Xms512m -Xmx1024m 4.1.2 springboot项目jar文件启动
通常在linux系统下直接加参数启动springboot项目
nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.activeprod nohup : 用于在系统后台不挂断地运行命令退出终端不会影响程序的运行 参数 让命令在后台执行终端退出后命令仍旧执行。 4.2 用的 JVM 调优的参数都有哪些 难易程度☆☆☆ 出现频率☆☆☆☆ 对于JVM调优主要就是调整年轻代、年老大、元空间的内存空间大小及使用的垃圾回收器类型。
https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html
1设置堆的初始大小和最大大小为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间通常把最大、初始大小设置为相同的值。
-Xms设置堆的初始化大小-Xmx设置堆的最大大小2 设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置则默认比例为8:1:1。Java官方通过增大Eden区的大小来减少YGC发生的次数但有时我们发现虽然次数减少了但Eden区满
的时候由于占用的空间较大导致释放缓慢此时STW的时间较长因此需要按照程序情况去调优。
-XXSurvivorRatio3表示年轻代中的分配比率survivor:eden 2:33年轻代和老年代默认比例为12。可以通过调整二者空间大小比率来设置两者的大小。
-XX:newSize 设置年轻代的初始大小
-XX:MaxNewSize 设置年轻代的最大大小 初始大小和最大大小两个值通常相同4线程堆栈的设置每个线程默认会开启1M的堆栈用于存放栈帧、调用参数、局部变量等但一般256K就够用。通常减少每个线程的堆栈可以产生更多的线程但这实际上还受限于操作系统。
-Xss 对每个线程stack大小的调整,-Xss128k5一般来说当survivor区不够大或者占用量达到50%就会把一些对象放到老年区。通过设置合理的eden区survivor区及使用率可以将年轻对象保存在年轻代从而避免full GC使用-Xmn设置年轻代的大小
6系统CPU持续飙高的话首先先排查代码问题如果代码没问题则咨询运维或者云服务器供应商通常服务器重启或者服务器迁移即可解决。
7对于占用内存比较多的大对象一般会选择在老年代分配内存。如果在年轻代给大对象分配内存年轻代内存不够了就要在eden区移动大量对象到老年代然后这些移动的对象可能很快消亡因此导致full GC。通过设置参数-XX:PetenureSizeThreshold1000000单位为B标明对象大小超过1M时在老年代(tenured)分配内存空间。
8一般情况下年轻对象放在eden区当第一次GC后如果对象还存活放到survivor区此后每GC一次年龄增加1当对象的年龄达到阈值就被放到tenured老年区。这个阈值可以同构-XX:MaxTenuringThreshold设置。如果想让对象留在年轻代可以设置比较大的阈值。
1-XX:UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器可以尽可能的减少垃圾回收时间。2-XX:UseParallelOldGC:设置老年代使用并行垃圾回收收集器。9尝试使用大的内存分页使用大的内存分页增加CPU的内存寻址能力从而系统的性能。
-XX:LargePageSizeInBytes 设置内存页的大小10使用非占用的垃圾收集器。
-XX:UseConcMarkSweepGC老年代使用CMS收集器降低停顿。4.3 说一下 JVM 调优的工具 难易程度☆☆☆☆ 出现频率☆☆☆☆ 4.3.1 命令工具
4.3.1.1 jpsJava Process Status
输出JVM中运行的进程状态信息(现在一般使用jconsole) 4.3.1.2 jstack
查看java进程内线程的堆栈信息。
jstack [option] pid java案例
package com.heima.jvm;public class Application {public static void main(String[] args) throws InterruptedException {while (true){Thread.sleep(1000);System.out.println(哈哈哈);}}
}
使用jstack查看进行堆栈运行信息 4.3.1.3 jmap
用于生成堆转存快照 jmap [options] pid 内存映像信息 jmap -heap pid 显示Java堆的信息 jmap -dump:formatb,fileheap.hprof pid formatb表示以hprof二进制格式转储Java堆的内存 file用于指定快照dump文件的文件名。 例显示了某一个java运行的堆信息
C:\Users\yuhonjmap -heap 53280
Attaching to process ID 53280, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.321-b07using thread-local object allocation.
Parallel GC with 8 thread(s) //并行的垃圾回收器Heap Configuration: //堆配置MinHeapFreeRatio 0 //空闲堆空间的最小百分比MaxHeapFreeRatio 100 //空闲堆空间的最大百分比MaxHeapSize 8524922880 (8130.0MB) //堆空间允许的最大值NewSize 178257920 (170.0MB) //新生代堆空间的默认值MaxNewSize 2841640960 (2710.0MB) //新生代堆空间允许的最大值OldSize 356515840 (340.0MB) //老年代堆空间的默认值NewRatio 2 //新生代与老年代的堆空间比值表示新生代老年代12SurvivorRatio 8 //两个Survivor区和Eden区的堆空间比值为8,表示S0:S1:Eden1:1:8MetaspaceSize 21807104 (20.796875MB) //元空间的默认值CompressedClassSpaceSize 1073741824 (1024.0MB) //压缩类使用空间大小MaxMetaspaceSize 17592186044415 MB //元空间允许的最大值G1HeapRegionSize 0 (0.0MB)//在使用 G1 垃圾回收算法时JVM 会将 Heap 空间分隔为若干个 Region该参数用来指定每个 Region 空间的大小。Heap Usage:
PS Young Generation
Eden Space: //Eden使用情况capacity 134217728 (128.0MB)used 10737496 (10.240074157714844MB)free 123480232 (117.75992584228516MB)8.000057935714722% used
From Space: //Survivor-From 使用情况capacity 22020096 (21.0MB)used 0 (0.0MB)free 22020096 (21.0MB)0.0% used
To Space: //Survivor-To 使用情况capacity 22020096 (21.0MB)used 0 (0.0MB)free 22020096 (21.0MB)0.0% used
PS Old Generation //老年代 使用情况capacity 356515840 (340.0MB)used 0 (0.0MB)free 356515840 (340.0MB)0.0% used3185 interned Strings occupying 261264 bytes.4.3.1.4 jhat
用于分析jmap生成的堆转存快照一般不推荐使用而是使用Ecplise Memory Analyzer
4.3.1.5 jstat
是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。
常见参数
①总结垃圾回收统计
jstat -gcutil pid字段含义S0幸存1区当前使用比例S1幸存2区当前使用比例E伊甸园区使用比例O老年代使用比例M元数据区使用比例CCS压缩使用比例YGC年轻代垃圾回收次数YGCT年轻代垃圾回收消耗时间FGC老年代垃圾回收次数FGCT老年代垃圾回收消耗时间GCT垃圾回收消耗总时间
②垃圾回收统计
jstat -gc pid4.3.2 可视化工具
4.3.2.1 jconsole
用于对jvm的内存线程类 的监控是一个基于 jmx 的 GUI 性能监控工具
打开方式java 安装目录 bin目录下 直接启动 jconsole.exe 就行 可以内存、线程、类等信息 4.3.2.2 VisualVM故障处理工具
能够监控线程内存情况查看方法的CPU时间和内存中的对 象已被GC的对象反向查看分配的堆栈
打开方式java 安装目录 bin目录下 直接启动 jvisualvm.exe就行 监控程序运行情况 查看运行中的dump 查看堆中的信息 4.4 java内存泄露的排查思路 难易程度☆☆☆☆ 出现频率☆☆☆☆ 原因
如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量的时候java虚拟机将抛出一个StackOverFlowError异常
如果java虚拟机栈可以动态拓展并且扩展的动作已经尝试过但是目前无法申请到足够的内存去完成拓展或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈那java虚拟机将会抛出一个OutOfMemoryError异常
如果一次加载的类太多元空间内存不足则会报OutOfMemoryError: Metaspace 1、通过jmap指定打印他的内存快照 dump 有的情况是内存溢出之后程序则会直接中断而jmap只能打印在运行中的程序所以建议通过参数的方式的生成dump文件配置如下 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/home/app/dumps/ 指定生成后文件的保存目录 2、通过工具 VisualVMEcplise MAT去分析 dump文件
VisualVM可以加载离线的dump文件如下图
文件–装入—选择dump文件即可查看堆快照信息 如果是linux系统中的程序则需要把dump文件下载到本地windows环境下打开VisualVM工具分析。VisualVM目前只支持在windows环境下运行可视化 3、通过查看堆信息的情况可以大概定位内存溢出是哪行代码出了问题 4、找到对应的代码通过阅读上下文的情况进行修复即可
4.5 CPU飙高排查方案与思路 难易程度☆☆☆☆ 出现频率☆☆☆☆ 1.使用top命令查看占用cpu的情况 2.通过top命令查看后可以查看是哪一个进程占用cpu较高上图所示的进程为30978
3.查看当前线程中的进程信息
ps H -eo pid,tid,%cpu | grep 40940pid 进行id tid 进程中的线程id % cpu使用率 4.通过上图分析在进程30978中的线程30979占用cpu较高 注意上述的线程id是一个十进制我们需要把这个线程id转换为16进制才行因为通常在日志中展示的都是16进制的线程id名称 转换方式 在linux中执行命令 printf %x\n 30979 5.可以根据线程 id 找到有问题的线程进一步定位到问题代码的源码行号
执行命令
jstack 30978 此处是进程id