A-1.编程语言的分类,特点
- 面向对象,面向过程,面向函数
- 静态类型,动态类型
- 编译执行,解释执行
- 有虚拟机,无虚拟机(VM)
- 有GC,无GC(Garbege Collector,JVM内存管理器)
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个左右的操作码,还有一些操作码保留给调试操作。
字节码(操作码)按性质分类
- 栈操作指令,包括与局部变量交互的指令:JVM是基于栈的真实计算机操作指令,此指令是虚拟机本身结构需要存在。跟JAVA语言代码不直接对应,是为了能够像计算机运行CPU指令一样运行所设计的指令。
- 程序流程控制指令:for循环,if else判断
- 对象操作指令,包括方法调用指令:JAVA面向对象
- 算术运算以及类型转换指令:加减乘除,数据类型相互转换
编译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。
CPU本身相当于操作的是栈JVM Stack,合起来就像是个计算机了。
本地变量表就相当于是磁盘或者数据库
计算CPU处理 — 操作数据栈
A-3.JVM类加载器
类的生命周期(前5步为类加载过程)
- 加载loading:找Class文件,不管是在文件夹里还是jar包里。
- 验证Verification:验证字节码格式是否正确,依赖是否完备。
- 准备Prepareation:静态字段,方法表
- 解析Resolution:把符号解析为实际的引用
- 初始化Initialization:构造器,静态变量赋值,静态代码块
- 使用Using
- 卸载Unloading:由类加载器执行
Q:初始化和加载的区别是什么?
类的加载时机(什么时候被JVM加载)
- (显式调用)当虚拟机启动时,初始化用户指定的主类,也就是启动执行main方法所在的类。
- (显式调用)当遇到以新建目标类实例的new指令时,初始化new指令的目标类。也就是new一个类的时候要初始化。
- (显式调用)当遇到调用静态方法的指令时,初始化该静态方法所在的类。
- (显式调用)当遇到访问静态字段的指令时,初始化该静态字段所在的类。
- (隐式调用)子类的初始化会触发父类的初始化。
- (隐式调用)如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化。
- (隐式调用)使用反射API对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化。
- (隐式调用)当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。
不会初始化(可能会加载)
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取Class对象,不会触发类的初始化,Hello.Class不会让Hello类初始化。
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化,默认为true。另:Class.forName(“jvm.Hello”)会加载Hello类。
- 通过ClassLoader默认的loadClass就去,也不会触发初始化动作(加载了,但是不初始化)。
JVM内部类加载器种类
- 启动类加载器(BootstrapClassLoader):在JVM底层实现,JDK中找不到此加载器的定义。加载JVM最核心的一些jar包和类以及公共类,如:Object,String,rf.jar
- 扩展类加载器(ExtClassLoader):核心类公共类以外的额外的类
- 应用类加载器(AppClassLoader):我们自己编写的java类,以及classpath中指定的jar包
- 自定义类加载器
package com.zhukova.geektrain.jvm;
import sun.management.snmp.jvminstr.JvmClassLoadingImpl;
import sun.management.snmp.jvmmib.JvmClassLoadingMBean;
import sun.management.snmp.jvmmib.JvmClassLoadingMeta;
import java.net.URL;
import java.util.Scanner;
public class MainApplication {
public static void main(String[] args){
// 应用类加载器
ClassLoader appClassLoader = JvmClassLoadingMeta.class.getClassLoader();
printClassLoader("应用类加载器AppClassLoader", appClassLoader);
// 扩展类加载器
ClassLoader extClassLoader = appClassLoader.getParent();
printClassLoader("扩展类加载器ExtClassLoader", extClassLoader);
// 启动类加载器(JVM底层C++编写,JDK中无法获取)
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
printClassLoader("启动类加载器BootstrapClassLoader", bootstrapClassLoader);
// 启动类加载器:代码层面拿不到,使用静态方法取得ClassPath
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
System.out.println("启动类加载器BootstrapClassLoader");
for (URL url : urls){
// 7个jar包,1个文件夹classes
System.out.println(url.toExternalForm());
}
}
private static void printClassLoader(String pLoaderName, ClassLoader pClassLoader){
System.out.println(pLoaderName);
if (pClassLoader == null) {
System.out.println(pLoaderName + " --> null");
} else {
System.out.println(pClassLoader);
}
}
}
Q:用JDK8找不到JvmClassLoaderPrintPath
加载器特点
- 双亲委托
- 负责依赖
- 缓存加载:默认情况下一个类只会被加载一次,放在缓存里,使用时从缓存读取,效率高。
添加引用类的几种方式
- 把.class类文件放到JDK的lib/ext下。==>被扩展类加载器ExtClassLoader加载
- 启动Java进程命令时,使用参数-Djava.ext.dirs额外地添加指定JDK使用的class的扩展路径或者jar包的扩展路径。 ==>被扩展类加载器ExtClassLoader加载
- 用命令java -cp或者java -classpath,可以让JVM引用指定路径里的class或者jar包。
- 把class文件放到当前路径,一般是classpath路径,
- 自定义ClassLoader加载
- 拿到当前执行类的ClassLoader,反射调用URLClassLoader.allUrl方法添加Jar包或路径。注意:JDK9以后无效,可以使用ClassForName(),new一个URLClassLoader出来。
A-4.内存模型
- 每个线程都只能访问自己的线程栈
- 每个线程都不能访问其他线程的局部变量
- 即使两个线程正在执行完全相同的代码,但每个线程都会在自己的线程栈内创建对应代码中声明的局部变量,所以每个线程都有一份自己的局部变量副本。
- 所有原生类型的局部变量都存储在线程栈中,因此对其他线程是不可见的。
- 线程可以将一个原生变量值的副本传给另一个线程,但不能共享原生局部变量本身。
- 堆内存中包含了JAVA代码中创建的所有对象,不管是哪个线程创建的。其中也涵盖了包装类型(Byte,Integer,Long等)
- 不管是创建一个对象并将其赋值给局部变量,还是赋值给另一个对象的成员变量,创建的对象都会被保存到堆内存中。
- 如果是原生数据类型的局部变量,那么它的内容就全部保留在线程栈上。
- 如果是对象引用,则栈中的局部变量槽位中保存着对象的引用地址,而实际的对象内容保存在堆中。
- 对象的成员变量与对象本身一起存储在堆上,不管成员变量的类型是原生数值,还是对象引用。
- 类的静态变量则和类定义一样都保存在堆中。
A-5.启动参数
现在一般认为JVM有1000+参数。如使用-Xms -Xmx参数配置内存分配。
java [options] classname [args]
java [options] -jar filename [args]
IDEA中配置参数:
JVM启动参数语法种类
- -:标准参数。所有的JVM都要实现这些参数,并且它是稳定向后兼容。如:-server
- -D:设置系统属性。给当前程序设置一个特殊的环境变量,会覆盖掉系统环境变量。如:-Dfile.encoding=UTF-8
- -X:非标准参数。默认JVM支持实现此类参数功能,但不保证所有的JVM的实现都支持,也不保证向后兼容。可使用java -X命令来查看当前JVM支持的非标准参数。如:-Xms2048m -Xmx8g
- -XX:非稳定参数。用于控制JVM一些具体行为,跟具体的JVM实现版本有关,随时可能会在下个版本取消。有两种形式:
4-1.-XX:+-Flags形式用于对布尔值Flags进行开关,+表示开,-表示关。
4-2.-XX:key=value形式,指定某个选项key的值。如:-XX:MaxPermSize=256m
JVM启动参数功能分类
- 系统属性参数,如环境变量,但在命令行中配置只影响当前进程,在操作系统中配置影响的是整个系统。-Da=A100相当于java里的System.setProperty(“a”, “A100”); String a = System.getProperty(“a”);
- 运行模式参数,如标准参数-server
-server:设置JVM使用server模式,启动速度慢,但运行时性能和内存管理效率高,适用于Web应用程序或服务端应用程序的生产环境。在具有64位JDK环境下默认启用该模式。
-client:设置JVM使用client模式,启动速度快,但运行时性能和内存管理效率低,适用于客户端应用程序或者PC应用开发和调试。JDK1.7之前在32位x86机器上默认启用该模式。
-Xint:设置JVM在解释模式(interpreted mode)下运行字节码,而不是把字节码编译成机器码执行,会降低运行速度。
-Xcomp:设置JVM在编译模式下运行本地机器码,带来最大程度的优化,执行速度较快。但需要编译所有的字节码成为本地机器码并存入缓存,此预热活动开销较大。
-Xmixed:混合模式,平衡解释模式和编译模式,把运行多次的热点代码编译成机器码,不编译运行次数较少的冷代码,降低编译成本。
- 堆内存设置参数
-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。
- GC设置参数:Q:JAVA8默认的GC算法是什么?
-XX:+-UseG1GC:使用G1垃圾回收器
-XX:+-UseConcMarkSweepGC:使用CMS垃圾回收器
-XX:+-UseSerialGC:使用串行垃圾回收器
-XX:+-UseParallelGC:使用并行垃圾回收器
-XX:+-UnlockExperimentalVMOptions -XX:+-UseZGC:JDK11以上使用
-XX:+-UnlockExperimentalVMOptions -XX:+-UseShenandoahGC:JDK12以上使用
- 分析诊断参数
-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
- 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,然后自己分析一下对应的字节码,有问题群里讨论。
package com.zhukova.geektrain.jvm;
public class HelloByte {
HelloByte() {
String str = "StrHello:";
int m = 10, n = 2;
if (m > n) {
System.out.println(str + m / 2);
}
}
public static void main(String[] args) {
new HelloByte();
}
}
// class version 52.0 (52)
// access flags 0x21
public class com/zhukova/geektrain/jvm/HelloByte {
// compiled from: HelloByte.java
// access flags 0x0
<init>()V
L0
LINENUMBER 4 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 5 L1
LDC "StrHello"
ASTORE 1
L2
LINENUMBER 6 L2
BIPUSH 10
ISTORE 2
L3
ICONST_2
ISTORE 3
L4
LINENUMBER 8 L4
ILOAD 2
ILOAD 3
IF_ICMPLE L5
L6
LINENUMBER 9 L6
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 2
ICONST_2
IDIV
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L5
LINENUMBER 11 L5
FRAME FULL [com/zhukova/geektrain/jvm/HelloByte java/lang/String I I] []
RETURN
L7
LOCALVARIABLE this Lcom/zhukova/geektrain/jvm/HelloByte; L0 L7 0
LOCALVARIABLE str Ljava/lang/String; L2 L7 1
LOCALVARIABLE m I L3 L7 2
LOCALVARIABLE n I L4 L7 3
MAXSTACK = 4
MAXLOCALS = 4
// access flags 0x9
public static main([Ljava/lang/String;)V
// parameter args
L0
LINENUMBER 14 L0
NEW com/zhukova/geektrain/jvm/HelloByte
DUP
INVOKESPECIAL com/zhukova/geektrain/jvm/HelloByte.<init> ()V
POP
L1
LINENUMBER 15 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}
2.(必做)自定义一个 Classloader,加载一个 Hello.xlass 文件,执行 hello 方法,此文件内容是一个 Hello.class 文件所有字节(x=255-x)处理后的文件。文件群里提供。
3.(必做)画一张图,展示 Xmx、Xms、Xmn、Meta、DirectMemory、Xss 这些内存参数的关系。
4.(选做)检查一下自己维护的业务系统的 JVM 参数配置,用 jstat 和 jstack、jmap 查看一下详情,并且自己独立分析一下大概情况,思考有没有不合理的地方,如何改进。
注意:如果没有线上系统,可以自己 run 一个 web/java 项目。
5.(选做)本机使用 G1 GC 启动一个程序,仿照课上案例分析一下 JVM 情况。