微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!
Java虚拟机专题提供Java虚拟机的最新资讯内容,帮你更好的了解Java虚拟机。
JVM内幕:Java虚拟机详解
这篇文章解释了Java 虚拟机(JVM)的内部架构。下图显示了遵守 Java SE 7 规范的典型的 JVM 核心内部组件。上图显示的组件分两个章节解释。第一章讨论针对每个线程创建的组件,第二章节讨论了线程无关组件。线程JVM 系统线程每个线程相关的程序计数器栈本地栈栈限制栈帧局部变量数组操作数栈动态链接线程共享堆内存管理非堆内存即时编译方法区类文件结构类加载器更快的类加载方法区在哪里类加载器参考运行时常量池异常表符号表Interned 字符串线程这里所说的线程指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。run() 返回时,被处理未捕获异常,原生线程将确认由于它的结束是否要终止 JVM 进程(比如这个线程是最后一个非守护线程)。当线程结束时,会释放原生线程和 Java 线程的所有资源。JVM 系统线程如果使用 jconsole 或者其它调试器,你会看到很多线程在后台运行。这些后台线程与触发 public static void main(String[]) 函数的主线程以及主线程创建的其他线程一起运行。Hotspot JVM 后台运行的系统线程主要有下面几个:虚拟机线程(VM thread)这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。周期性任务线程这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。GC 线程这些线程支持 JVM 中不同的垃圾回收活动。编译器线程这些线程在运行时将字节码动态编译成本地平台相关的机器码。信号分发线程这个线程接收发送到 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)每次方法调用都会新建一个新的栈帧并把它压栈到栈顶。当方法正常返回或者调用过程中抛出未捕获的异常时,栈帧将出栈。更多关于异常处理的细节,可以参考下面的异常信息表章节。每个栈帧包含:局部变量数组返回值操作数栈类当前方法的运行时常量池引用局部变量数组局部变量数组包含了方法执行过程中的所有变量,包括 this 引用、所有方法参数、其他局部变量。对于类方法(也就是静态方法),方法参数从下标 0 开始,对于对象方法,位置0保留为 this。有下面这些局部变量:booleanbytecharlongshortintfloatdoublereferencereturnAddress除了 long 和 double 类型以外,所有的变量类型都占用局部变量数组的一个位置。long 和 double 需要占用局部变量数组两个连续的位置,因为它们是 64 位双精度,其它类型都是 32 位单精度。操作数栈操作数栈在执行字节码指令过程中被用到,这种方式类似于原生 CPU 寄存器。大部分 JVM 字节码把时间花费在操作数栈的操作上:入栈、出栈、复制、交换、产生消费变量的操作。因此,局部变量数组和操作数栈之间的交换变量指令操作通过字节码频繁执行。比如,一个简单的变量初始化语句将产生两条跟操作数栈交互的字节码。int i;被编译成下面的字节码:0: iconst_0 // Push 0 to top of the operand stack1: 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老年代永久代内存管理对象和数组永远不会显式回收,而是由垃圾回收器自动回收。通常,过程是这样的:新的对象和数组被创建并放入老年代。Minor垃圾回收将发生在新生代。依旧存活的对象将从 eden 区移到 survivor 区。Major垃圾回收一般会导致应用进程暂停,它将在三个区内移动对象。仍然存活的对象将被从新生代移动到老年代。每次进行老年代回收时也会进行永久代回收。它们之中任何一个变满时,都会进行回收。非堆内存非堆内存指的是那些逻辑上属于 JVM 一部分对象,但实际上不在堆上创建。非堆内存包括:永久代,包括:方法区驻留字符串(interned strings)代码缓存(Code Cache):用于编译和存储那些被 JIT 编译器编译成原生代码的方法。即时编译(JIT)Java 字节码是解释执行的,但是没有直接在 JVM 宿主执行原生代码快。为了提高性能,Oracle Hotspot 虚拟机会找到执行最频繁的字节码片段并把它们编译成原生机器码。编译出的原生机器码被存储在非堆内存的代码缓存中。通过这种方法,Hotspot 虚拟机将权衡下面两种时间消耗:将字节码编译成本地代码需要的额外时间和解释执行字节码消耗更多的时间。方法区方法区存储了每个类的信息,比如:Classloader 引用运行时常量池数值型常量字段引用方法引用属性字段数据针对每个字段的信息字段名类型修饰符属性(Attribute)方法数据每个方法方法名返回值类型参数类型(按顺序)修饰符属性方法代码每个方法字节码操作数栈大小局部变量大小局部变量表异常表每个异常处理器开始点结束点异常处理代码的程序计数器(PC)偏移量被捕获的异常类对应的常量池下标所有线程共享同一个方法区,因此访问方法区数据的和动态链接的进程必须线程安全。如果两个线程试图访问一个还未加载的类的字段或方法,必须只加载一次,而且两个线程必须等它加载完毕才能继续执行。类文件结构一个编译后的类文件包含下面的结构: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 文件字节码。如果你编译下面这个简单的类:package org.jvminternals;public class SimpleClass {public void sayHello() {System.out.println("He
深入理解Java虚拟机一
一、运行时数据区域​ 1、程序计数器:当前线程执行字节码的行号指示器(通过改变计数器的值来选择下条需要执行的字节码指令)每个线程有独立的程序计数器(线程私有,为了切换线程时能恢复到挣钱的执行位置)如果执行java方法,计数器记录正在执行的字节码指令地址。如果执行的是Native方法,计数器为空。唯一没规定任何OutOfMemoryError情况的区域。2、虚拟机栈为执行Java方法服务线程私有,声明周期跟线程一致一个Java方法执行到结束的过程:栈帧从入栈到出栈的过程栈帧存储局部变量表(包括基本数据类型和对象的引用类型)、操作栈、动态链接、方法出口等信息异常:线程请求的栈深度大于虚拟机允许的深度,抛出StackOverflowError。虚拟机动态扩展过程中无法申请到足够的内存,会抛出OutOfMemoryError异常。3、本地方法栈为虚拟机用到的Native方法服务也会抛出StackOverflowError和OutOfMemoryError的异常4、Java堆用来存储对象的实例所有线程共享的一块内存区域从内存回收的角度可以分为新生代和老年代5、方法区存放被虚拟机加载的类信息、常量、静态变量等线程共享6、运行时常量池方法区的一部分存放编译期生成的各种字面量和符号引用二、垃圾回收(GC)哪些内存需要回收什么时候回收怎么回收1、判断对象是否存活1、引用计数法:给Java对象添加一个引用计数器,每当有一个地方引用它时,计数器+1;引用失效则-1。当计算器不为0时,判断对象存活缺点:如果两个对象相互循环引用时,因为计算器不为0,不能被回收。实际上对象应该被回收。2、可达性分析算法: ​ (1)原理:把"GC Roots"的对象作为起点,然后向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,即该对象不可达,也就说明此对象是不可用的。(2)可作为GC Root对象:虚拟机栈(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象方法区常量引用的对象本地方法栈中JNI引用的镀锡3、判断一个类可回收:该类所有实例对象被回收加载该类的ClassLoader已经被回收该类对应的Class对象没被引用,无法通过反射访问该类的方法2、引用类型通过引用的强度分为强、软、弱、虚四种引用类型强应用:一般Object b=new Object()这类引用,只要强引用存在,GC就不会回收被引用的对象。软引用:系统在发生内存溢出之前,会将这些对象二次回收。如果还没足够内存,才会抛出内存溢出异常。通过SoftReference来实现。弱引用:GC回收时,无论内存是否足够,都会收会被弱引用关联的对象。通过WeakReference来实现。虚引用:作用是在对象被回收时收到一个系统通知。通过PhantomReference来实现。3、垃圾收集算法1、标记-清除算法:标记出所有需要回收的对象,标记完统一清除被标记的对象。缺点:标记和清理的效率不高标记清除后会产生大量不连续的内存碎片2、复制算法:将内存分为大小相等的两块,每次只用一块,当这一块内存用完,将存活对象复制到另一块,将使用过的内存一次清理。优缺点:不会产生内存碎片的问题缺点是将内存缩小到了之前的一半在对象存活率高时进行多次复制操作,效率会低。3、标记-整理算法:标记需要回收的对象,将存活对象向一端移动(整理),清理掉可回收的对象。4、分代收集算法:根据对象存活周期不同,将Java堆内存分为新生代和老年代。新生代:只有少量对象存活,使用复制算法。老年代:大量对象存活,使用标记清除或者标记整理算法。三、类加载机制1、类加载时机1、定义:把Class文件加载到内存中,并对数据进行校验、解析和初始化,行成可被虚拟机直接使用的Java类型。类从被加载到虚拟机内存中开始,到卸载出内存结束。2、生命周期:加载验证准备解析初始化使用卸载 加载、验证、准备、初始化、卸载的顺序确定。3、需要对类进行初始化的场景new实例化对象、读取或设置类的静态字段,调用类的静态方法(被final修饰,已在编译期将结果放入常量池的静态字段除外)对类进行反射初始化一个类,若父类还没初始化,先触发父类的初始化需指定一个执行的主类(包含main方法的类),虚拟机先初始化该类JDK1.7动态语言,MethodHandle实例解析结果REF_getStatis、REF_putStatis、REF_invokeStatis的方法句柄4、不会方法初始化的场景:所有引用类的方式不会触发初始化,例如子类引用父类的静态字段,只触发父类初始化。5、接口初始化和类初始化的区别:接口初始化时,不要求其父类接口全部完成初始化。2、类加载过程包括加载、验证、准备、解析、初始化5步1、加载:通过类全限定名获取定义该类的二进制字节流将字节流的静态存储结构转换为方法区的运行时数据结构内存中生成该类的Class对象,作为访问该类数据的入口2、验证:文件格式验证,验证字节流是否符合Class文件格式规范元数据验证,对字节码描述的信息进行语义分析字节码验证,确定语义合法符号引用验证,对常量池符号引用校验3、准备: 为类变量(static修饰的变量)分配内存并设置变量初始值4、解析: 将常量池符号引用替换为直接引用(直接指向目标的指针)的过程5、初始化: 开始执行类中定义的Java代码3、类加载器同一个Class文件,被两个不同的类加载器加载,这两个类不相等。相等包括equals、instanceOf、isInstance方法返回的结果。1、类别:启动类加载器(Bootstrap ClassLoader):加载<JAVA_HOME>lib目标,或者被-Xbootclasspath参数指定的路径,可被虚拟机识别的类库扩展类加载器(Extension ClassLoader):加载<JAVA_HOME>libext目录,或被java.ext.dirs系统变量指定的路径的类库应用类加载器(Application ClassLoader):加载ClassPath上指定的类库2、双亲委托机制除了顶层的类加载器外,其他的类加载器都有自己的父类加载器。父子之间通过组合来复用父加载器代码。双亲委托机制的工作流程:一个类加载器收到类加载的请求,首先将请求委托给父类加载器去完成,最终所有加载请求都会传递给顶层的启动加载器中。当父加载器发现未找到所需的类而无法完成加载请求时,子加载器才尝试去加载。ClassLoaderprotected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {//检查请求的类是否已经被加载Class<?> c = findLoadedClass(name);if (c == null) {try {if (parent != null) {//让父类加载器去尝试加载c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//父类加载器抛异常}if (c == null) {//然后调用自身的findClass方法来进行类加载c = findClass(name);}}return c;}先检查是否被加载过,如果没有则调用父加载类去加载父加载器为空,则调用启动类加载器父加载器加载失败,则抛出ClassNotFoundException异常然后去调用自身的findClass方法去进行类加载