JVM性能调优(1) —— JVM内存模型和类加载运行机制
一、JVM内存模型运行一个 Java 应用程序,必须要先安装 JDK 或者 JRE 包。因为 Java 应用在编译后会变成字节码,通过字节码运行在 JVM 中,而 JVM 是 JRE 的核心组成部分。JVM 不仅承担了 Java 字节码的分析和执行,同时也内置了自动内存分配管理机制。这个机制可以大大降低手动分配回收机制可能带来的内存泄露和内存溢出风险,使 Java 开发人员不需要关注每个对象的内存分配以及回收,从而更专注于业务本身。 在 Java 中,JVM 内存模型主要分为堆、方法区、程序计数器、虚拟机栈和本地方法栈。其中,堆和方法区被所有线程共享,虚拟机栈、本地方法栈、程序计数器是线程私有的。 1、堆堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。 但需要注意的是,这些区域的划分因不同的垃圾收集器而不同。大部分垃圾收集器都是基于分代收集理论设计的,就会采用这种分代模型。而一些新的垃圾收集器不采用分代设计,比如 G1 收集器就是把堆内存拆分为多个大小相等的 Region。 2、方法区在 jdk8 之前,HotSopt 虚拟机的方法区又被称为永久代,由于永久代的设计容易导致内存溢出等问题,jdk8 之后就没有永久代了,取而代之的是元空间(MetaSpace)。元空间并没有处于堆内存上,而是直接占用的本地内存,因此元空间的最大大小受本地内存限制。 方法区与堆空间类似,是所有线程共享的。方法区主要是用来存放已被虚拟机加载的类型信息、常量、静态变量等数据。方法区是一个逻辑分区,包含元空间、运行时常量池、字符串常量池,元空间物理上使用的本地内存,运行时常量池和字符串常量池是在堆中开辟的一块特殊内存区域。这样做的好处之一是可以避免运行时动态生成的常量的复制迁移,可以直接使用堆中的引用。要注意的是,字符串常量池在 jvm 中只有一个,而运行时常量池是和类型数据绑定的,每个 Class 一个。 1)类型信息(类或接口):
2)运行时常量池:
3)字段信息:
4)方法信息:
5)指向类加载器的引用:
6)指向 Class 类的引用:
3、虚拟机栈每当启动一个新的线程,虚拟机都会在虚拟机栈里为它分配一个线程栈,线程栈与线程同生共死。线程栈以 栈帧 为单位保存线程的运行状态,虚拟机只会对线程栈执行两种操作:以栈帧为单位的压栈或出栈。每个方法在执行的同时都会创建一个栈帧,每个方法从调用开始到结束,就对应着一个栈帧在线程栈中压栈和出栈的过程。方法可以通过两种方式结束,一种通过 return 正常返回,一种通过抛出异常而终止。方法返回后,虚拟机都会弹出当前栈帧然后释放掉。 当虚拟机调用一个Java方法时.它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入Java栈中。 栈帧由三部分组成:局部变量区、操作数栈、帧数据区。 1)局部变量区:
2)操作数栈:
3)帧数据区:主要保存常量池入口、异常表、正常方法返回的信息
4、本地方法栈本地方法栈与虚拟机栈所发挥的作用是相似的,当线程调用Java方法时,会创建一个栈帧并压入虚拟机栈;而调用本地方法时,虚拟机会保持栈不变,不会压入新的栈帧,虚拟机只是简单的动态链接并直接调用指定的本地方法,使用的是某种本地方法栈。比如某个虚拟机实现的本地方法接口是使用C连接模型,那么它的本地方法栈就是C栈。 本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,它可以做任何他想做的事情,本地方法不受虚拟机控制。 5、程序计数器每一个运行的线程都会有它的程序计数器(PC寄存器),与线程的生命周期一样。执行某个方法时,PC寄存器的内容总是下一条将被执行的地址,这个地址可以是一个本地指针,也可以是在方法字节码中相对于该方法起始指令的偏移量。如果该线程正在执行一个本地方法,那么此时PC寄存器的值是 undefined。 程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。多线程环境下,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。 二、类加载机制写好的源代码,需要编译后加载到虚拟机才能运行。java 源文件编译成 class 文件后,jvm 通过类加载器把 class 文件加载到虚拟机,然后经过类连接(类连接又包括验证、准备、解析三个阶段),最后经过初始化,字节码就可以被解释执行了。对于一些热点代码,虚拟机还存在一道即时编译,会把字节码编译成本地平台相关的机器码,以提高热点代码的执行效率。 装载、验证、准备、初始化这几个阶段的顺序是确定的,类型的加载过程必须按照这种顺序开始,而解析阶段可以在初始化阶段之后再开始,一般是在第一次使用到这个对象时才会开始解析。这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段,比如发现引用了另一个类,那么就会先触发另一个类的加载过程。 接下来通过如下类和代码来详细分析下类加载的过程: 1 package com.lyyzoo.jvm.test01; 2 3 public class Person<T> { 4 5 static final String SEX_MAN = "Male"; 6 final String SEX_WOMAN = "Female" 7 8 static 9 System.out.println("Person static init"); 10 System.out.println("SEX_MAN: " + SEX_MAN); 11 } 12 13 void sayHello(T str) { 14 System.out.println("Person say hello: " + str); 15 16 } 17 18 19 ///////////////////////////////////////////////////////////////////// 20 21 22 23 24 import java.io.Serializable; 25 26 class User extends Person<String> implements Serializable { 27 private final long serialVersionUID = -4482416396338787067L 28 29 // 静态常量 30 final String FIELD_NAME = "username" 31 int AGE_MAX = 100 32 33 静态变量 34 static String staticName = "Rambo" 35 int staticAge = 20 36 37 类属性 38 private String name = "兰博" 39 int age = 25 40 41 静态代码块 42 43 System.out.println("user static init" 44 System.out.println("staticName=" + staticName); 45 System.out.println("staticAge=" + staticAge); 46 47 48 public User() { 49 50 51 public User(String name,int age) { 52 this.name = name; 53 this.age = age; 54 55 56 实例方法 57 printInfo() { 58 System.out.println("name:" + name + ",age:" + age); 59 60 61 静态方法 62 staticPrintInfo() { 63 System.out.println("FIELD_NAME:" + FIELD_NAME + ",AGE_MAX:" + AGE_MAX); 64 65 66 泛型方法重载 67 @Override 68 sayHello(String str) { 69 super.sayHello(str); 70 System.out.println("User say hello: " + 71 72 73 方法将抛出异常 74 willThrowException() { 75 int i = 0 76 try 77 int r = 10 / i; 78 return r; 79 } catch (Exception e) { 80 System.out.println("catch exception" 81 82 } finally 83 System.out.println("finally handle" 84 } 85 86 87 88 89 90 91 92 93 94 class Main { 95 96 main(String[] args) { 97 System.out.println("FIELD_NAME: " + User.FIELD_NAME); 98 99 User.staticPrintInfo(); 100 101 User user = new User(); 102 user.printInfo(); 103 104 } 三、类编译和Class 文件结构*.java 文件被编译成 *.class 文件的过程,这个编译一般称为前端编译,主要使用 javac 来完成前端编译。Java class文件是8位字节的二进制流,数据项按顺序存储在class文件中,相邻的项之间没有任何间隔,这样可以使class文件紧凑。class 文件主要包含 版本信息、常量池、类型索引、字段表、方法表、属性表等信息。 将 User 类编译成 class 文件后,再通过 javap 反编译 class 文件,可以看到一个 class 文件大体包含的结构: 说明:用“【】”标识的是手动添加的注释 3 Mechrevo@hello-world MINGW64 /e/repo-study/test-concurrent/target/classes/com/lyyzoo/jvm/test01 4 【javap -v 命令反编译 Class】 5 $ javap -v User.class 6 Classfile /E:/repo-study/test-concurrent/target/classes/com/lyyzoo/jvm/test01/User. 7 Last modified 2020-9-3; size 2389 bytes 8 【魔数】 9 MD5 checksum ec5a961c2a46926522bafddcb3204fb9 10 Compiled from "User.java" class com.lyyzoo.jvm.test01.User extends com.lyyzoo.jvm.test01.Person<java.lang.String> java.io.Serializable 12 【版本号】 13 minor version: 0 14 major version: 52 flags: ACC_PUBLIC,ACC_SUPER 【常量池】 17 Constant pool: 18 #1 = Methodref #29.#76 com/lyyzoo/jvm/test01/Person."<init>":()V 19 #2 = String #77 兰博 20 #3 = Fieldref #14.#78 com/lyyzoo/jvm/test01/User.name:Ljava/lang/String; 21 #4 = Fieldref #14.#79 com/lyyzoo/jvm/test01/User.age:I 22 #5 = Fieldref #80.#81 java/lang/System.out:Ljava/io/PrintStream; 23 #6 = Class #82 java/lang/StringBuilder 24 #7 = Methodref #6.#76 java/lang/StringBuilder."<init>":()V 25 #8 = String #83 name: 26 #9 = Methodref #6.#84 java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 27 #10 = String #85 ,age: 28 #11 = Methodref #6.#86 java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 29 #12 = Methodref #6.#87 java/lang/StringBuilder.toString:()Ljava/lang/String; 30 #13 = Methodref #88.#89 java/io/PrintStream.println:(Ljava/lang/String;)V 31 #14 = Class #90 com/lyyzoo/jvm/test01/User 32 #15 = String #91 FIELD_NAME:username,AGE_MAX:100 33 #16 = Methodref #29.#92 com/lyyzoo/jvm/test01/Person.sayHello:(Ljava/lang/Object;)V 34 #17 = String #93 User say hello: 35 #18 = String #94 finally handle 36 #19 = Class #95 java/lang/Exception 37 #20 = String #96 catch exception 38 #21 = Class #97 java/lang/String 39 #22 = Methodref #14.#98 com/lyyzoo/jvm/test01/User.sayHello:(Ljava/lang/String;)V 40 #23 = String #99 Rambo 41 #24 = Fieldref #14.#100 com/lyyzoo/jvm/test01/User.staticName:Ljava/lang/String; 42 #25 = Fieldref #14.#101 com/lyyzoo/jvm/test01/User.staticAge:I 43 #26 = String #102 user static init 44 #27 = String #103 staticName= 45 #28 = String #104 staticAge= 46 #29 = Class #105 com/lyyzoo/jvm/test01/Person 47 #30 = Class #106 java/io/Serializable 48 #31 = Utf8 serialVersionUID 49 #32 = Utf8 J 50 #33 = Utf8 ConstantValue 51 #34 = Long -4482416396338787067l 52 #36 = Utf8 FIELD_NAME 53 #37 = Utf8 Ljava/lang/String; 54 #38 = String #107 username 55 #39 = Utf8 AGE_MAX 56 #40 = Utf8 I 57 #41 = Integer 100 58 #42 = Utf8 staticName 59 #43 = Utf8 staticAge 60 #44 = Utf8 name 61 #45 = Utf8 age 62 #46 = Utf8 <init> 63 #47 = Utf8 ()V 64 #48 = Utf8 Code 65 #49 = Utf8 LineNumberTable 66 #50 = Utf8 LocalVariableTable 67 #51 = Utf8 this 68 #52 = Utf8 Lcom/lyyzoo/jvm/test01/User; 69 #53 = Utf8 (Ljava/lang/String;I)V 70 #54 = Utf8 MethodParameters 71 #55 = Utf8 printInfo 72 #56 = Utf8 staticPrintInfo 73 #57 = Utf8 sayHello 74 #58 = Utf8 (Ljava/lang/String;)V 75 #59 = Utf8 str 76 #60 = Utf8 willThrowException 77 #61 = Utf8 ()I 78 #62 = Utf8 r 79 #63 = Utf8 e 80 #64 = Utf8 Ljava/lang/Exception; 81 #65 = Utf8 i 82 #66 = Utf8 StackMapTable 83 #67 = Class #90 84 #68 = Class #95 85 #69 = Class #108 java/lang/Throwable 86 #70 = Utf8 (Ljava/lang/Object;)V 87 #71 = Utf8 <clinit> 88 #72 = Utf8 Signature 89 #73 = Utf8 Lcom/lyyzoo/jvm/test01/Person<Ljava/lang/String;>;Ljava/io/Serializable; 90 #74 = Utf8 SourceFile 91 #75 = Utf8 User.java 92 #76 = NameAndType #46:#47 "<init>":()V 93 #77 = Utf8 兰博 94 #78 = NameAndType #44:#37 name:Ljava/lang/String; 95 #79 = NameAndType #45:#40 age:I 96 #80 = Class #109 java/lang/System 97 #81 = NameAndType #110:#111 out:Ljava/io/PrintStream; 98 #82 = Utf8 java/lang/StringBuilder 99 #83 = Utf8 name: 100 #84 = NameAndType #112:#113 append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 101 #85 = Utf8,age: 102 #86 = NameAndType #112:#114 append:(I)Ljava/lang/StringBuilder; 103 #87 = NameAndType #115:#116 toString:()Ljava/lang/String; 104 #88 = Class #117 java/io/PrintStream 105 #89 = NameAndType #118:#58 println:(Ljava/lang/String;)V 106 #90 = Utf8 com/lyyzoo/jvm/test01/User 107 #91 = Utf8 FIELD_NAME:username,AGE_MAX:100 108 #92 = NameAndType #57:#70 sayHello:(Ljava/lang/Object;)V 109 #93 = Utf8 User say hello: 110 #94 = Utf8 handle 111 #95 = Utf8 java/lang/Exception 112 #96 = Utf8 exception 113 #97 = Utf8 java/lang/String 114 #98 = NameAndType #57:#58 sayHello:(Ljava/lang/String;)V 115 #99 = Utf8 Rambo 116 #100 = NameAndType #42:#37 staticName:Ljava/lang/String; 117 #101 = NameAndType #43:#40 staticAge:I 118 #102 = Utf8 user init 119 #103 = Utf8 staticName= 120 #104 = Utf8 staticAge= 121 #105 = Utf8 com/lyyzoo/jvm/test01/Person 122 #106 = Utf8 java/io/Serializable 123 #107 = Utf8 username 124 #108 = Utf8 java/lang/Throwable 125 #109 = Utf8 java/lang/System 126 #110 = Utf8 out 127 #111 = Utf8 Ljava/io/PrintStream; 128 #112 = Utf8 append 129 #113 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; 130 #114 = Utf8 (I)Ljava/lang/131 #115 = Utf8 toString 132 #116 = Utf8 ()Ljava/lang/133 #117 = Utf8 java/io/PrintStream 134 #118 = Utf8 println 135 { 136 【字段表集合】 137 final java.lang.String FIELD_NAME; 138 descriptor: Ljava/lang/139 flags: ACC_PUBLIC,ACC_STATIC,ACC_FINAL 140 ConstantValue: String username 141 142 AGE_MAX; 143 descriptor: I 144 145 ConstantValue: int 100 146 147 【方法表】 148 com.lyyzoo.jvm.test01.User(); 149 【描述符索引】 150 descriptor: ()V 151 【访问标志】 152 flags: ACC_PUBLIC 153 【方法体代码指令】 154 Code: 155 【方法栈大小】 156 stack=2,locals=1,args_size=1 157 0: aload_0 158 1: invokespecial #1 Method com/lyyzoo/jvm/test01/Person."<init>":()V 159 4160 5: ldc #2 String 兰博 161 7: putfield #3 Field name:Ljava/lang/String; 162 10163 11: bipush 25 164 13: putfield #4 Field age:I 165 16: return 166 【属性表,方法局部变量】 167 LineNumberTable: 168 line 27: 0 169 line 17: 4 170 line 18: 10 171 line 28: 16 172 【本地变量表,方法入参】 173 LocalVariableTable: 174 Start Length Slot Name Signature 175 0 17 0 this Lcom/lyyzoo/jvm/test01/176 177 public com.lyyzoo.jvm.test01.User(java.lang.String,1)">178 descriptor: (Ljava/lang/179 180 181 stack=2,locals=3,args_size=3 182 0183 1: invokespecial #1 184 4185 5: ldc #2 186 7: putfield #3 187 10188 11: bipush 25 189 13: putfield #4 190 16191 17: aload_1 192 18: putfield #3 193 21194 22: iload_2 195 23: putfield #4 196 26: 197 198 line 30: 0 199 line 17: 4 200 line 18: 10 201 line 31: 16 202 line 32: 21 203 line 33: 26 204 205 206 【可以看出,对象实例方法的第一个参数始终都是 this,这也是为什么我们可以在方法内调用 this 的原因】 207 0 27 0 208 0 27 1 name Ljava/lang/209 0 27 2 age I 210 MethodParameters: 211 Name Flags 212 name 213 age 214 215 printInfo(); 216 217 218 219 stack=3,1)">220 0: getstatic #5 Field java/lang/System.out:Ljava/io/PrintStream; 221 3: new #6 class java/lang/StringBuilder 222 6: dup 223 7: invokespecial #7 Method java/lang/StringBuilder."<init>":()V 224 10: ldc #8 String name: 225 12: invokevirtual #9 Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 226 15227 16: getfield #3 228 19: invokevirtual #9 229 22: ldc #10 String,1)">230 24: invokevirtual #9 231 27232 28: getfield #4 233 31: invokevirtual #11 Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 234 34: invokevirtual #12 Method java/lang/StringBuilder.toString:()Ljava/lang/String; 235 37: invokevirtual #13 Method java/io/PrintStream.println:(Ljava/lang/String;)V 236 40: 237 238 line 37: 0 239 line 38: 40 240 241 242 0 41 0 243 244 staticPrintInfo(); 245 246 247 248 249 stack=2,locals=0,args_size=0 250 0: getstatic #5 251 3: ldc #15 String FIELD_NAME:username,1)">252 5: invokevirtual #13 253 8: 254 255 line 42: 0 256 line 43: 8 257 【注意,静态方法第一个参数就不再是 了】 258 259 260 sayHello(java.lang.String); 261 descriptor: (Ljava/lang/262 263 264 stack=3,locals=2,args_size=2 265 0266 1267 2: invokespecial #16 Method com/lyyzoo/jvm/test01/Person.sayHello:(Ljava/lang/Object;)V 268 5: getstatic #5 269 8: 270 11271 12: invokespecial #7 272 15: ldc #17 String User say hello: 273 17: invokevirtual #9 274 20275 21: invokevirtual #9 276 24: invokevirtual #12 277 27: invokevirtual #13 278 30: 279 280 line 48: 0 281 line 49: 5 282 line 50: 30 283 284 285 【第一个参数为 】 286 0 31 0 287 0 31 1 str Ljava/lang/288 289 290 str 291 292 willThrowException(); 293 descriptor: ()I 294 295 296 stack=2,locals=5,1)">297 0: iconst_0 298 1: istore_1 299 2: bipush 10 300 4: iload_1 301 5: idiv 302 6: istore_2 303 7304 8: istore_3 305 9: getstatic #5 306 12: ldc #18 String finally handle 307 14: invokevirtual #13 308 17: iload_3 309 18: ireturn 310 19: astore_2 311 20: getstatic #5 312 23: ldc #20 String catch exception 313 25: invokevirtual #13 314 28315 29316 30: getstatic #5 317 33: ldc #18 318 35: invokevirtual #13 319 38320 39321 40: astore 4 322 42: getstatic #5 323 45: ldc #18 324 47: invokevirtual #13 325 50: aload 4 326 52: athrow 327 【方法异常表】 328 Exception table: 329 from to target type 330 2 9 19 Class java/lang/331 2 9 40 any 332 19 30 40333 40 42 40334 335 line 54: 0 336 line 56: 2 337 line 57: 7 338 line 62: 9 339 line 57: 17 340 line 58: 19 341 line 59: 20 342 line 60: 28 343 line 62: 30 344 line 60: 38 345 line 62: 40 346 line 63: 50 347 348 349 7 12 2 r I 350 20 20 2 e Ljava/lang/351 0 53 0 352 2 51 1 i I 353 StackMapTable: number_of_entries = 2 354 frame_type = 255 /* full_frame */ 355 offset_delta = 19 356 locals = [ class com/lyyzoo/jvm/test01/User,1)"> ] 357 stack = [ class java/lang/Exception ] 358 frame_type = 84 same_locals_1_stack_item 359 stack = [ Throwable ] 360 361 sayHello(java.lang.Object); 362 descriptor: (Ljava/lang/363 【重载泛型方法时,会多出 ACC_BRIDGE、ACC_SYNTHETIC 两个标志,ACC_BRIDGE代表是jvm自动生成的桥接方法,ACC_SYNTHETIC代表是jvm生成的不可见方法】 364 365 366 stack=2,1)">367 0368 1369 2: checkcast #21 class java/lang/String 370 5: invokevirtual #22 Method sayHello:(Ljava/lang/String;)V 371 8: 372 373 line 5: 0 374 375 376 0 9 0 377 378 379 str synthetic 380 381 【静态代码块】 382 {}; 383 384 flags: ACC_STATIC 385 386 stack=3,1)">387 0: ldc #23 String Rambo 388 2: putstatic #24 Field staticName:Ljava/lang/String; 389 5: bipush 20 390 7: putstatic #25 Field staticAge:I 391 10: getstatic #5 392 13: ldc #26 String user static init 393 15: invokevirtual #13 394 18: getstatic #5 395 21: 396 24397 25: invokespecial #7 398 28: ldc #27 String staticName= 399 30: invokevirtual #9 400 33: getstatic #24 401 36: invokevirtual #9 402 39: invokevirtual #12 403 42: invokevirtual #13 404 45: getstatic #5 405 48: 406 51407 52: invokespecial #7 408 55: ldc #28 String staticAge= 409 57: invokevirtual #9 410 60: getstatic #25 411 63: invokevirtual #11 412 66: invokevirtual #12 413 69: invokevirtual #13 414 72: 415 416 line 13: 0 417 line 14: 5 418 line 22: 10 419 line 23: 18 420 line 24: 45 421 line 25: 72 422 423 Signature: #73 Lcom/lyyzoo/jvm/test01/Person<Ljava/lang/String;>;Ljava/io/Serializable; 424 SourceFile: "User.java" ? 我们也可以安装 [jclasslib Bytecode viewer] 插件,就可以在IDEA中清晰地看到 Class 包含的信息: 1、魔数与Class文件信息魔数唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动。 Minior version 是次版本号,Major version 是主版本号。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加 1,所以 jdk1.8 的 Major version 是 52。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。 Access flags?用于识别类或者接口层次的访问信息,比如这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型 等等。 2、常量池虚拟机把常量池组织为入口列表,常量池中的许多入口都指向其他的常量池入口(比如引用了其它类),而且 class 文件中的许多条目也会指向常量池中的入口。列表中的第一项索引值为1,第二项索引值为2,以此类推。虽然没有索引值为0的入口,但是 constant_pool_count 会把这一入口也算进去,比如上面的 Constant pool count 为 119,而常量池实际的索引值最大为 118。 常量池主要存放两大类常量:字面量和符号引用。
常量池中每一项都是一个表,常量表主要有如下17种常量类型。 常量池的项目类型: 再理解下符号引用和直接应用:
比如看 sayHello 这个方法,首先要调用 super.sayHello,即父类 Person 的 sayHello 方法,那么第三个指令就会在常量池寻找 [#16] 这个索引,然后可以从常量池找到这个方法的相关信息,再通过 [#29] 找到 Person 类信息。 3、类索引、父类索引与接口索引Class文件中由这三项数据来确定该类型的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。 它们各自指向一个类型为 CONSTANT_Class_info 的常量表,通过?CONSTANT_Class_info 常量中的索引值可以找到定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。 4、字段表字段表用于描述接口或者类中声明的变量。Java语言中的“字段”包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。 描述符:
描述符标识字符含义: 比如从构造方法的描述符?<Ljava/lang/String;I)V> 可以看出,方法的参数包括对象类型 java.lang.String、基本类型 int,返回值为 void。 5、方法表方发表与字段表类似,方发表用于描述方法的访问标志、名称索引、描述符索引、属性表集合、代码指令等 1)异常表: 如果方法表有异常捕获的话,还会有异常表。当方法抛出异常时,就会从异常表查找能处理的异常处理器。 2)重载多出的方法: 如果父类方法在子类中被重写,那方法表中就会包含父类方法的信息,如果重写泛型方法,还会出现编译器自动添加的桥接方法。 因为泛型编译后的实际类型为 Object,如果子类泛型不是 Object,那么编译器会自动在子类中生成一个 Object 类型的桥接方法。桥接方法的内部会先做类型转换检查,然后调用重载的方法。因为我们在声明变量时一般是声明的超类,实际类型为子类,而超类方法的参数是Object类型的,因此就会调用到桥接方法,进而调用子类重载后的方法。 ?而且,当我们通过反射根据方法名获取方法时,要注意泛型重载可能获取到桥接方法,此时可以通过 method.isBridge() 方法判断是否是桥接方法。 3)类构造器和实例构造器: 方法表还包括实例构造方法 <init> 和类构造方法 <clinit> 。<init> 就是对应的实例构造器。<clinit> 是编译时将类初始化的代码搜集在一起形成的类初始化方法,如静态变量赋值、静态代码块。 初始化阶段会调用类构造器 <clinit> 来初始化类,因此其一定是线程安全的,是由虚拟机来保证的。这种机制我们可以用来实现安全的单例模式,枚举类的初始化也是在 <clinit> 方法中初始化的。 6、属性表属性表集合主要是为了正确识别Class文件而定义的一些属性,如 Code、Deprecated、ConstantValue、Exceptions、SourceFile 等等。 每一个属性,它的名称都要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示。 四、类加载1、类初始化的时机类和接口被加载的时机因不同的虚拟机可能不同,但类初始化的触发时机有且仅有六种情况:
这六种情况称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。 触发接口初始化的情况:
1)主动初始化: 从输出可以看出,对 final 常量的引用不会触发类的初始化,调用静态方法时触发了类的初始化,同时,一定会先触发父类的初始化,而且类只会被初始化一次。 注意初始化的顺序是按代码的顺序从上到下初始化: 2)被动初始化,如下被动引用不会触发类的初始化:
3)不难判断,例子中定义的类的加载顺序如下: 2、加载在加载阶段,Java虚拟机必须完成以下三件事情:
这个二进制流可以从 Class 文件中获取,从JAR包、WAR包中获取,从网络中获取,实时生成、还可以从加密文件中获取,在加载时再解密(防止Class文件被反编译)。这个加载是由类加载器加载进虚拟机的,非数组类型可以使用内置的引导类加载器来加载,也可以使用开发人员自定义的类加载器来加载,我们可以自己控制字节流的获取方式。而数组类型本身不通过类加载器加载,它是由虚拟机直接在内存中构造出来的。 加载阶段会把 Class 常量池中的各项常量存放到运行时常量池中(下图中的常量池只挑选了部分常量来展示)。加载阶段的最终产品就是 Class 类的实例对象,它成为程序与方法区内部数据结构之间的入口,可以通过这个 Class 实例来获得类的信息、方法、字段、类加载器等等。 在装载过程中,虚拟机还会确认装载类的所有超类是否都被装载了,根据 super class 项解析符号引用,这就会导致超类的装载、连接和初始化。 3、验证这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。 验证阶段会完成下面四个阶段的检验:
4、准备准备阶段是为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,初始值是指这个数据类型的零值,而赋值的过程是放在 <clinit> 方法中,在初始化阶段执行的。注意实例变量是在创建实例对象时才初始化值的。 基本数据类型的零值: 准备阶段还会为常量字段(final 修饰的常量,即字段表中有 ConstantValue 属性的字段)分配内存并直接赋值为定义的字面值。 User 类经过准备阶段后: 5、解析解析过程就是根据符号引用查找到实体,再把符号引用替换成一个直接引用的过程。因为所有的符号引用都保存在常量池中,所以这个过程常被称作常量池解析。 1)静态解析与动态连接: 所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在运行期间用到时转化为直接引用,这部分称为动态连接。 静态解析的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。这类方法包含 静态方法、私有方法、实例构造器、父类方法以及被 final 修饰的方法,这5种方法调用会在类加载的时候就把符号引用解析为该方法的直接引用(有可能是在初始化的时候去解析的)。 动态连接这个特性给Java带来了更强大的动态扩展能力,比如使用运行时对象类型,因为要到运行期间才能确定具体使用的类型。这也使得Java方法调用过程变得相对复杂,某些调用需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。 2)符号引用解析: 对于符号引用类型如?CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等,会查找到对应的类型数据、方法地址、字段地址的直接引用,然后将符号引用替换为直接引用。 对于 CONSTANT_String _info 类型指向的字面量,虚拟机会检查字符串常量池中是否已经有相同字符串的引用,有则替换为这个字符串的引用,否则在堆中创建一个新的字符串对象,并将对象的引用放到字符串常量池中,然后替换常量池中的符号引用。 对于数值类型的常量,如 CONSTANT_Long_info、CONSTANT_Integer_info,并不需要解析,虚拟机会直接使用那些常量值。 6、初始化直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,初始化阶段就是执行类构造器 <clinit> 方法的过程。 1)<clinit> 方法:
2)User 类初始化后: 一个类被装载、连接和初始化完成后,它就随时可以使用了。程序可以访问它的静态字段,调用它的静态方法,或者创建它的实例。 7、即时编译初始化完成后,类在调用执行过程中,执行引擎会把字节码转为机器码,然后在操作系统中才能执行。在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是即时编译。 最初,虚拟机中的字节码是由解释器( Interpreter )完成编译的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,即时编译器(JIT)会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后保存到内存中,这样可以减少解释器的中间损耗,获得更高的执行效率。如果没有即时编译,每次运行相同的代码都会使用解释器编译。 五、类加载器1、类加载器子系统在Java虚拟机中,负责查找并装载类型的那部分被称为类加载器子系统。类加载器子系统会负责整个类加载的过程:装载、验证、准备、解析、初始化。 1)Java 虚拟机有两种类加载器,启动类加载器和用户自定义类加载器:
2)类唯一性: 对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间,由不同的类加载器加载的类将被放在虚拟机内部的不同命名空间中。比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。这就是有时候我们测试代码时发现明明是同一个Class,却报强转失败之类的错误。 2、双亲委派模型Java 1.8 之前采用三层类加载器、双亲委派的类加载架构。三层类加载器包括启动类加载器、扩展类加载器、应用程序类加载器。 1)三层类加载器
2)双亲委派模型 除了启动类加载器之外,所有的类加载器都有一个父类加载器。应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。一般来说,开发人员自定义的类加载器的父类加载器一般是应用程序类加载器。 双亲委派模型:类加载器在尝试去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,如果父类加载器没有,继续寻找父类加载器,依次类推,如果到启动类加载器都没找到才从自身查找。这个类加载过程就是双亲委派模型。 首先要明白,Java 虚拟机判定两个 Java 类是否相同,不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。不同类加载器加载的类之间是不兼容的。 双亲委派模型就是为了保证 Java 核心库的类型安全的。所有 Java 应用都至少需要引用 java.lang.Object 类,也就是说在运行的时候,java.lang.Object 这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成或者自己定义了一个 java.lang.Object 类的话,很可能就存在多个版本的 java.lang.Object 类,而这些类之间是不兼容的。通过双亲委派模型,对于 Java 核心库的类加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。有了双亲委派模型,就算自己定义了一个?java.lang.Object 类,也不会被加载。 3)ClassLoader 类加载器之间的父子关系一般不是以继承的关系来实现的,通常是使用组合、委托关系来复用父加载器的代码。ClassLoader 中有一个 parent 属性来表示父类加载器,如果 parent 为 null,就会调用本地方法直接使用启动类加载器来加载类。类加载器在成功加载某个类之后,会把得到的 java.lang.Class 类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。 3、线程上下文类加载器线程上下文类加载器可通过 java.lang.Thread 中的方法 getContextClassLoader() 获得,可以通过 setContextClassLoader(ClassLoader cl) 来设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl) 方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是应用程序类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。线程上线文类加载器使得父类加载器可以去请求子类加载器完成类加载的行为,这在一定程度上是违背了双亲委派模型的原则。 六、对象及其生命周期1、实例化对象1)实例化一个类有四种途径:
2)实例化对象的过程:
2、对象的内存布局在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 1)对象头: 对象头主要由两部分组成:Mark Word 和类型指针,如果是数组对象,还会包含一个数组长度。
这三部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特。64 位虚拟机中,为了节约内存可以使用选项 +UseCompressedOops 开启指针压缩,某些数据会由 64位压缩至32位。 2)实例数据: 实例数据部分是对象真正存储的有效信息,即对象的各个字段数据,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。 3)对齐填充: 对齐填充仅仅起着占位符的作用,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被设计成正好是8字节的倍数,因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。 4)计算对象占用内存大小: 从上面的内容可以看出,一个对象对内存的占用主要分两部分:对象头和实例数据。在64位机器上,对象头中的 Mark Word 和类型指针各占 64 比特,就是16字节。实例数据部分,可以根据类型来判断,如 int 占 4 个字节,long 占 8 个字节,字符串中文占3个字节、数字或字母占1个字节来计算,就大概能计算出一个对象占用的内存大小。当然,如果是数组、Map、List 之类的对象,就会占用更多的内存。 3、对象访问定位创建对象后,这个引用变量会压入栈中,即一个 reference,它是一个指向对象的引用,这个引用定位的方式主要有两种:使用句柄访问对象和直接指针访问对象。 1)通过句柄访问对象: 使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。 使用句柄来访问的最大好处就是 reference 中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象)时只会改变句柄中的实例数据指针,而?reference 本身不需要被修改。 2)通过直接指针访问对象: 如果使用直接指针访问的话,Java堆中对象的内存布局就必须放置访问类型数据的相关信息(Mark Word 中记录了类型指针),reference 中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。 使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,HotSpot 虚拟机主要就是使用这种方式进行对象访问。 4、垃圾收集当对象不再被程序所引用时,它所使用的堆空间就需要被回收,以便被后续的新对象所使用。JVM 的内存分配管理机制会自动帮我们回收无用的对象,它知道如何确定对象不再被引用,什么时候去回收这些垃圾对象,使用什么回收策略来回收更高效,以及如何管理内存,这部分就是JVM的垃圾收集相关的内容了。 参考本文是学习、参考了如下书籍和课程,再通过自己的总结和实践总结而来。如果想了解更多深入的细节,建议阅读原著。 《深入JAVA虚拟机 第二版》 《深入理解Java虚拟机:JVM高级特性与最佳实践 第三版》 《极客时间:Java性能调优实战》 ? (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |