A-1.编程语言的分类,特点

  • 面向对象,面向过程,面向函数
  • 静态类型,动态类型
  • 编译执行,解释执行
  • 有虚拟机,无虚拟机(VM)
  • 有GC,无GC(Garbege Collector,JVM内存管理器)

JVM理论知识(2021/09) - 图1

JAVA
面向对象,静态类型,编译执行,有VM,有GC,有运行时runtime,二进制字节码跨平台的高级语言。

GC-垃圾回收器
Garbege Collector,垃圾回收器,JVM内存管理器。所有对象的创建和使用的生命周期到最后销毁的管理都有GC这个模块来完成。

RUNTIME-运行时
在JVM运行JAVA程序时,需要启动的JAVA进程,包括它依赖的一些基本的库,这样组成的一整个的环境。

跨平台
程序在Window,Linux,Mac上都可以运行。

  • JAVA:二进制字节码跨平台,编译完成的字节码class文件分发到不同的平台上运行。
  • C++:源代码跨平台,将cpp代码文件放在平台上编译后再运行。Windows上:exe或dll文件;Linux/Mac上:so文件或可运行文件或命令。

面向对象:以功能来划分问题,耦合性较强低,可维护性较好。
面向过程:偏向流程,按步骤一步一步调用函数。耦合性较高,可维护性较差。

A-2.字节码技术

定义
Java bytecode由单字节(byte)的指令组成,理论上最多支持256个操作码(opcode)。实际上JAVA只使用了200个左右的操作码,还有一些操作码保留给调试操作。

字节码(操作码)按性质分类

  1. 栈操作指令,包括与局部变量交互的指令:JVM是基于栈的真实计算机操作指令,此指令是虚拟机本身结构需要存在。跟JAVA语言代码不直接对应,是为了能够像计算机运行CPU指令一样运行所设计的指令。
  2. 程序流程控制指令:for循环,if else判断
  3. 对象操作指令,包括方法调用指令:JAVA面向对象
  4. 算术运算以及类型转换指令:加减乘除,数据类型相互转换

编译class文件命令
javac -g package/ClassName.java
(默认不显示本地变量信息,需要显示的话在命令中加-g)
查看class文件命令
javap -c verbose package.ClassName
(默认不显示版本号修饰符常量池行号等信息,需要显示的话在命令中加verbose)

字节码运行时RUNTIME结构
JVM是一台其于栈的计算机器。
每个线程运行的时候,都有一个独属于自己的线程栈(JVM Stack),用于存储栈帧(Frame)。
每一次方法调用,JVM都会自动创建一个栈帧,当一个方法内部再去调用了其他方法,又会再重新创建一个栈帧。
每一个栈帧都是由操作数栈(Operand Stack),局部变量数组(Local variables)以及一个Class引用组成。
变量通过栈的操作指令从本地变量表(Local variables)加载到栈上(压栈),或者运行完后再把它写回本地变量表中。
Class引用指向当前方法在运行时RUNTIME的常量池(Constant pool)中对应的class。

JVM理论知识(2021/09) - 图2
CPU本身相当于操作的是栈JVM Stack,合起来就像是个计算机了。
本地变量表就相当于是磁盘或者数据库
计算CPU处理 — 操作数据栈

A-3.JVM类加载器

类的生命周期(前5步为类加载过程)

  1. 加载loading:找Class文件,不管是在文件夹里还是jar包里。
  2. 验证Verification:验证字节码格式是否正确,依赖是否完备。
  3. 准备Prepareation:静态字段,方法表
  4. 解析Resolution:把符号解析为实际的引用
  5. 初始化Initialization:构造器,静态变量赋值,静态代码块
  6. 使用Using
  7. 卸载Unloading:由类加载器执行

Q:初始化和加载的区别是什么?

类的加载时机(什么时候被JVM加载)

  1. (显式调用)当虚拟机启动时,初始化用户指定的主类,也就是启动执行main方法所在的类。
  2. (显式调用)当遇到以新建目标类实例的new指令时,初始化new指令的目标类。也就是new一个类的时候要初始化。
  3. (显式调用)当遇到调用静态方法的指令时,初始化该静态方法所在的类。
  4. (显式调用)当遇到访问静态字段的指令时,初始化该静态字段所在的类。
  5. (隐式调用)子类的初始化会触发父类的初始化。
  6. (隐式调用)如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化。
  7. (隐式调用)使用反射API对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化。
  8. (隐式调用)当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。

不会初始化(可能会加载)

  1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  2. 定义对象数组,不会触发该类的初始化。
  3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  4. 通过类名获取Class对象,不会触发类的初始化,Hello.Class不会让Hello类初始化。
  5. 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化,默认为true。另:Class.forName(“jvm.Hello”)会加载Hello类。
  6. 通过ClassLoader默认的loadClass就去,也不会触发初始化动作(加载了,但是不初始化)。

JVM内部类加载器种类

  1. 启动类加载器(BootstrapClassLoader):在JVM底层实现,JDK中找不到此加载器的定义。加载JVM最核心的一些jar包和类以及公共类,如:Object,String,rf.jar
  2. 扩展类加载器(ExtClassLoader):核心类公共类以外的额外的类
  3. 应用类加载器(AppClassLoader):我们自己编写的java类,以及classpath中指定的jar包
  4. 自定义类加载器

JVM理论知识(2021/09) - 图3

  1. package com.zhukova.geektrain.jvm;
  2. import sun.management.snmp.jvminstr.JvmClassLoadingImpl;
  3. import sun.management.snmp.jvmmib.JvmClassLoadingMBean;
  4. import sun.management.snmp.jvmmib.JvmClassLoadingMeta;
  5. import java.net.URL;
  6. import java.util.Scanner;
  7. public class MainApplication {
  8. public static void main(String[] args){
  9. // 应用类加载器
  10. ClassLoader appClassLoader = JvmClassLoadingMeta.class.getClassLoader();
  11. printClassLoader("应用类加载器AppClassLoader", appClassLoader);
  12. // 扩展类加载器
  13. ClassLoader extClassLoader = appClassLoader.getParent();
  14. printClassLoader("扩展类加载器ExtClassLoader", extClassLoader);
  15. // 启动类加载器(JVM底层C++编写,JDK中无法获取)
  16. ClassLoader bootstrapClassLoader = extClassLoader.getParent();
  17. printClassLoader("启动类加载器BootstrapClassLoader", bootstrapClassLoader);
  18. // 启动类加载器:代码层面拿不到,使用静态方法取得ClassPath
  19. URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
  20. System.out.println("启动类加载器BootstrapClassLoader");
  21. for (URL url : urls){
  22. // 7个jar包,1个文件夹classes
  23. System.out.println(url.toExternalForm());
  24. }
  25. }
  26. private static void printClassLoader(String pLoaderName, ClassLoader pClassLoader){
  27. System.out.println(pLoaderName);
  28. if (pClassLoader == null) {
  29. System.out.println(pLoaderName + " --> null");
  30. } else {
  31. System.out.println(pClassLoader);
  32. }
  33. }
  34. }

Q:用JDK8找不到JvmClassLoaderPrintPath

加载器特点

  1. 双亲委托
  2. 负责依赖
  3. 缓存加载:默认情况下一个类只会被加载一次,放在缓存里,使用时从缓存读取,效率高。

添加引用类的几种方式

  1. 把.class类文件放到JDK的lib/ext下。==>被扩展类加载器ExtClassLoader加载
  2. 启动Java进程命令时,使用参数-Djava.ext.dirs额外地添加指定JDK使用的class的扩展路径或者jar包的扩展路径。 ==>被扩展类加载器ExtClassLoader加载
  3. 用命令java -cp或者java -classpath,可以让JVM引用指定路径里的class或者jar包。
  4. 把class文件放到当前路径,一般是classpath路径,
  5. 自定义ClassLoader加载
  6. 拿到当前执行类的ClassLoader,反射调用URLClassLoader.allUrl方法添加Jar包或路径。注意:JDK9以后无效,可以使用ClassForName(),new一个URLClassLoader出来。

A-4.内存模型

JVM理论知识(2021/09) - 图4

  • 每个线程都只能访问自己的线程栈
  • 每个线程都不能访问其他线程的局部变量
  • 即使两个线程正在执行完全相同的代码,但每个线程都会在自己的线程栈内创建对应代码中声明的局部变量,所以每个线程都有一份自己的局部变量副本。
  • 所有原生类型的局部变量都存储在线程栈中,因此对其他线程是不可见的。
  • 线程可以将一个原生变量值的副本传给另一个线程,但不能共享原生局部变量本身。
  • 堆内存中包含了JAVA代码中创建的所有对象,不管是哪个线程创建的。其中也涵盖了包装类型(Byte,Integer,Long等)
  • 不管是创建一个对象并将其赋值给局部变量,还是赋值给另一个对象的成员变量,创建的对象都会被保存到堆内存中。
  • 如果是原生数据类型的局部变量,那么它的内容就全部保留在线程栈上。
  • 如果是对象引用,则栈中的局部变量槽位中保存着对象的引用地址,而实际的对象内容保存在堆中。
  • 对象的成员变量与对象本身一起存储在堆上,不管成员变量的类型是原生数值,还是对象引用。
  • 类的静态变量则和类定义一样都保存在堆中。

A-5.启动参数

现在一般认为JVM有1000+参数。如使用-Xms -Xmx参数配置内存分配。
java [options] classname [args]
java [options] -jar filename [args]

IDEA中配置参数:
image.png

JVM启动参数语法种类

  1. -:标准参数。所有的JVM都要实现这些参数,并且它是稳定向后兼容。如:-server
  2. -D:设置系统属性。给当前程序设置一个特殊的环境变量,会覆盖掉系统环境变量。如:-Dfile.encoding=UTF-8
  3. -X:非标准参数。默认JVM支持实现此类参数功能,但不保证所有的JVM的实现都支持,也不保证向后兼容。可使用java -X命令来查看当前JVM支持的非标准参数。如:-Xms2048m -Xmx8g
  4. -XX:非稳定参数。用于控制JVM一些具体行为,跟具体的JVM实现版本有关,随时可能会在下个版本取消。有两种形式:

4-1.-XX:+-Flags形式用于对布尔值Flags进行开关,+表示开,-表示关。
4-2.-XX:key=value形式,指定某个选项key的值。如:-XX:MaxPermSize=256m

JVM启动参数功能分类

  1. 系统属性参数,如环境变量,但在命令行中配置只影响当前进程,在操作系统中配置影响的是整个系统。-Da=A100相当于java里的System.setProperty(“a”, “A100”); String a = System.getProperty(“a”);
  2. 运行模式参数,如标准参数-server

-server:设置JVM使用server模式,启动速度慢,但运行时性能和内存管理效率高,适用于Web应用程序或服务端应用程序的生产环境。在具有64位JDK环境下默认启用该模式。
-client:设置JVM使用client模式,启动速度快,但运行时性能和内存管理效率低,适用于客户端应用程序或者PC应用开发和调试。JDK1.7之前在32位x86机器上默认启用该模式。
-Xint:设置JVM在解释模式(interpreted mode)下运行字节码,而不是把字节码编译成机器码执行,会降低运行速度。
-Xcomp:设置JVM在编译模式下运行本地机器码,带来最大程度的优化,执行速度较快。但需要编译所有的字节码成为本地机器码并存入缓存,此预热活动开销较大。
-Xmixed:混合模式,平衡解释模式和编译模式,把运行多次的热点代码编译成机器码,不编译运行次数较少的冷代码,降低编译成本。

  1. 堆内存设置参数

-Xmx:指定最大堆内内存,不包括堆外使用内存,非堆,线程栈内存。
-Xms:指定初始堆内存。专用服务器建议保持-Xms和-Xmx一致,否则应用刚启动可能就有好几个FullGC,当两者配置不一致时,堆内存扩容可能会导致性能抖动。以及还没有达到-Xmx,系统内存先不够导致出错。
-Xmn:等价于-XX:NewSize。在G1垃圾回收器之外表示的是young区的大小,G1垃圾回收器不应该设置此选项。官方建议设置为-Xmx的1/2到1/4。
-XX:MaxPermSize=size:元数据区,JDK1.7之前使用,此处为持久代。
-XX:MaxMetaspaceSize=size:元数据区,JDK8以后默认不限制Meta空间,一般不允许设置该选项。
-XX:MaxDirectMemorySize=size:系统可使用的最大堆外内存,不受GC管理。等同于-Dsun.nio.MaxDirectMemorySize
-Xss:每个线程栈的字节数,影响栈的深度 。如线程的调用栈发生内存溢出。等同于-XX:ThreadStackSize=size。

  1. GC设置参数:Q:JAVA8默认的GC算法是什么?

-XX:+-UseG1GC:使用G1垃圾回收器
-XX:+-UseConcMarkSweepGC:使用CMS垃圾回收器
-XX:+-UseSerialGC:使用串行垃圾回收器
-XX:+-UseParallelGC:使用并行垃圾回收器
-XX:+-UnlockExperimentalVMOptions -XX:+-UseZGC:JDK11以上使用
-XX:+-UnlockExperimentalVMOptions -XX:+-UseShenandoahGC:JDK12以上使用

  1. 分析诊断参数

-XX:+-HeapDumpOnOutOfMemoryError:当发生OutOfMemory异常,自动生成Dump堆内存快照。
-XX:HeapDumpPath:与上一个搭配使用,指定内存溢出时Dump文件的保存目录。如果未指定由黑夜为启动Java程序的工作目录。自动Dump的hprof文件会存储到/usr/local/目录下。
-XX:OnError:发生致命错误(fatal error)时执行的脚本。例如,写一个脚本来记录出错时间,执行一些命令,或者curl一下某个在线报警的url。例:java -XX:OnError=”gdb - %p” myApp。其中%p表示进程PID。
-XX:OnOutOfMemoryError:抛出OutOfMemoryError错误时,需要执行的脚本。
-XX:ErrorFile:致命错误的日志文件名,绝对路径或者相对路径。
-Xdebug

  1. JavaAgent参数:通过无侵入方式实现一些功能,比如注入AOP代码,执行统计等,权限非常大。

-agentlib:libname[=options]:启用native本土方式的agent,参考LD_LIBRARY_PATH路径。
-agentpath:pathname[=options]:启用native本土方式的agent。
-javaagent:jarpath[=options]:启用外部的agent库
-Xnoagent:禁用所有agent。
示例:开启CPU使用时间抽样分析:JAVA_OPTS=”-agentlib:hprof=cpu=samples,file=cpu.samples.log”

A-6.作业

1.(选做)自己写一个简单的 Hello.java,里面需要涉及基本类型,四则运行,if 和 for,然后自己分析一下对应的字节码,有问题群里讨论。

  1. package com.zhukova.geektrain.jvm;
  2. public class HelloByte {
  3. HelloByte() {
  4. String str = "StrHello:";
  5. int m = 10, n = 2;
  6. if (m > n) {
  7. System.out.println(str + m / 2);
  8. }
  9. }
  10. public static void main(String[] args) {
  11. new HelloByte();
  12. }
  13. }
  1. // class version 52.0 (52)
  2. // access flags 0x21
  3. public class com/zhukova/geektrain/jvm/HelloByte {
  4. // compiled from: HelloByte.java
  5. // access flags 0x0
  6. <init>()V
  7. L0
  8. LINENUMBER 4 L0
  9. ALOAD 0
  10. INVOKESPECIAL java/lang/Object.<init> ()V
  11. L1
  12. LINENUMBER 5 L1
  13. LDC "StrHello"
  14. ASTORE 1
  15. L2
  16. LINENUMBER 6 L2
  17. BIPUSH 10
  18. ISTORE 2
  19. L3
  20. ICONST_2
  21. ISTORE 3
  22. L4
  23. LINENUMBER 8 L4
  24. ILOAD 2
  25. ILOAD 3
  26. IF_ICMPLE L5
  27. L6
  28. LINENUMBER 9 L6
  29. GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
  30. NEW java/lang/StringBuilder
  31. DUP
  32. INVOKESPECIAL java/lang/StringBuilder.<init> ()V
  33. ALOAD 1
  34. INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
  35. ILOAD 2
  36. ICONST_2
  37. IDIV
  38. INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
  39. INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
  40. INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
  41. L5
  42. LINENUMBER 11 L5
  43. FRAME FULL [com/zhukova/geektrain/jvm/HelloByte java/lang/String I I] []
  44. RETURN
  45. L7
  46. LOCALVARIABLE this Lcom/zhukova/geektrain/jvm/HelloByte; L0 L7 0
  47. LOCALVARIABLE str Ljava/lang/String; L2 L7 1
  48. LOCALVARIABLE m I L3 L7 2
  49. LOCALVARIABLE n I L4 L7 3
  50. MAXSTACK = 4
  51. MAXLOCALS = 4
  52. // access flags 0x9
  53. public static main([Ljava/lang/String;)V
  54. // parameter args
  55. L0
  56. LINENUMBER 14 L0
  57. NEW com/zhukova/geektrain/jvm/HelloByte
  58. DUP
  59. INVOKESPECIAL com/zhukova/geektrain/jvm/HelloByte.<init> ()V
  60. POP
  61. L1
  62. LINENUMBER 15 L1
  63. RETURN
  64. L2
  65. LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
  66. MAXSTACK = 2
  67. MAXLOCALS = 1
  68. }

2.(必做)自定义一个 Classloader,加载一个 Hello.xlass 文件,执行 hello 方法,此文件内容是一个 Hello.class 文件所有字节(x=255-x)处理后的文件。文件群里提供。
3.(必做)画一张图,展示 Xmx、Xms、Xmn、Meta、DirectMemory、Xss 这些内存参数的关系。
作业2内存参数关系图.png
4.(选做)检查一下自己维护的业务系统的 JVM 参数配置,用 jstat 和 jstack、jmap 查看一下详情,并且自己独立分析一下大概情况,思考有没有不合理的地方,如何改进。
注意:如果没有线上系统,可以自己 run 一个 web/java 项目。
5.(选做)本机使用 G1 GC 启动一个程序,仿照课上案例分析一下 JVM 情况。