JVM虚拟机的认识

3f90d820d030a9b32a1490db97530cef9f9.jpg

线程

JVM允许一个应用并发执行多个线程,Hostpoot JVM中的Java线程与原生操作系统有直接的映射关系。当线程本地存储,缓冲区分配,同步对象,栈,程序计数器准备好以后,就会创建一个操作系统的原生线程。Java线程结束,原生线程随之被回收,操作系统负责调用所有线程,并把他们分配到任何可用的CPU上。当原生线程初始化完成后,就会调用Java线程的run()方法。run()返回时,被处理未捕获异常,原生线程将确认由于它的结束是否要终止JVM进程(比如这个线程是最后一个非守护线程)。当线程结束时,会释放原生线程和Java线程的所有资源。

 

JVM系统线程

如果使用jconsole或者其他调试器,你就会看到很多线程在后台运行。这些后台线程与触发main()函数的主线程以及主线程创建的其他线程一起运行。Hostpot JVM后台运行的系统线程主要有下面几个:

(1) 虚拟机线程(VM thread):这个线程等待JVM到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要JVM位于安全点。这些操作的类型有:stop-the-world垃圾回收,线程栈dump,线程暂停,线程偏向锁(biased locking)解除。

(2) 周期性任务线程 :这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。

(3) GC线程:这些线程支持JVM中不同的垃圾回收活动

(4) 编译器线程:这些线程在允许时将字节码动态编译成本地平台相关的激情码

(5) 信号分发线程:这个线程接收发送到JVM的信号并调用适当的JVM方法处理

 

线程相关组件

每个运行的线程都包含下面这些组件:

程序计数器(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)

每次方法调用都会新建一个新的栈帧并把它压栈到栈顶。当方法正常返回或者调用过程中抛出未捕获的异常时,栈帧将出栈。

每个栈帧包含:

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

 

局部变量数组

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

局部变量包含:boolean,byte,char,long,short,int,float,double,reference,retrunAddress

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

操作数栈

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

int i;

被编译成下面的字节码:

0: iconst_0 //Push 0 to top of the operand stack

1: istore_1  // Pop value form top of operand stack and store as local varia

 

动态链接

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

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

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

线程间共享

 

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

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

新生代,经常被分为Eden和Survivor

老年代

永久代

内存管理

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

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

 

 

非堆内存

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

非堆内存包括:

永久代,包括:(1)方法区(2)驻留字符串(interned strings)

代码缓存(Code Cache):用于编译和存储那些被JIT编译器编译成原生代码的方法。

即时编译(JIT)

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

方法区

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

Classloader引用

运行时常量池(数值型常量,字段引用,方法引用,属性)

字段数据 (针对每个字段的信息包含字段名,类型,修饰符,属性【Attribute】)

方法数据 (每个方法的方法名,返回值类型,参数类型(按顺序),修饰符,属性)

方法代码(每个方法的字节码,操作数栈大小,局部变量大小,局部变量表,异常表,每个异常处理器,开始点,结束点,异常处理代码的程序计数器PC偏移量,被捕获的异常类对应的常量池下标)。

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

类文件结构

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

Magic,minor_version,manjor_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和RetentionPolciy.RUNTIME注解。

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

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

package org.jvminternals;

public class SimpleClass {

    public void sayHello() {

        System.out.println("Hello");

    }

}

运行命令,得到结果输出:javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimleClass.class.

9283ba348a26312d5599e968ee5a6e08148.jpg

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

转载于:https://my.oschina.net/ttscjr/blog/1941477

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值