1.编程语言概述
编程语言有不同的分类方法:
- 面向过程、面向对象、面向函数
- 静态类型、动态类型
- 编译执行、解释执行
- 有虚拟机、无虚拟机
- 有GC、无GC
Java语言是一种面向对象、静态类型、编译执行,有虚拟机、有垃圾回收器和运行时的跨平台高级语言。
2.编程语言的跨平台性
C++要想跨平台,只能是在源代码级别的,编写跨平台的代码,然后再到不同的平台上编译编译后运行;Java则能达到字节码级别的跨平台,java代码编译后,生成class文件,在不同的平台均可运行。我的另一篇Java【中级01】文章里面有更为深入的探讨。
Java不仅跨平台,而且是向前兼容的。现在java最新版本已经到Java17了,但很多公司使用的还是java8,Java8版本的代码能正常运行在java17的环境下的。(其实这个兼容性到底是向前还是向后,要分不同的角度,我也没仔细研究,用到再查吧。无非就是一些特性能用不能用、或者功能有些许变化的问题,这里我就不求甚解了,阁下感兴趣的话详细可以参考此处)
注:
向前兼容对应的英文是 forward compatibility 或 upward compatibility。
向后兼容对应的英文是 backward compatibility 或 downward compatibility。
向前兼容表示旧版本的系统可以接受新版本的数据,是旧版本对新版本的兼容。
向后兼容表示新版本的系统可以接受旧版本的数据,是新版本对旧版本的兼容。
3.C++和Java内存管理的差别
C/C++完全相信而且惯着程序员,让大家自行管理内存,可以编写很自由的代码,但一不小心就会造成内存泄漏等问题,导致程序崩溃。
Java/Golang完全不相信程序员,但也惯着程序员。所有的内存生命周期都由JVM运行时统一管理。在绝大部分场景下,你可以非常自由的写代码,而且不用关心内存到底是什么情况。内存使用有问题的时候,我们可以通过JVM来进行信息相关的分析诊断和调整。
Rust语言选择既不相信程序员,也不惯着程序员。让你在写代码的时候,必须清楚明白的用Rust
的规则管理好你的变量,好让机器能明白高效地分析和管理内存。但是这样会导致代码不利于人的理解,写代码很不自由,学习成本也很高。
4.java程序运行过程
一段源码程序,比如打印“ILOVEU”这个单词的程序,通过javac命令编译后,生成了一个class文件,这个class文件就是字节码文件,使用java命令运行这个class文件时,java虚拟机就通过类加载器加载这个class然后将它运行起来
5.java字节码技术
Java bytecode由单字节(byte)的指令组成,理论上最多支持256个操作码(opcode)。
实际上Java只使用了200左右的操作码,还有一些操作码则保留给调试操作。根据指令的性质,主要分为四个大类:
1.栈操作指令,包括与局部变量交互的指令
2.程序流程控制指令
3.对象操作指令,包括方法调用指令
4.算术运算以及类型转换指令
package outputs;
public class HelloByteCode{
public static void main(String[] args){
HelloByteCode obj = new HelloByteCode();
}
}
编译:javac HelloByteCode.java
生成了HelloByteCode.class
查看字节码:javap -c HelloByteCode.class
显示的字节码是以助记符的方式给出的
Compiled from "HelloByteCode.java"
public class outputs.HelloByteCode {
public outputs.HelloByteCode();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class outputs/HelloByteCode
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
}
两个基本的栈操作命令(操作码):
- aload_0为加载对象,a表示对象的引用,0为对象标识号
- astore_1,存数据
在jvm里面,所有的计算都是在栈上的,但程序中的变量和一些值都是存放在本地变量表里的,所以在执行时,就需要通过load指令把数据加载到栈上,运算完成后再通过store指令保存回本地变量表中。类似的还有iload_0, istore_0
执行javap -c -verbose HelloByteCode.class命令,查看更为详细的信息
Classfile /E:/outputs/HelloByteCode.class
Last modified 2021年12月10日; size 296 bytes
MD5 checksum b6e3ee2ab93cf95186a2eadc6c4ecb21
Compiled from "HelloByteCode.java"
public class outputs.HelloByteCode
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // outputs/HelloByteCode
super_class: #4 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
#2 = Class #14 // outputs/HelloByteCode
#3 = Methodref #2.#13 // outputs/HelloByteCode."<init>":()V
#4 = Class #15 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 SourceFile
#12 = Utf8 HelloByteCode.java
#13 = NameAndType #5:#6 // "<init>":()V
#14 = Utf8 outputs/HelloByteCode
#15 = Utf8 java/lang/Object
{
public outputs.HelloByteCode();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class outputs/HelloByteCode
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
LineNumberTable:
line 7: 0
line 9: 8
}
SourceFile: "HelloByteCode.java"
JVM是一台基于栈的计算机器。每个线程都有一个独属于自己的线程栈(JVM Stack),用于存储栈帧(Frame)。
每一次方法调用,JVM都会自动创建一个栈帧。栈帧由操作数栈,局部变量数组以及一个Class引用组成。Class引用指向当前方法在运行时常量池中对应的Class。
字节码中的算术操作与类型转换操作码助记符
i2d int转换为double类型
其他常用操作码的助记符
istore_0 iload_0 int类型的值加载和保存
iconst_1常量,后面的1是真实的常量值
if_icmpge/goto 构成一个循环
invokestatic,顾名思义,这个指令用于调用某个类的静态方法,这是方法调用指令中最
快的一个。
invokespecial, 用来调用构造函数,但也可以用于调用同一个类中的 private 方法, 以及
可见的超类方法。
invokevirtual,如果是具体类型的目标对象,invokevirtual 用于调用公共,受保护和
package 级的私有方法。
invokeinterface,当通过接口引用来调用方法时,将会编译为 invokeinterface 指令。
invokedynamic,JDK7 新增加的指令,是实现“动态类型语言”(Dynamically Typed
Language)支持而进行的升级改进,同时也是 JDK8 以后支持 lambda 表达式的实现基
础。
6.JVM 类加载器
- 加载(Loading):找 Class 文件
- 验证(Verification):验证格式、依赖
- 准备(Preparation):静态字段、方法表
- 解析(Resolution):符号解析为引用
- 初始化(Initialization):构造器、静态变量赋值、静态代码块
- 使用(Using)
- 卸载(Unloading)
类的加载时机
- 当虚拟机启动时,初始化用户指定的主类,就是启动执行的 main 方法所在的类;
- 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类,就是 new一个类的时候要初始化;
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
- 子类的初始化会触发父类的初始化;
- 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射 API 对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化;
- 当初次调用 MethodHandle 实例时, 初始化该 MethodHandle 指向的方法所在的类
不会初始化(可能会加载)
1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
2. 定义对象数组,不会触发该类的初始化。
3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不
会触发定义常量所在的类。
4. 通过类名获取 Class 对象,不会触发类的初始化,Hello.class 不会让 Hello 类初始
化。
5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触
发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。Class.forName
(“jvm.Hello”)默认会加载 Hello 类。
6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作(加载了,但是
不初始化)。
三类加载器
- 启动类加载器(BootstrapClassLoader)(C++实现)
- 扩展类加载器(ExtClassLoader)(真实的java类)
- 应用类加载器(AppClassLoader)(真实的java类)
加载器的特点
- 双亲委托
- 负责依赖
- 缓存加载
添加引用类的几种方式
1、放到 JDK 的 lib/ext 下,或者-Djava.ext.dirs
2、 java –cp/classpath 或者 class 文件放到当前路径
3、自定义 ClassLoader 加载
4、拿到当前执行类的 ClassLoader,反射调用 addUrl 方法添加 Jar 或路径(JDK9 无效)。
public class JvmAppClassLoaderAddURL{
public static void main(String[] args){
String appPath = "file:/d:/app/";
URLCalssLoader urlClassLoader = ()
}
}
7.JVM内存模型
每个线程都只能访问自己的线程栈。
每个线程都不能访问(看不见)其他线程的局部变量。
所有原生类型的局部变量都存储在线程栈中,因此对其他线程是不可见的。
线程可以将一个原生变量值的副本传给另一个线程,但不能共享原生局部变量本身。
堆内存中包含了 Java 代码中创建的所有对象,不管是哪个线程创建的。 其中也涵盖了包装类型(例如 Byte,Integer,Long 等)。
不管是创建一个对象并将其赋值给局部变量, 还是赋值给另一个对象的成员变量, 创建的对象都会被保存到堆内存中。
如果是原生数据类型的局部变量,那么它的内容就全部保留在线程栈上。
如果是对象引用,则栈中的局部变量槽位中保存着对象的引用地址,而实际的对象内容保存在堆中。
对象的成员变量与对象本身一起存储在堆上, 不管成员变量的类型是原生数值,还是对象引用。
类的静态变量则和类定义一样都保存在堆中。
方法中使用的原生数据类型和对象引用地址在栈上存储;
对象、对象成员与类定义、静态变量在堆上。
堆内存又称为“共享堆”,堆中的所有对象,可以被所有线程访问, 只要他们能拿到对象的引用地址。
如果一个线程可以访问某个对象时,也就可以访问该对象的成员变量。
如果两个线程同时调用某个对象的同一方法,则它们都可以访问到这个对象的成员变量,但每个线程的局部变量副本是独立的。
每启动一个线程,JVM 就会在栈空间栈分配对应的 线程栈, 比如 1MB 的空间(-Xss1m)。
线程栈也叫做 Java 方法栈。 如果使用了JNI 方法,则会分配一个单独的本地方法栈(NativeStack)。线程执行过程中,一般会有多个方法组成调用栈(Stack Trace), 比如 A 调用 B,B调用 C......每执行到一个方法,就会创建对应的 栈帧(Frame)。
栈帧是一个逻辑上的概念,具体的大小在一个方法编写完成后基本上就能确定。
比如返回值 需要有一个空间存放吧,每个局部变量都需要对应的地址空间,此外还有给指令使用的 操作数栈,以及 class 指针(标识这个栈帧对应的是哪个类的方法,指向非堆里面的 Class 对象)。
堆内存是所有线程共用的内存空间,JVM 将Heap 内存分为年轻代(Young generation)和老年代(Old generation, 也叫 Tenured)两部分。年轻代还划分为 3 个内存池,新生代(Edenspace)和存活区(Survivor space), 在大部分GC 算法中有 2 个存活区(S0, S1),在我们可以观察到的任何时刻,S0 和 S1 总有一个是空的,但一般较小,也不浪费多少空间。Non-Heap 本质上还是 Heap,只是一般不归 GC管理,里面划分为 3 个内存池。Metaspace, 以前叫持久代(永久代, Permanentgeneration), Java8 换了个名字叫 Metaspace.CCS, Compressed Class Space, 存放 class 信息的,和 Metaspace 有交叉。Code Cache, 存放 JIT 编译器编译后的本地机器代码。
8.JVM启动参数
1. 系统属性参数
-Dfile.encoding=UTF-8
-Duser.timezone=GMT+08
-Dmaven.test.skip=true
-Dio.netty.eventLoopThreads=8
设置方法: System.setProperty("a","A100");
String a=System.getProperty("a");
2. 运行模式参数
1. -server:设置 JVM 使用 server 模式,特点是启动速度比较慢,
但运行时性能和内存管理效率很高,适用于生产环境。
在具有 64 位能力的 JDK 环境下将默认启用该模式,而忽略 -client 参数。
2. -client :JDK1.7 之前在32位的 x86 机器上的默认值是 -client 选项。
设置 JVM 使用 client 模式,特点是启动速度比较快,
但运行时性能和内存管理效率不高,通常用于客户端应用程序或者 PC 应用开发和调试。
此外,我们知道 JVM 加载字节码后,可以解释执行,也可以编译成本地代码再执行,
所以可以配置 JVM 对字节码的处理模式
3. -Xint:在解释模式(interpreted mode)下运行,
-Xint 标记会强制 JVM 解释执行所有的字节码,这当然会降低运行速度,通常低10倍或更多。
4. -Xcomp:-Xcomp 参数与-Xint 正好相反,JVM 在第一次使用时会
把所有的字节码编译成本地代码,从而带来最大程度的优化。【注意预热】
5. -Xmixed:-Xmixed 是混合模式,将解释模式和编译模式进行混合使用,
有 JVM 自己决定,这是 JVM 的默认模式,也是推荐模式。
我们使用 java -version 可以看到 mixed mode 等信息。
3. 堆内存设置参数
-Xmx, 指定最大堆内存。 如 -Xmx4g. 这只是限制了 Heap 部分的最大值为4g。
这个内存不包括栈内存,也不包括堆外使用的内存。
-Xms, 指定堆内存空间的初始大小。 如 -Xms4g。 而且指定的内存大小,并
不是操作系统实际分配的初始值,而是GC先规划好,用到才分配。 专用服务
器上需要保持 –Xms 和 –Xmx 一致,否则应用刚启动可能就有好几个 FullGC。
当两者配置不一致时,堆内存扩容可能会导致性能抖动。
-Xmn, 等价于 -XX:NewSize,使用 G1 垃圾收集器 不应该 设置该选项,在其
他的某些业务场景下可以设置。官方建议设置为 -Xmx 的 1/2 ~ 1/4.
-XX:MaxPermSize=size, 这是 JDK1.7 之前使用的。Java8 默认允许的
Meta空间无限大,此参数无效。
-XX:MaxMetaspaceSize=size, Java8 默认不限制 Meta 空间, 一般不允许设
置该选项。
-XX:MaxDirectMemorySize=size,系统可以使用的最大堆外内存,这个参
数跟 -Dsun.nio.MaxDirectMemorySize 效果相同。
-Xss, 设置每个线程栈的字节数。 例如 -Xss1m 指定线程栈为 1MB,与-
XX:ThreadStackSize=1m 等价
4. GC 设置参数
-XX:+UseG1GC:使用 G1 垃圾回收器
-XX:+UseConcMarkSweepGC:使用 CMS 垃圾回收器
-XX:+UseSerialGC:使用串行垃圾回收器
-XX:+UseParallelGC:使用并行垃圾回收器
// Java 11+
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
// Java 12+
-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC
5. 分析诊断参数
-XX:+-HeapDumpOnOutOfMemoryError 选项, 当 OutOfMemoryError 产生,即内存溢出(堆内存或持久代)时,
自动 Dump 堆内存。
示例用法: java -XX:+HeapDumpOnOutOfMemoryError -Xmx256m ConsumeHeap
-XX:HeapDumpPath 选项, 与 HeapDumpOnOutOfMemoryError 搭配使用, 指定内存溢出时 Dump 文件的目
录。
如果没有指定则默认为启动 Java 程序的工作目录。
示例用法: java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/ ConsumeHeap
自动 Dump 的 hprof 文件会存储到 /usr/local/ 目录下。
-XX:OnError 选项, 发生致命错误时(fatal error)执行的脚本。
例如, 写一个脚本来记录出错时间, 执行一些命令, 或者 curl 一下某个在线报警的 url.
示例用法:java -XX:OnError="gdb - %p" MyApp
可以发现有一个 %p 的格式化字符串,表示进程 PID。
-XX:OnOutOfMemoryError 选项, 抛出 OutOfMemoryError 错误时执行的脚本。
-XX:ErrorFile=filename 选项, 致命错误的日志文件名,绝对路径或者相对路径。
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=1506,远程调试
6. JavaAgent 参数
Agent 是 JVM 中的一项黑科技, 可以通过无侵入方式来做很多事情,比如注入 AOP 代码,执行统
计等等,权限非常大。这里简单介绍一下配置选项,详细功能需要专门来讲。
设置 agent 的语法如下:
-agentlib:libname[=options] 启用 native 方式的 agent, 参考 LD_LIBRARY_PATH 路径。
-agentpath:pathname[=options] 启用 native 方式的 agent。
-javaagent:jarpath[=options] 启用外部的 agent 库, 比如 pinpoint.jar 等等。
-Xnoagent 则是禁用所有 agent。
以下示例开启 CPU 使用时间抽样分析:
JAVA_OPTS="-agentlib:hprof=cpu=samples,file=cpu.samples.log"