【初级01】java JVM核心技术(1):字节码、类加载器、GC机制

本文介绍了Java作为面向对象、静态类型、有虚拟机和垃圾回收的语言,其跨平台性和向前兼容性的特点。对比了Java与C++内存管理的区别,并详细阐述了Java程序的运行过程,字节码技术,包括字节码的组成和分类。接着讲解了JVM的类加载器机制,从加载到卸载的全过程,以及类加载的触发条件。最后讨论了JVM内存模型,包括栈和堆的分配以及对象生命周期,以及JVM启动参数的设置选项。
摘要由CSDN通过智能技术生成

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 类加载器

  1.  加载(Loading):找 Class 文件
  2.  验证(Verification):验证格式、依赖
  3.  准备(Preparation):静态字段、方法表
  4.  解析(Resolution):符号解析为引用
  5.  初始化(Initialization):构造器、静态变量赋值、静态代码块
  6. 使用(Using)
  7. 卸载(Unloading)

类的加载时机

  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 时,也不会触
发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。Class.forName
(“jvm.Hello”)默认会加载 Hello 类。
6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作(加载了,但是
不初始化)。    

三类加载器

  1. 启动类加载器(BootstrapClassLoader)(C++实现)
  2. 扩展类加载器(ExtClassLoader)(真实的java类)
  3. 应用类加载器(AppClassLoader)(真实的java类)

加载器的特点

  1. 双亲委托
  2. 负责依赖
  3. 缓存加载

添加引用类的几种方式

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"

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值