一、JVM内存结构
其中堆和方法区是线程共享的,虚拟机栈,程序计数器,本地方法栈是线程隔离的
堆:JVM 最大的内存空间,就大部分对象都是存在堆内存中的,堆内存又做了细分,分为了新生代和老年代还有一个 jdk8之前是永久代,jdk8 及其以后都是元空间,元空间并不是堆内存的一部分,而是本地内存。
From Survivor 和 to Survivor 是存活区,垃圾回收使用复制算法的时候会用到
虚拟机栈:是线程独享的,当创建线程的时候就会创建虚拟机栈,虚拟机栈由栈帧组成,每一次的方法调用都会创建栈帧,每次方法的调用返回都会对应着入栈和出栈操作
本地方法栈:和虚拟机栈类似,虚拟机栈管理 java 方法,本地方法栈管理的是 native 方法,本地方法都是由 C 语言完成的
程序计数器:用来记录各个线程执行的字节码的地址,像分支、循环、跳转、异常等都需要依赖程序计数器;为什么需要程序计数器呢?java 是多线程语言,当执行的线程数量超过 CPU 核心时,线程之间会根据时间片争抢 CPU资源,如果某个线程还没执行完,就被另一个线程抢去了 CPU资源,那下次这个线程再抢到 CPU资源的时候,运行到哪了呢?这就需要程序计数器来记录了。
方法区:线程共享的,主要包括静态变量,字符串常量池,运行时常量池,类信息,静态常量池
静态常量池:也叫class文件常量池,主要存放字面量(例如文本字符串、final修饰的常量),
符号引用(例如类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)
运行时常量池:当类加载到内存中后,JVM就会将静态常量池中的内容存放到运行时的常量池中;运行时常量池里面存储的主要是编译期间生成的字面量、符号引用等等。
字符串常量池:也可以理解成运行时常量池分出来的一部分,类加载到内存的时候,字符串会存到字符串常量池里面
大于等于 jdk8 的方法区
为什么要用元空间代替永久代?主要有两方面的原因:
1、Oracle把HotSpot和JRockit 虚拟机都收购了,为了合并HotSpot和JRockit的代码,而JRockit没有永久代(PS:是不是JRockit团队打赢了胜仗呢 奸笑脸.jpg)
2、元空间存在于本地内存,意味着只要本地内存足够,它不会出现像永久代中“java.lang.OutOfMemoryError: PermGen space”这种错误。
二、类加载过程
1、编译:
java代码需要先编译成.class文件才能被 java 虚拟机运行,刚学 java 的时候一般都会运行一段 Hello World 程序来正式踏入 java 的世界,一般使用文本编译器写下如下代码:
public class Test {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
然后使用 javac 命令编译这段代码
javac Test.java
会生成一个 Test.class 文件 ,如果用文本编辑器打开会看到一段“天书”,如下:
cafe babe 0000 0034 001d 0a00 0600 0f09
0010 0011 0800 120a 0013 0014 0700 1507
我只截取了其中 2 行
使用命令:
javap -v -p Test
// 描述信息
Classfile /src/test/java/Test.class
Last modified 2021-2-24; size 413 bytes
MD5 checksum d0f5daff29065702d8fac7c86b57070b
Compiled from "Test.java"
// 描述信息
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
// 常量池
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello World
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // Test
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello World
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Test
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
// 方法信息
public Test();
descriptor: ()V
flags: 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 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "Test.java"
指令表查看:
指令表查看
2、加载
读取类的二进制流
转为方法区数据结构,并存放到方法区
在Java堆中产生java.lang.Class对象
3、链接
链接里又包含三个小步骤:
3.1、验证
作用:验证 class 文件是不是符合规范
-
文件格式的验证
● 是否以 0xCAFEBABE 开头
● 版本号是否合理 -
元数据验证
● 是否有父类
● 是否继承了final类( final类不能被继承,如果继承了就有问题了。)
● 非抽象类实现了所有抽象方法 -
字节码验证
● 运行检查
● 栈数据类型和操作码操作参数吻合(比如栈空间只有2字节,但其实却需要大于2字节,此时就认为这个字节码是有问题的)
● 跳转指令指向合理的位置 -
符号引用验证
● 常量池中描述类是否存在
● 访问的方法或字段是否存在且有足够的权限
可使用.Xverify:none关闭验证
3.2、准备
- 作用:为类的静态变量分配内存,初始化为系统的初始值
● final static修饰的变量:直接赋值为用户定义的值,比如private final static int value = 1 ,直接赋值1
● private static int value = 123 ,该阶段的值依然是0
3.3、解析
作用:符号引用转换成直接引用
符号引用和直接引用
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
如上 Methodref、Fieldref 为符号引用,要想真正引用到这些方法、变量、类等信息,就需要把这些符号转化成能够找到的对象指针,或者是对象的地址偏移量,转换之后的引用就是直接引用。
再直白点符号引用就是做了一个标记说我要去引用谁,而直接引用就是真正引用的对象
4、初始化
- 执行方法,clinit方法由编译器自动收集类里面的所有静态变量的赋值动作及静态语句块合并而成,也叫类构造器方法
- 初始化的顺序和源文件中的顺序一致
static {
i = 1;
}
static int i = 0;
public static void main(String[] args) {
System.out.println(i);
}
输出为 0
static int i = 0;
static {
i = 1;
}
public static void main(String[] args) {
System.out.println(i);
}
输出为 1
- 子类的 clinit 被调用前,会先调用父类的 clinit
- JVM 会保证 clinit 方法的线程安全性
- 初始化时,如果实例化一个新对象,会调用方法对实例变量进行初始化,并执行对应的构造方法内的代码。
static {
System.out.println("静态代码块");
}
{
System.out.println("构造块");
}
public Test() {
System.out.println("构造方法");
}
public static void main(String[] args) {
System.out.println("main");
new Test();
}
输出结果:
静态代码块
main
构造块
构造方法
public class Test {
static {
System.out.println("Test 静态代码块");
}
{
System.out.println("Test 构造块");
}
public Test() {
System.out.println("Test 构造方法");
}
public static void main(String[] args) {
new Sub();
}
}
class Super {
static {
System.out.println("Super 静态代码块");
}
{
System.out.println("Super 构造块");
}
public Super() {
System.out.println("Super 构造方法");
}
}
class Sub extends Super {
static {
System.out.println("Sub 静态块");
}
public Sub() {
System.out.println("Sub 构造方法");
}
{
System.out.println("Sub 构造块");
}
}
输出结果:
Test 静态代码块
Super 静态代码块
Sub 静态块
Super 构造块
Super 构造方法
Sub 构造块
Sub 构造方法
上面只是常规的类加载流程,实际上 JVM 并会不完全按照上面的加载顺序执行,比如说解析不一定在初始化之前
今天的文章就到这里,感兴趣的可以扫微信关注一下