笔记JVM内幕

转载自多篇文章

线程:程序执行过程中的一个线程实体。 Java线程结束后,原生线程被回收,操作系统负责调度所有的线程,并将其分配到可用的CPU上,当原生线程初始化完毕后,就会调用java线程的run()方法。run()返回时,被处理未捕获异常,原生线程将确认由于它的结束是否要终止JVM的进程(比如这个线程是最后一个非守护线程)。当线程结束时,会释放原生线程和java线程的所有资源。

JVM 系统线程

Hotspot JVM 后台运行的系统线程主要有下面几个:

虚拟机线程(VM thread) 这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。
周期性任务线程 这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。
GC 线程 这些线程支持 JVM 中不同的垃圾回收活动。
编译器线程 这些线程在运行时将字节码动态编译成本地平台相关的机器码。
信号分发线程 这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。

每个运行的线程都会包括以下组件:1.程序计数器(PC) 2堆(heap) 3.本地方法区4.方法区5.虚拟机栈




程序计数器(PC)

PC 指当前指令(或操作码)的地址,本地指令除外。如果当前方法是 native 方法,那么PC 的值为 undefined。所有的 CPU 都有一个 PC,典型状态下,每执行一条指令 PC 都会自增,因此 PC 存储了指向下一条要被执行的指令地址。JVM 用 PC 来跟踪指令执行的位置,PC 将实际上是指向方法区(Method Area)的一个内存地址。

栈(Stack)

每个线程拥有自己的栈,栈包含每个方法执行的栈帧。栈是一个后进先出(LIFO)的数据结构,因此当前执行的方法在栈的顶部。每次方法调用时,一个新的栈帧创建并压栈到栈顶。当方法正常返回或抛出未捕获的异常时,栈帧就会出栈。除了栈帧的压栈和出栈,栈不能被直接操作。所以可以在堆上分配栈帧,并且不需要连续内存。

Native栈

并非所有的 JVM 实现都支持本地(native)方法,那些提供支持的 JVM 一般都会为每个线程创建本地方法栈。如果 JVM 用 C-linkage 模型实现 JNI(Java Native Invocation),那么本地栈就是一个 C 的栈。在这种情况下,本地方法栈的参数顺序、返回值和典型的 C 程序相同。本地方法一般来说可以(依赖 JVM 的实现)反过来调用 JVM 中的 Java 方法。这种 native 方法调用 Java 会发生在栈(一般是 Java 栈)上;线程将离开本地方法栈,并在 Java 栈上开辟一个新的栈帧。

栈的限制

栈可以是动态分配也可以固定大小。如果线程请求一个超过允许范围的空间,就会抛出一个StackOverflowError。如果线程需要一个新的栈帧,但是没有足够的内存可以分配,就会抛出一个 OutOfMemoryError。

栈帧(Frame)

每次方法调用都会新建一个新的栈帧并把它压栈到栈顶。当方法正常返回或者调用过程中抛出未捕获的异常时,栈帧将出栈。更多关于异常处理的细节,可以参考下面的异常信息表章节。

每个栈帧包含:

  • 局部变量数组
  • 返回值
  • 操作数栈
  • 类当前方法的运行时常量池引用

局部变量数组

局部变量数组包含了方法执行过程中的所有变量,包括 this 引用、所有方法参数、其他局部变量。对于类方法(也就是静态方法),方法参数从下标 0 开始,对于对象方法,位置0保留为 this。

有下面这些局部变量:

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

除了 long 和 double 类型以外,所有的变量类型都占用局部变量数组的一个位置。long 和 double 需要占用局部变量数组两个连续的位置,因为它们是 64 位双精度,其它类型都是 32 位单精度。

操作数栈

操作数栈在执行字节码指令过程中被用到,这种方式类似于原生 CPU 寄存器。大部分 JVM 字节码把时间花费在操作数栈的操作上:入栈、出栈、复制、交换、产生消费变量的操作。因此,局部变量数组和操作数栈之间的交换变量指令操作通过字节码频繁执行。比如,一个简单的变量初始化语句将产生两条跟操作数栈交互的字节码。

1
int i;

被编译成下面的字节码:

1
2
0:    iconst_0    // Push 0 to top of the operand stack
1:    istore_1    // Pop value from top of operand stack and store as local variable 1

更多关于局部变量数组、操作数栈和运行时常量池之间交互的详细信息,可以在类文件结构部分找到。

动态链接

每个栈帧都有一个运行时常量池的引用。这个引用指向栈帧当前运行方法所在类的常量池。通过这个引用支持动态链接(dynamic linking)。

C/C++ 代码一般被编译成对象文件,然后多个对象文件被链接到一起产生可执行文件或者 dll。在链接阶段,每个对象文件的符号引用被替换成了最终执行文件的相对偏移内存地址。在 Java中,链接阶段是运行时动态完成的。

当 Java 类文件编译时,所有变量和方法的引用都被当做符号引用存储在这个类的常量池中。符号引用是一个逻辑引用,实际上并不指向物理内存地址。JVM 可以选择符号引用解析的时机,一种是当类文件加载并校验通过后,这种解析方式被称为饥饿方式。另外一种是符号引用在第一次使用的时候被解析,这种解析方式称为惰性方式。无论如何 ,JVM 必须要在第一次使用符号引用时完成解析并抛出可能发生的解析错误。绑定是将对象域、方法、类的符号引用替换为直接引用的过程。绑定只会发生一次。一旦绑定,符号引用会被完全替换。如果一个类的符号引用还没有被解析,那么就会载入这个类。每个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联的)偏移量。

线程间共享

堆被用来在运行时分配类实例、数组。不能在栈上存储数组和对象。因为栈帧被设计为创建以后无法调整大小。栈帧只存储指向堆中对象或数组的引用。与局部变量数组(每个栈帧中的)中的原始类型和引用类型不同,对象总是存储在堆上以便在方法结束时不会被移除。对象只能由垃圾回收器移除。

为了支持垃圾回收机制,堆被分为了下面三个区域:

  • 新生代
    • 经常被分为 Eden 和 Survivor
  • 老年代
  • 永久代

内存管理

对象和数组永远不会显式回收,而是由垃圾回收器自动回收。通常,过程是这样的:

  1. 新的对象和数组被创建并放入老年代。
  2. Minor垃圾回收将发生在新生代。依旧存活的对象将从 eden 区移到 survivor 区。
  3. Major垃圾回收一般会导致应用进程暂停,它将在三个区内移动对象。仍然存活的对象将被从新生代移动到老年代。
  4. 每次进行老年代回收时也会进行永久代回收。它们之中任何一个变满时,都会进行回收。

非堆内存

非堆内存指的是那些逻辑上属于 JVM 一部分对象,但实际上不在堆上创建。

非堆内存包括:

  • 永久代,包括:
    • 方法区
    • 驻留字符串(interned strings)
  • 代码缓存(Code Cache):用于编译和存储那些被 JIT 编译器编译成原生代码的方法。

即时编译(JIT)

Java 字节码是解释执行的,但是没有直接在 JVM 宿主执行原生代码快。为了提高性能,Oracle Hotspot 虚拟机会找到执行最频繁的字节码片段并把它们编译成原生机器码。编译出的原生机器码被存储在非堆内存的代码缓存中。通过这种方法,Hotspot 虚拟机将权衡下面两种时间消耗:将字节码编译成本地代码需要的额外时间和解释执行字节码消耗更多的时间。

方法区

方法区存储了每个类的信息,比如:

  • Classloader 引用
  • 运行时常量池
    • 数值型常量
    • 字段引用
    • 方法引用
    • 属性
  • 字段数据
    • 针对每个字段的信息
      • 字段名
      • 类型
      • 修饰符
      • 属性(Attribute)
  • 方法数据
    • 每个方法
      • 方法名
      • 返回值类型
      • 参数类型(按顺序)
      • 修饰符
      • 属性
  • 方法代码
    • 每个方法
      • 字节码
      • 操作数栈大小
      • 局部变量大小
      • 局部变量表
      • 异常表
      • 每个异常处理器
      • 开始点
      • 结束点
      • 异常处理代码的程序计数器(PC)偏移量
      • 被捕获的异常类对应的常量池下标

所有线程共享同一个方法区,因此访问方法区数据的和动态链接的进程必须线程安全。如果两个线程试图访问一个还未加载的类的字段或方法,必须只加载一次,而且两个线程必须等它加载完毕才能继续执行。

类文件结构

一个编译后的类文件包含下面的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
     u4            magic;
     u2            minor_version;
     u2            major_version;
     u2            constant_pool_count;
     cp_info        contant_pool[constant_pool_count – 1];
     u2            access_flags;
     u2            this_class;
     u2            super_class;
     u2            interfaces_count;
     u2            interfaces[interfaces_count];
     u2            fields_count;
     field_info        fields[fields_count];
     u2            methods_count;
     method_info        methods[methods_count];
     u2            attributes_count;
     attribute_info    attributes[attributes_count];
}
magic, minor_version, major_version 类文件的版本信息和用于编译这个类的 JDK 版本。
constant_pool 类似于符号表,尽管它包含更多数据。下面有更多的详细描述。
access_flags 提供这个类的描述符列表。
this_class 提供这个类全名的常量池(constant_pool)索引,比如org/jamesdbloom/foo/Bar。
super_class 提供这个类的父类符号引用的常量池索引。
interfaces 指向常量池的索引数组,提供那些被实现的接口的符号引用。
fields 提供每个字段完整描述的常量池索引数组。
methods 指向constant_pool的索引数组,用于表示每个方法签名的完整描述。如果这个方法不是抽象方法也不是 native 方法,那么就会显示这个函数的字节码。
attributes 不同值的数组,表示这个类的附加信息,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 注解。

可以用 javap 查看编译后的 java class 文件字节码。

如果你编译下面这个简单的类:

1
2
3
4
5
6
package org.jvminternals;
public class SimpleClass {
     public void sayHello() {
         System.out.println( "Hello" );
     }
}

运行下面的命令,就可以得到下面的结果输出: javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class org.jvminternals.SimpleClass
   SourceFile: "SimpleClass.java"
   minor version: 0
   major version: 51
   flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
    #1 = Methodref          #6.#17         //  java/lang/Object."<init>":()V
    #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;
    #3 = String             #20            //  "Hello"
    #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V
    #5 = Class              #23            //  org/jvminternals/SimpleClass
    #6 = Class              #24            //  java/lang/Object
    #7 = Utf8               <init>
    #8 = Utf8               ()V
    #9 = Utf8               Code
   #10 = Utf8               LineNumberTable
   #11 = Utf8               LocalVariableTable
   #12 = Utf8               this
   #13 = Utf8               Lorg/jvminternals/SimpleClass;
   #14 = Utf8               sayHello
   #15 = Utf8               SourceFile
   #16 = Utf8               SimpleClass.java
   #17 = NameAndType        #7:#8          //  "<init>":()V
   #18 = Class              #25            //  java/lang/System
   #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;
   #20 = Utf8               Hello
   #21 = Class              #28            //  java/io/PrintStream
   #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V
   #23 = Utf8               org/jvminternals/SimpleClass
   #24 = Utf8               java/lang/Object
   #25 = Utf8               java/lang/System
   #26 = Utf8               out
   #27 = Utf8               Ljava/io/PrintStream;
   #28 = Utf8               java/io/PrintStream
   #29 = Utf8               println
   #30 = Utf8               (Ljava/lang/String;)V
{
   public org.jvminternals.SimpleClass();
     Signature: ()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 3: 0
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
           0      5      0    this   Lorg/jvminternals/SimpleClass;
 
   public void sayHello();
     Signature: ()V
     flags: ACC_PUBLIC
     Code:
       stack=2, locals=1, args_size=1
         0: getstatic      #2    // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc            #3    // String "Hello"
         5: invokevirtual  #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
       LineNumberTable:
         line 6: 0
         line 7: 8
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
           0      9      0    this   Lorg/jvminternals/SimpleClass;
}

这个 class 文件展示了三个主要部分:常量池、构造器方法和 sayHello 方法。

  • 常量池:提供了通常由符号表提供的相同信息,详细描述见下文。
  • 方法:每一个方法包含四个区域,
    • 签名和访问标签
    • 字节码
    • LineNumberTable:为调试器提供源码中的每一行对应的字节码信息。上面的例子中,Java 源码里的第 6 行与 sayHello 函数字节码序号 0 相关,第 7 行与字节码序号 8 相关。
    • LocalVariableTable:列出了所有栈帧中的局部变量。上面两个例子中,唯一的局部变量就是 this。

这个 class 文件用到下面这些字节码操作符:

aload0 这个操作码是aload格式操作码中的一个。它们用来把对象引用加载到操作码栈。 表示正在被访问的局部变量数组的位置,但只能是0、1、2、3 中的一个。还有一些其它类似的操作码用来载入非对象引用的数据,如iload, lload, float 和 dload。其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double。局部变量数组位置大于 3 的局部变量可以用 iload, lload, float, dload 和 aload 载入。这些操作码都只需要一个操作数,即数组中的位置
ldc 这个操作码用来将常量从运行时常量池压栈到操作数栈
getstatic 这个操作码用来把一个静态变量从运行时常量池的静态变量列表中压栈到操作数栈
invokespecial, invokevirtual 这些操作码属于一组函数调用的操作码,包括:invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual。在这个 class 文件中,invokespecial 和 invokevirutal 两个指令都用到了,两者的区别是,invokevirutal 指令调用一个对象的实例方法,invokespecial 指令调用实例初始化方法、私有方法、父类方法。
return 这个操作码属于ireturn、lreturn、freturn、dreturn、areturn 和 return 操作码组。每个操作码返回一种类型的返回值,其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double,a 表示 对象引用。没有前缀类型字母的 return 表示返回 void

跟任何典型的字节码一样,操作数与局部变量、操作数栈、运行时常量池的主要交互如下所示。

构造器函数包含两个指令。首先,this 变量被压栈到操作数栈,然后父类的构造器函数被调用,而这个构造器会消费 this,之后 this 被弹出操作数栈。

 

sayHello() 方法更加复杂,正如之前解释的那样,因为它需要用运行时常量池中的指向符号引用的真实引用。第一个操作码 getstatic 从System类中将out静态变量压到操作数栈。下一个操作码 ldc 把字符串 “Hello” 压栈到操作数栈。最后 invokevirtual 操作符会调用 System.out 变量的 println 方法,从操作数栈作弹出”Hello” 变量作为 println 的一个参数,并在当前线程开辟一个新栈帧。

这里和大家简单分享一下JAVA和JVM运行的原理,Java语言写的源程序通过Java编译器,编译成与平台无关的‘字节码程序’(.class文件,也就是0,1二进制程序),然后在OS之上的Java解释器中解释执行,而JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器

JAVA和JVM运行的原理

1.Java语言运行的过程

Java语言写的源程序通过Java编译器,编译成与平台无关的‘字节码程序’(.class文件,也就是0,1二进制程序),然后在OS之上的Java解释器中解释执行。

Java语言运行的过程

也相当与

Java语言运行的过程

注:JVM(java虚拟机)包括解释器,不同的JDK虚拟机是相同的,解释器不同。

2.JVM:

JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。

java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。

JVM执行程序的过程 :

I.加载。class文件

II.管理并分配内存

III.执行垃圾收集

JRE(java运行时环境)由JVM构造的java程序的运行环境 

JVM执行程序的过程




一、java的运行原理

jvm学习笔记

jvm

  • 开发人员编写java代码(.java文件)

  • 编译器将.java文件编译成字节码文件(.class文件)

  • 字节码被装入内存,当字节码被装入内存之后,它就会被解释器解释执行或是被即时编译器有选择的转换成机器码执行

二、jvm体系结构

jvm学习笔记

jvm

1、类装载(classLoader)子系统

根据给定的全限定名类名来装在.class文件的内容到Runtime Data Area中的method Area(方法区域)。

2、执行引擎(Execution Engine)子系统

执行引擎也叫做解释器(Interpreter),负责解释命令提交给操作系统执行。

3、本地接口(Native Interface)组件

本地接口的作用是融合不同的编程语言为JAVA所用,目前该方法使用的比较少,因为现在的异构领域间的通信很发达,比如socket、webservice等

4、运行数据域(RuntimeDataArea)组件

运行数据区是整个 JVM 的重点。我们所有写的程序都被加载到这里,之后才开始运行

4.1 Java Stack(栈)

栈是在线程创建的时候创建的,它的生命周期是跟随线程的生命周期的,线程结束栈内存就被释放了,所以栈不存在垃圾回收问题。栈主要用于存放引用和基本数据类型(八大基本数据类型)。当一个线程把栈内存用光了之后(一般是递归函数造成的)就会产生StackOverflowError异常。如果虚拟机栈内存可动态扩展(目前大部分的java虚拟机都可动态扩展),如果扩展时不能申请到足够的内存地址时就会产生oom异常栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法 A 被调用时就产生了一个栈帧 F1,并被压入到栈中,A 方法又调用了 B 方法,于是产生栈帧 F2 也被压入栈,执行完毕后,先弹出 F2 栈帧,再弹出 F1 栈帧,遵循“先进后出”原则。

栈帧中保存的数据类型:

本地变量(Local Variables)

包括输入参数和输出参数以及方法内的变量;

栈操作(Operand Stack)

记录出栈、入栈的操作;

栈帧数据(Frame Data)

包括类文件、方法等等。

jvm学习笔记

jvm

图示在一个栈中有两个栈帧,栈帧 2 是最先被调用的方法,先入栈,然后方法 2 又调用了方法 1,栈帧 1 处于栈顶的位置,栈帧 2 处于栈底,执行完毕后,依次弹出栈帧 1 和栈帧2,线程结束,栈释放

4.2 Java heap(堆)

堆内存是跟随jvm的,此部分是所有线程共享的,一个jvm只有一个堆内存,但是堆内存的大小是可以调节的。堆主要用于存贮new出来的对象实例以及实例变量以及数组。当新创建的对象申请的内存大于当前堆内存中的空闲空间时就会出现oom(Out of Memory)异常.堆是垃圾收集器管理的主要区域,所以有时候也被称为“GC堆”

堆内存组成:

jvm学习笔记

jvm

新生区

(Young Generation Space)

伊甸区(Eden Sapce)

所有的类都是在该区域内new出来的,当该区域内空间用完而jvm又需要创建新的对象时,jvm对该区进行垃圾回收,将不再被其他对象引用的对象销毁,并且将剩余的对象移到幸存0区。加入0区空间也满了,jvm就会堆0区进行垃圾回收。并将对象移到1区,如果1区也满了同样会对1区进行来及回收,并将对象移到养老区

幸存0区(Survivor 0 Space)

主要保存从伊甸区筛选出来的对象

 

幸存1区(Survivor 1 Space)

主要保存从伊甸区筛选出来的对象

 

Tenure generation space 养老区

养老区用于保存从新生区筛选出来的 JAVA 对象,一般池对象都在这个区域活跃。

Permanent Space 永久存储区

永久存储区是一个常驻内存区域,用于存放 JDK 自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。此外,该区域还会保存一些常量,这些常量可以是编译期已克制的常量,也可以是运行期生成的常量(例如String.intern()方法)

4.3 Method Area(方法区) 

方法区是被所有线程共享,用于存贮已被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据。此部分还包含运行时常量池(Runtime Constant Pool),运行时常量池保存的时编译器生成的各种变量和符号引用。

4.4 PC Register(程序计数器)

程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。如果线程执行的时一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是native方法,那么这个计数器的值则为空(Undefined)。此内存区域是唯一一个在java虚拟机规范中没有规定任何oom(Out Of Memory)情况的区域

4.5 Native Method Stack(本地方法栈)

本地方法栈是为虚拟机使用到的native方法服务,它的实现的语言、方法与结构并没有强制性的规定。这个区域会抛出 StackOverflowError 和 OutOfMemoryError 异常。
















猜你喜欢

转载自blog.csdn.net/weixin_41888669/article/details/79670249