jvm_jvm调优 - CSDN
jvm 订阅
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。 展开全文
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
信息
软件语言
Java
简    称
JVM
应用学科
计算机软件
中文名
Java虚拟机
外文名
Java Virtual Machine
JVM概述
Java虚拟机有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统。Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。Java虚拟机不仅是一种跨平台的软件,而且是一种新的网络计算平台。该平台包括许多相关的技术,如符合开放接口标准的各种API、优化技术等。Java技术使同一种应用可以运行在不同的平台上。Java平台可分为两部分,即Java虚拟机(Java virtual machine,JVM)和Java API类库。 [1] 
收起全文
精华内容
参与话题
  • 一次性精通JVM JAVA虚拟机

    千人学习 2020-07-23 17:23:22
    为什么要学JVM JVM是JAVA的老祖宗,一切JAVA代码都运行在JVM之上,只有深入理解和掌握虚拟机才能写出更强大的代码。 JVM是高级程序员的必备技能,是程序员进阶,拿高薪的必备技能。 ...
  • Java虚拟机(JVM)你只要看这一篇就够了!

    万次阅读 多人点赞 2020-05-23 21:39:43
    1. Java 内存区域与内存溢出异常 1.1 运行时数据区域 根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。 ...字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行...

    本文是学习了《深入理解Java虚拟机》之后的总结,主要内容都来自于书中,也有作者的一些理解。一是为了梳理知识点,归纳总结,二是为了分享交流,如有错误之处还望指出。

    用XMind画了一张导图(源文件对部分节点有详细备注和参考资料,需要的朋友可以关注我的微信公众号:Java团长,然后回复“JVM”获取):

    1. Java 内存区域与内存溢出异常

    1.1 运行时数据区域

    根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。

     

    1.1.1 程序计数器

    内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成

    如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

    1.1.2 Java 虚拟机栈

    线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表操作数栈动态链接方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

    局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

    StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
    OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

    1.1.3 本地方法栈

    区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

    1.1.4 Java 堆

    对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

    OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

    1.1.5 方法区

    属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

    现在用一张图来介绍每个区域存储的内容。

     

    1.1.6 运行时常量池

    属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。

    1.1.7 直接内存

    非虚拟机运行时数据区的部分

    在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
    OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。

    1.2 HotSpot 虚拟机对象探秘

    主要介绍数据是如何创建、如何布局以及如何访问的。

    1.2.1 对象的创建

    创建过程比较复杂,建议看书了解,这里提供个人的总结。

    遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载。

    类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)。

    前面讲的每个线程在堆中都会有私有的分配缓冲区(TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。

    内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。

    执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成。

    1.2.2 对象的内存布局

    在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)实例数据(Instance Data)对齐填充(Padding)

    对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。

    实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。

    对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。

    1.2.3 对象的访问定位

    使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。

    通过句柄访问

    Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址。详情见图。

     

    使用直接指针访问

    reference 中直接存储对象地址

     

    比较:使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。

    1.3 实战

    // 待填

    2. 垃圾回收器与内存分配策略

    2.1 概述

    程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

    2.2 对象已死吗?

    在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的。

    2.2.1 引用计数法

    给对象添加一个引用计数器。但是难以解决循环引用问题。

     

    从图中可以看出,如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的两块内存依然保持着互相引用无法回收。

    2.2.2 可达性分析法

    通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

     

    可作为 GC Roots 的对象:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象

    2.2.3 再谈引用

    前面的两种方式判断存活时都与‘引用’有关。但是 JDK 1.2 之后,引用概念进行了扩充,下面具体介绍。

    下面四种引用强度一次逐渐减弱

    强引用

    类似于 Object obj = new Object(); 创建的,只要强引用在就不回收。

    软引用

    SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。

    弱引用

    WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

    虚引用

    PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

    2.2.4 生存还是死亡

    即使在可达性分析算法中不可达的对象,也并非是“facebook”的,这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:如果对象在进行中可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

    如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象简历关联即可。

    finalize() 方法只会被系统自动调用一次。

    2.2.5 回收方法区

    在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。

    永久代垃圾回收主要两部分内容:废弃的常量和无用的类。

    判断废弃常量:一般是判断没有该常量的引用。

    判断无用的类:要以下三个条件都满足

    • 该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
    • 加载该类的 ClassLoader 已经被回收
    • 该类对应的 java.lang.Class 对象没有任何地方呗引用,无法在任何地方通过反射访问该类的方法

    2.3 垃圾回收算法

    仅提供思路

    2.3.1 标记 —— 清除算法

    直接标记清除就可。

    两个不足:

    • 效率不高
    • 空间会产生大量碎片

    2.3.2 复制算法

    把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。

    解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。

    2.3.3 标记-整理算法

    不同于针对新生代的复制算法,针对老年代的特点,创建该算法。主要是把存活对象移到内存的一端。

    2.3.4 分代回收

    根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。

    新生代

    每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。

    老年代

    老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 —— 清除 或者 标记 —— 整理 算法回收。

    2.4 HotSpot 的算法实现

    // 待填

    2.5 垃圾回收器

    收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。

     

    说明:如果两个收集器之间存在连线说明他们之间可以搭配使用。

    2.5.1 Serial 收集器

    这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。

     

    2.5.2 ParNew 收集器

    可以认为是 Serial 收集器的多线程版本。

     

    并行:Parallel

    指多条垃圾收集线程并行工作,此时用户线程处于等待状态

    并发:Concurrent

    指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。

    2.5.3 Parallel Scavenge 收集器

    这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。

    CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。

    作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。

    2.5.4 Serial Old 收集器

    收集器的老年代版本,单线程,使用 标记 —— 整理

     

    2.5.5 Parallel Old 收集器

    Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记 —— 整理

     

    2.5.6 CMS 收集器

    CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。

    运作步骤:

    1. 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
    2. 并发标记(CMS concurrent mark):进行 GC Roots Tracing
    3. 重新标记(CMS remark):修正并发标记期间的变动部分
    4. 并发清除(CMS concurrent sweep)

     

    缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记 —— 清除 算法带来的空间碎片

    2.5.7 G1 收集器

    面向服务端的垃圾回收器。

    优点:并行与并发、分代收集、空间整合、可预测停顿。

    运作步骤:

    1. 初始标记(Initial Marking)
    2. 并发标记(Concurrent Marking)
    3. 最终标记(Final Marking)
    4. 筛选回收(Live Data Counting and Evacuation)

     

    2.6 内存分配与回收策略

    2.6.1 对象优先在 Eden 分配

    对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲区,将线程优先在 (TLAB) 上分配。少数情况会直接分配在老年代中。

    一般来说 Java 堆的内存模型如下图所示:
     

     

    新生代 GC (Minor GC)

    发生在新生代的垃圾回收动作,频繁,速度快。

    老年代 GC (Major GC / Full GC)

    发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。

    2.6.2 大对象直接进入老年代

    2.6.3 长期存活的对象将进入老年代

    2.6.4 动态对象年龄判定

    2.6.5 空间分配担保

    3. Java 内存模型与线程

     

    3.1 Java 内存模型

    屏蔽掉各种硬件和操作系统的内存访问差异。

     

    3.1.1 主内存和工作内存之间的交互

    操作 作用对象 解释
    lock 主内存 把一个变量标识为一条线程独占的状态
    unlock 主内存 把一个处于锁定状态的变量释放出来,释放后才可被其他线程锁定
    read 主内存 把一个变量的值从主内存传输到线程工作内存中,以便 load 操作使用
    load 工作内存 把 read 操作从主内存中得到的变量值放入工作内存中
    use 工作内存 把工作内存中一个变量的值传递给执行引擎,
    每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行这个操作
    assign 工作内存 把一个从执行引擎接收到的值赋接收到的值赋给工作内存的变量,
    每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
    store 工作内存 把工作内存中的一个变量的值传送到主内存中,以便 write 操作
    write 工作内存 把 store 操作从工作内存中得到的变量的值放入主内存的变量中

    3.1.2 对于 volatile 型变量的特殊规则

    关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。

    一个变量被定义为 volatile 的特性:

    1. 保证此变量对所有线程的可见性。但是操作并非原子操作,并发情况下不安全。

    如果不符合 运算结果并不依赖变量当前值,或者能够确保只有单一的线程修改变量的值变量不需要与其他的状态变量共同参与不变约束 就要通过加锁(使用 synchronize 或 java.util.concurrent 中的原子类)来保证原子性。

    1. 禁止指令重排序优化。

    通过插入内存屏障保证一致性。

    3.1.3 对于 long 和 double 型变量的特殊规则

    Java 要求对于主内存和工作内存之间的八个操作都是原子性的,但是对于 64 位的数据类型,有一条宽松的规定:允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行,即允许虚拟机实现选择可以不保证 64 位数据类型的 load、store、read 和 write 这 4 个操作的原子性。这就是 long 和 double 的非原子性协定。

    3.1.4 原子性、可见性与有序性

    回顾下并发下应该注意操作的那些特性是什么,同时加深理解。

    • 原子性(Atomicity)

    由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write。大致可以认为基本数据类型的操作是原子性的。同时 lock 和 unlock 可以保证更大范围操作的原子性。而 synchronize 同步块操作的原子性是用更高层次的字节码指令 monitorenter 和 monitorexit 来隐式操作的。

    • 可见性(Visibility)

    是指当一个线程修改了共享变量的值,其他线程也能够立即得知这个通知。主要操作细节就是修改值后将值同步至主内存(volatile 值使用前都会从主内存刷新),除了 volatile 还有 synchronize 和 final 可以保证可见性。同步块的可见性是由“对一个变量执行 unlock 操作之前,必须先把此变量同步会主内存中( store、write 操作)”这条规则获得。而 final 可见性是指:被 final 修饰的字段在构造器中一旦完成,并且构造器没有把 “this” 的引用传递出去( this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final 字段的值。

    • 有序性(Ordering)

    如果在被线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句指“线程内表现为串行的语义”,后半句是指“指令重排”现象和“工作内存与主内存同步延迟”现象。Java 语言通过 volatile 和 synchronize 两个关键字来保证线程之间操作的有序性。volatile 自身就禁止指令重排,而 synchronize 则是由“一个变量在同一时刻指允许一条线程对其进行 lock 操作”这条规则获得,这条规则决定了持有同一个锁的两个同步块只能串行的进入。

    3.1.5 先行发生原则

    也就是 happens-before 原则。这个原则是判断数据是否存在竞争、线程是否安全的主要依据。先行发生是 Java 内存模型中定义的两项操作之间的偏序关系。

    天然的先行发生关系

    规则 解释
    程序次序规则 在一个线程内,代码按照书写的控制流顺序执行
    管程锁定规则 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作
    volatile 变量规则 volatile 变量的写操作先行发生于后面对这个变量的读操作
    线程启动规则 Thread 对象的 start() 方法先行发生于此线程的每一个动作
    线程终止规则 线程中所有的操作都先行发生于对此线程的终止检测
    (通过 Thread.join() 方法结束、 Thread.isAlive() 的返回值检测)
    线程中断规则 对线程 interrupt() 方法调用优先发生于被中断线程的代码检测到中断事件的发生
    (通过 Thread.interrupted() 方法检测)
    对象终结规则 一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始
    传递性 如果操作 A 先于 操作 B 发生,操作 B 先于 操作 C 发生,那么操作 A 先于 操作 C

    3.2 Java 与线程

    3.2.1 线程的实现

    使用内核线程实现

    直接由操作系统内核支持的线程,这种线程由内核完成切换。程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口 —— 轻量级进程(LWP),轻量级进程就是我们通常意义上所讲的线程,每个轻量级进程都有一个内核级线程支持。

     

    使用用户线程实现

    广义上来说,只要不是内核线程就可以认为是用户线程,因此可以认为轻量级进程也属于用户线程。狭义上说是完全建立在用户空间的线程库上的并且内核系统不可感知的。

     

    使用用户线程夹加轻量级进程混合实现

    直接看图

     

    Java 线程实现

    平台不同实现方式不同,可以认为是一条 Java 线程映射到一条轻量级进程。

    3.2.2 Java 线程调度

    协同式线程调度

    线程执行时间由线程自身控制,实现简单,切换线程自己可知,所以基本没有线程同步问题。坏处是执行时间不可控,容易阻塞。

    抢占式线程调度

    每个线程由系统来分配执行时间。

    3.2.3 状态转换

    五种状态:

    • 新建(new)

    创建后尚未启动的线程。

    • 运行(Runable)

    Runable 包括了操作系统线程状态中的 Running 和 Ready,也就是出于此状态的线程有可能正在执行,也有可能正在等待 CPU 为他分配时间。

    • 无限期等待(Waiting)

    出于这种状态的线程不会被 CPU 分配时间,它们要等其他线程显示的唤醒。

    以下方法会然线程进入无限期等待状态:
    1.没有设置 Timeout 参数的 Object.wait() 方法。
    2.没有设置 Timeout 参数的 Thread.join() 方法。
    3.LookSupport.park() 方法。

    • 限期等待(Timed Waiting)

    处于这种状态的线程也不会分配时间,不过无需等待配其他线程显示地唤醒,在一定时间后他们会由系统自动唤醒。

    以下方法会让线程进入限期等待状态:
    1.Thread.sleep() 方法。
    2.设置了 Timeout 参数的 Object.wait() 方法。
    3.设置了 Timeout 参数的 Thread.join() 方法。
    4.LockSupport.parkNanos() 方法。
    5.LockSupport.parkUntil() 方法。

    • 阻塞(Blocked)

    线程被阻塞了,“阻塞状态”和“等待状态”的区别是:“阻塞状态”在等待着获取一个排他锁,这个时间将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。

    • 结束(Terminated)

    已终止线程的线程状态。

     

    4. 线程安全与锁优化

    // 待填

    5. 类文件结构

    // 待填

    有点懒了。。。先贴几个网址吧。

    1. Official:The class File Format
    2.亦山: 《Java虚拟机原理图解》 1.1、class文件基本组织结构

    6. 虚拟机类加载机制

    虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。

    在 Java 语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。

    6.1 类加载时机

    类的生命周期( 7 个阶段)

     

    其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段可以在初始化之后再开始(运行时绑定或动态绑定或晚期绑定)。

    以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):

    1. 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
    2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候。
    3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
    4. 当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
    5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。

    前面的五种方式是对一个类的主动引用,除此之外,所有引用类的方法都不会触发初始化,佳作被动引用。举几个例子~

    public class SuperClass {
        static {
            System.out.println("SuperClass init!");
        }
        public static int value = 1127;
    }
    
    public class SubClass extends SuperClass {
        static {
            System.out.println("SubClass init!");
        }
    }
    
    public class ConstClass {
        static {
            System.out.println("ConstClass init!");
        }
        public static final String HELLOWORLD = "hello world!"
    }
    
    public class NotInitialization {
        public static void main(String[] args) {
            System.out.println(SubClass.value);
            /**
             *  output : SuperClass init!
             * 
             * 通过子类引用父类的静态对象不会导致子类的初始化
             * 只有直接定义这个字段的类才会被初始化
             */
    
            SuperClass[] sca = new SuperClass[10];
            /**
             *  output : 
             * 
             * 通过数组定义来引用类不会触发此类的初始化
             * 虚拟机在运行时动态创建了一个数组类
             */
    
            System.out.println(ConstClass.HELLOWORLD);
            /**
             *  output : 
             * 
             * 常量在编译阶段会存入调用类的常量池当中,本质上并没有直接引用到定义类常量的类,
             * 因此不会触发定义常量的类的初始化。
             * “hello world” 在编译期常量传播优化时已经存储到 NotInitialization 常量池中了。
             */
        }
    }

    6.2 类的加载过程

    6.2.1 加载

    1. 通过一个类的全限定名来获取定义次类的二进制流(ZIP 包、网络、运算生成、JSP 生成、数据库读取)。
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法去这个类的各种数据的访问入口。

    数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的,数组创建过程如下:

    1. 如果数组的组件类型是引用类型,那就递归采用类加载加载。
    2. 如果数组的组件类型不是引用类型,Java 虚拟机会把数组标记为引导类加载器关联。
    3. 数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。

    内存中实例的 java.lang.Class 对象存在方法区中。作为程序访问方法区中这些类型数据的外部接口。
    加载阶段与连接阶段的部分内容是交叉进行的,但是开始时间保持先后顺序。

    6.2.2 验证

    是连接的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机要求。

    文件格式验证

    1. 是否以魔数 0xCAFEBABE 开头
    2. 主、次版本号是否在当前虚拟机处理范围之内
    3. 常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
    4. 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
    5. CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
    6. Class 文件中各个部分集文件本身是否有被删除的附加的其他信息
    7. ……

    只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面 3 个验证阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。

    元数据验证

    1. 这个类是否有父类(除 java.lang.Object 之外)
    2. 这个类的父类是否继承了不允许被继承的类(final 修饰的类)
    3. 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
    4. 类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)

    这一阶段主要是对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。

    字节码验证

    1. 保证任意时刻操作数栈的数据类型与指令代码序列都鞥配合工作(不会出现按照 long 类型读一个 int 型数据)
    2. 保证跳转指令不会跳转到方法体以外的字节码指令上
    3. 保证方法体中的类型转换是有效的(子类对象赋值给父类数据类型是安全的,反过来不合法的)
    4. ……

    这是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,保证校验类的方法在运行时不会做出危害虚拟机安全的事件。

    符号引用验证

    1. 符号引用中通过字符创描述的全限定名是否能找到对应的类
    2. 在指定类中是否存在符方法的字段描述符以及简单名称所描述的方法和字段
    3. 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
    4. ……

    最后一个阶段的校验发生在迅疾将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,还有以上提及的内容。
    符号引用的目的是确保解析动作能正常执行,如果无法通过符号引用验证将抛出一个 java.lang.IncompatibleClass.ChangeError 异常的子类。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

    6.2.3 准备

    这个阶段正式为类分配内存并设置类变量初始值,内存在方法去中分配(含 static 修饰的变量不含实例变量)。

    public static int value = 1127;
    这句代码在初始值设置之后为 0,因为这时候尚未开始执行任何 Java 方法。而把 value 赋值为 1127 的 putstatic 指令是程序被编译后,存放于 clinit() 方法中,所以初始化阶段才会对 value 进行赋值。

    基本数据类型的零值

    数据类型 零值 数据类型 零值
    int 0 boolean false
    long 0L float 0.0f
    short (short) 0 double 0.0d
    char '\u0000' reference null
    byte (byte) 0  

    特殊情况:如果类字段的字段属性表中存在 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 1127。

    6.2.4 解析

    这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

    1. 符号引用
      符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量。
    2. 直接引用
      直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和迅疾的内存布局实现有关

    解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行,分别对应于常量池的 7 中常量类型。

    6.2.5 初始化

    前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。

    6.3 类加载器

    通过一个类的全限定名来获取描述此类的二进制字节流。

    6.3.1 双亲委派模型

    从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)

    1. 启动类加载器
      加载 lib 下或被 -Xbootclasspath 路径下的类

    2. 扩展类加载器
      加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类

    3. 引用程序类加载器
      ClassLoader负责,加载用户路径上所指定的类库。

     

    除顶层启动类加载器之外,其他都有自己的父类加载器。
    工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载。

    6.3.2 破坏双亲委派模型

    keyword:线程上下文加载器(Thread Context ClassLoader)

    最后

    前面两次粗略的阅读,能理解内容,但是很难记住细节。每每碰到不会的知识点就上网查,所以知识点太碎片脑子里没有体系不仅更不容易记住,而且更加容易混乱。但是通过这种方式记录发现自己清晰了很多,就算以后忘记,知识再次捡起的成本也低了很多。

    这次还有一些章节虽然阅读了,但是还未完成记录。等自己理解深刻有空闲了就再次记录下来,这里的内容均出自周志明老师的《深入理解 Java 虚拟机》,有兴趣的可以入手纸质版。

    展开全文
  • JVM架构和GC垃圾回收机制(JVM面试不用愁)

    万次阅读 多人点赞 2019-05-23 10:16:14
    JVM架构和GC垃圾回收机制详解 JVM架构图分析 下图:参考网络+书籍,如有侵权请见谅 (想了解Hadoop内存溢出请看: Hadoop内存溢出(OOM)分类、参数调优化) JVM被分为三个主要的子系统 (1)类加载器子系统(2...

    JVM架构和GC垃圾回收机制详解

    JVM架构图分析

    下图:参考网络+书籍,如有侵权请见谅 (想了解Hadoop内存溢出请看: Hadoop内存溢出(OOM)分类、参数调优化

    JVM被分为三个主要的子系统

    (1)类加载器子系统(2)运行时数据区(3)执行引擎

    1. 类加载器子系统

    Java的动态类加载功能是由类加载器子系统处理。当它在运行时(不是编译时)首次引用一个类时,它加载、链接并初始化该类文件。

    1.1 加载

    类由此组件加载。启动类加载器 (BootStrap class Loader)、扩展类加载器(Extension class Loader)和应用程序类加载器(Application class Loader) 这三种类加载器帮助完成类的加载。

    1.  启动类加载器 – 负责从启动类路径中加载类,无非就是rt.jar。这个加载器会被赋予最高优先级。

    2.  扩展类加载器 – 负责加载ext 目录(jre\lib)内的类.

    3.  应用程序类加载器 – 负责加载应用程序级别类路径,涉及到路径的环境变量等etc.

    上述的类加载器会遵循委托层次算法(Delegation Hierarchy Algorithm)加载类文件

    1.2 链接

    1.  校验 – 字节码校验器会校验生成的字节码是否正确,如果校验失败,我们会得到校验错误

    2.  准备 – 分配内存并初始化默认值给所有的静态变量。

    3.  解析 – 所有符号内存引用方法区(Method Area)原始引用所替代。

    1.3 初始化

    这是类加载的最后阶段,这里所有的静态变量会被赋初始值, 并且静态块将被执行。

    2. 运行时数据区(Runtime Data Area)

    The 运行时数据区域被划分为5个主要组件:

    2.1 方法区(Method Area)

    所有类级别数据将被存储在这里,包括静态变量。每个JVM只有一个方法区,它是一个共享的资源。

    2.2 堆区(Heap Area)

    所有的对象和它们相应的实例变量以及数组将被存储在这里。每个JVM同样只有一个堆区。由于方法区堆区的内存由多个线程共享,所以存储的数据不是线程安全的

    2.3 栈区(Stack Area)

    对每个线程会单独创建一个运行时栈。对每个函数呼叫会在栈内存生成一个栈帧(Stack Frame)。所有的局部变量将在栈内存中创建。栈区是线程安全的,因为它不是一个共享资源。栈帧被分为三个子实体:

    a 局部变量数组 – 包含多少个与方法相关的局部变量并且相应的值将被存储在这里。

    b 操作数栈 – 如果需要执行任何中间操作,操作数栈作为运行时工作区去执行指令。

    c 帧数据 – 方法的所有符号都保存在这里。在任意异常的情况下,catch块的信息将会被保存在帧数据里面。

    如上是JVM三大核心区域

    2.4 PC寄存器

    每个线程都有一个单独的PC寄存器来保存当前执行指令的地址,一旦该指令被执行,pc寄存器会被更新至下条指令的地址。

    2.5 本地方法栈

    本地方法栈保存本地方法信息。对每一个线程,将创建一个单独的本地方法栈。

    3. 执行引擎

    分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐段执行。

    3.1  解释器:

     解释器能快速的解释字节码,但执行却很慢。 解释器的缺点就是,当一个方法被调用多次,每次都需要重新解释。

    编译器

    JIT编译器消除了解释器的缺点。执行引擎利用解释器转换字节码,但如果是重复的代码则使用JIT编译器将全部字节码编译成本机代码。本机代码将直接用于重复的方法调用,这提高了系统的性能。

    a. 中间代码生成器 – 生成中间代码

    b. 代码优化器 – 负责优化上面生成的中间代码

    c. 目标代码生成器 – 负责生成机器代码或本机代码

    d.  探测器(Profiler) – 一个特殊的组件,负责寻找被多次调用的方法。

    3.3  垃圾回收器:

    收集并删除未引用的对象。可以通过调用"System.gc()"来触发垃圾回收,但并不保证会确实进行垃圾回收。JVM的垃圾回收只收集哪些由new关键字创建的对象。所以,如果不是用new创建的对象,你可以使用finalize函数来执行清理。

    Java本地接口 (JNI)JNI 会与本地方法库进行交互并提供执行引擎所需的本地库。

    本地方法库:它是一个执行引擎所需的本地库的集合。

    通过一个小程序认识JVM

    package com.spark.jvm;
    /**
     * 从JVM调用的角度分析java程序堆内存空间的使用:
     * 当JVM进程启动的时候,会从类加载路径中找到包含main方法的入口类HelloJVM
     * 找到HelloJVM会直接读取该文件中的二进制数据,并且把该类的信息放到运行时的Method内存区域中。
     * 然后会定位到HelloJVM中的main方法的字节码中,并开始执行Main方法中的指令
     * 此时会创建Student实例对象,并且使用student来引用该对象(或者说给该对象命名),其内幕如下:
     * 第一步:JVM会直接到Method区域中去查找Student类的信息,此时发现没有Student类,就通过类加载器加载该Student类文件;
     * 第二步:在JVM的Method区域中加载并找到了Student类之后会在Heap区域中为Student实例对象分配内存,
     * 并且在Student的实例对象中持有指向方法区域中的Student类的引用(内存地址);
     * 第三步:JVM实例化完成后会在当前线程中为Stack中的reference建立实际的应用关系,此时会赋值给student
     * 接下来就是调用方法
     * 在JVM中方法的调用一定是属于线程的行为,也就是说方法调用本身会发生在线程的方法调用栈:
     * 线程的方法调用栈(Method Stack Frames),每一个方法的调用就是方法调用栈中的一个Frame,
     * 该Frame包含了方法的参数,局部变量,临时数据等 student.sayHello();
     */
    public class HelloJVM {
    	//在JVM运行的时候会通过反射的方式到Method区域找到入口方法main
    	public static void main(String[] args) {//main方法也是放在Method方法区域中的
    		/**
    		 * student(小写的)是放在主线程中的Stack区域中的
    		 * Student对象实例是放在所有线程共享的Heap区域中的
    		 */
    		Student student = new Student("spark");
    		/**
    		 * 首先会通过student指针(或句柄)(指针就直接指向堆中的对象,句柄表明有一个中间的,student指向句柄,句柄指向对象)
    		 * 找Student对象,当找到该对象后会通过对象内部指向方法区域中的指针来调用具体的方法去执行任务
    		 */
    		student.sayHello();
    	}
    }
    
    class Student {
    	// name本身作为成员是放在stack区域的但是name指向的String对象是放在Heap中
    	private String name;
    	public Student(String name) {
    		this.name = name;
    	}
    	//sayHello这个方法是放在方法区中的
    	public void sayHello() {
    	System.out.println("Hello, this is " + this.name);
    	}
    }

    JVM三大性能调优参数:-Xms –Xmx –Xss

    -Xms –Xmx是对堆的性能调优参数,一般两个设置是一样的,如果不一样,当Heap不够用,会发生内存抖动。一般都调大这两个参数,并且两个大小一样。

    -Xss是对每一个线程栈的性能调优参数,影响堆栈调用的深度

    实战演示从OOM推导出JVM GC时候基于的内存结构:Young Generation(Eden、From、To)、OldGeneration、Permanent Generation

    JVMHeap区域(年轻代、老年代)和方法区(永久代)结构图:

    从Java GC的角度解读代码:程序20行new的Person对象会首先会进入年轻代Eden中(如果对象太大可能直接进入年老代)。在GC之前对象是存在Eden和from中的,进行GC的时候Eden中的对象被拷贝到To这样一个survive空间(survive幸存)空间:包括from和to,他们的空间大小是一样的,又叫s1和s2)中(有一个拷贝算法),From中的对象(算法会考虑经过GC幸存的次数)到一定次数(阈值(如果说每次GC之后这个对象依旧在Survive中存在,GC一次他的Age就会加1,默认15就会放到OldGeneration。但是实际情况比较复杂,有可能没有到阈值就从Survive区域直接到Old Generation区域。在进行GC的时候会对Survive中的对象进行判断,Survive空间中有一些对象Age是一样的,也就是经过的GC次数一样,年龄相同的这样一批对象的总和大于等于Survive空间一半的话,这组对象就会进入old Generation中,(是一种动态的调整))),会被复制到OldGeneration,如果没到次数From中的对象会被复制到To中,复制完成后To中保存的是有效的对象,Eden和From中剩下的都是无效的对象,这个时候就把Eden和From中所有的对象清空。在复制的时候Eden中的对象进入To中,To可能已经满了,这个时候Eden中的对象就会被直接复制到Old Generation中,From中的对象也会直接进入Old Generation中。就是存在这样一种情况,To比较小,第一次复制的时候空间就满了,直接进入old Generation中。复制完成后,To和From的名字会对调一下,因为Eden和From都是空的,对调后Eden和To都是空的,下次分配就会分配到Eden。一直循环这个流程。好处:使用对象最多和效率最高的就是在Young Generation中,通过From to就避免过于频繁的产生FullGC(Old Generation满了一般都会产生FullGC)

    虚拟机在进行MinorGC(新生代的GC)的时候,会判断要进入OldGeneration区域对象的大小,是否大于Old Generation剩余空间大小,如果大于就会发生Full GC

    刚分配对象在Eden中,如果空间不足尝试进行GC,回收空间,如果进行了MinorGC空间依旧不够就放入Old Generation,如果OldGeneration空间还不够就OOM了。

    比较大的对象,数组等,大于某值(可配置)就直接分配到老年代,(避免频繁内存拷贝)

    年轻代和年老代属于Heap空间的

    Permanent Generation(永久代)可以理解成方法区,(它属于方法区)也有可能发生GC,例如类的实例对象全部被GC了,同时它的类加载器也被GC掉了,这个时候就会触发永久代中对象的GC。

    如果OldGeneration满了就会产生FullGC

    满原因:1,from survive中对象的生命周期到一定阈值

    2,分配的对象直接是大对象

    3、由于To 空间不够,进行GC直接把对象拷贝到年老代(年老代GC时候采用不同的算法)

    如果Young Generation大小分配不合理或空间比较小,这个时候导致对象很容易进入Old Generation中,而Old Generation中回收具体对象的时候速度是远远低于Young Generation回收速度。

    因此实际分配要考虑年老代和新生代的比例,考虑Eden和survives的比例

    Permanent Generation中发生GC的时候也对性能影响非常大,也是Full GC

    JVM GC时候核心参数:

    -XX:NewRatio –XX:SurvivorRatio –XX:NewSize –XX:MaxNewSize

    –XX:NewSize–XX:MaxNewSize指定新生代初始大小和最大大小。

    1,-XX:NewRatio    是年老代 新生代相对的比例,比如NewRatio=2,表明年老代是新生代的2倍。老年代占了heap的2/3,新生代占了1/3

    2,-XX:SurvivorRatio 配置的是在新生代里面Eden和一个Servive的比例

    如果指定NewRatio还可以指定NewSizeMaxNewSize,如果同时指定了会如何???

    NewRatio=2,这个时候新生代会尝试分配整个Heap大小的1/3的大小,但是分配的空间不会小于-XX:NewSize也不会大于 –XX:MaxNewSize

    3,-XX:NewSize –XX:MaxNewSize

    实际设置比例还是设置固定大小,固定大小理论上速度更高。

    -XX:NewSize –XX:MaxNewSize理论越大越好,但是整个Heap大小是有限的,一般年轻代的设置大小不要超过年老代。

    -XX:SurvivorRatio新生代里面Eden和一个Servive的比例,如果SurvivorRatio是5的话,也就是Eden区域是SurviveTo区域的5倍。Survive由From和To构成。结果就是整个Eden占用了新生代5/7,From和To分别占用了1/7,如果分配不合理,Eden太大,这样产生对象很顺利,但是进行GC有一部分对象幸存下来,拷贝到To,空间小,就没有足够的空间,对象会被放在old Generation中。如果Survive空间大,会有足够的空间容纳GC后存活的对象,但是Eden区域小,会被很快消耗完,这就增加了GC的次数。

    JVM的GC日志解读:

    一、 JVM YoungGeneration下MinorGC日志详解

    [GC (Allocation Failure) [PSYoungGen:2336K->288K(2560K)] 8274K->6418K(9728K), 0.0112926 secs] [Times:user=0.06 sys=0.00, real=0.01 secs]

    PSYoungGen(是新生代类型,新生代日志收集器),2336K表示使用新生代GC前,占用的内存,->288K表示GC后占用的内存,(2560K)代表整个新生代总共大小

    8274K(GC前整个JVM Heap对内存的占用)->6418K(MinorGC后内存占用总量)(9728K)(整个堆的大小)0.0112926 secs(Minor GC消耗的时间)] [Times: user=0.06 sys=0.00, real=0.01 secs] 用户空间,内核空间时间的消耗,real整个的消耗

    二、 JVM的GC日志Full GC日志每个字段彻底详解

    [Full GC (Ergonomics) [PSYoungGen: 984K->425K(2048K)] [ParOldGen:7129K->7129K(7168K)] 8114K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1022588 secs] [Times: user=0.56 sys=0.02,real=0.10 secs]

    [Full GC (Allocation Failure) [PSYoungGen: 425K->425K(2048K)][ParOldGen: 7129K->7129K(7168K)] 7555K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1003696 secs] [Times: user=0.64 sys=0.03,real=0.10 secs]

    [Full GC(表明是Full GC) (Ergonomics) [PSYoungGen:FullGC会导致新生代Minor GC产生]984K->425K(2048K)][ParOldGen:(老年代GC)7129K(GC前多大)->7129K(GC后,并没有降低内存占用,因为写的程序不断循环一直有引用)(7168K) (老年代总容量)] 8114K(GC前占用整个Heap空间大小)->7555K (GC后占用整个Heap空间大小) (9216K) (整个Heap大小,JVM堆的大小), [Metaspace: (java6 7是permanentspace,java8改成Metaspace,类相关的一些信息) 2613K->2613K(1056768K) (GC前后基本没变,空间很大)], 0.1022588 secs(GC的耗时,秒为单位)] [Times: user=0.56 sys=0.02, real=0.10 secs](用户空间耗时,内核空间耗时,真正的耗时时间)

    三、 Java8中的JVM的MetaSpace

    Metaspace的使用C语言实现的,使用的是OS的空间,Native Memory Space可动态的伸缩,可以根据类加载的信息的情况,在进行GC的时候进行调整自身的大小,来延缓下一次GC的到来。

    可以设置Metaspace的大小,如果超过最大大小就会OOM,不设置如果把整个操作系统的内存耗尽了出现OOM,一般会设置一个足够大的初始值,安全其间会设置最大值。

    永久代发生GC有两种情况,类的所有的实例被GC掉,且class load不存。

    对于元数据空间 简化了GC, class load不存在了就需要进行GC。

    三种基本的GC算法基石

    一、 标记/清除算法

    内存中的对象构成一棵树,当有效的内存被耗尽的时候,程序就会停止,做两件事,第一:标记,标记从树根可达的对象(途中水红色),第二:清除(清楚不可达的对象)。标记清除的时候有停止程序运行,如果不停止,此时如果存在新产生的对象,这个对象是树根可达的,但是没有被标记(标记已经完成了),会清除掉。

    缺点:递归效率低性能低;释放空间不连续容易导致内存碎片;会停止整个程序运行;

    二、 复制算法

    把内存分成两块区域:空闲区域和活动区域,第一还是标记(标记谁是可达的对象),标记之后把可达的对象复制到空闲区,将空闲区变成活动区,同时把以前活动区对象1,4清除掉,变成空闲区。

    速度快但耗费空间,假定活动区域全部是活动对象,这个时候进行交换的时候就相当于多占用了一倍空间,但是没啥用。

    三、 标记整理算法

    平衡点

    标记谁是活跃对象,整理,会把内存对象整理成一课树一个连续的空间,

    JVM垃圾回收分代收集算法

    综合了上述算法优略

    1, 分代GC在新生代的算法:采用了GC的复制算法,速度快,因为新生代一般是新对象,都是瞬态的用了可能很快被释放的对象。

    2, 分代GC在年老代的算法 标记/整理算法,GC后会执行压缩,整理到一个连续的空间,这样就维护着下一次分配对象的指针,下一次对象分配就可以采用碰撞指针技术,将新对象分配在第一个空闲的区域。

    JVM垃圾回收器串行、并行、并发垃圾回收器概述

    1, JVM中不同的垃圾回收器

    2, 串行,并行,并发垃圾回收器(和JVM历史有关系,刚开始串行)

    Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

    JVM中Serial收集器、ParNew收集器、Parallel收集器解析

    Serial收集器 单线程方式(没有线程切换开销,如果受限物理机器单线程可采用)串行且采用stop the world在工作的时候程序会停止

    Serial和serial old

    ParNew收集器:多线程(多CPU和多Core的环境中高效),生产环境对低延时要求高的话,就采用ParNew和CMS组合来进行server端的垃圾回收

    Parallel 收集器:多线程,并行, 它可以控制JVM吞吐量的大小,吞吐量优先的收集器,一般设置1%,可设置程序暂停的时间,会通过把新生代空间变小,来完成回收,频繁的小规模垃圾回收,会影响程序吞吐量大小

    JVM中CMS收集器解密

    低延迟进行垃圾回收,在线服务和处理速度要求高的情况下很重要

    配置:XX:UseConcMarkSweepGC

    concurrence(并发) Mark(标记)Sweep(清理)

    低延时

    把垃圾回收分成四个阶段

    CMS-initial-mark初始标记阶段会stop the world,短暂的暂停程序根据跟对象标记的对象所连接的对象是否可达来标记出哪些可到达

    CMS-concurrent-mark并发标记,根据上一次标记的结果确定哪些不可到达,线程并发或交替之行,基本不会出现程序暂停。

    CMS-remark再次标记,会出现程序暂停,所有内存那一时刻静止,确保被全部标记,有可能第二阶段之前有可能被标记为垃圾的对象有可能被引用,在此标记确认。

    CMS-concurrent-sweep并发清理垃圾,把标记的垃圾清理掉了,没有压缩,有可能产生内存碎片,不连续的内存块,这时候就不能更好的使用内存,可以通过一个参数配置,根据内存的情况执行压缩。

    JVM中G1收集器

    可以像CMS收集器一样,GC操作与应用的现场一起并发执行

    紧凑的空闲内存区域且没有很长的GC停顿时间

    需要可预测的GC暂停耗时

    不想牺牲太多吞吐量性能

    启动后不需要请求更大的Java堆

    通过案例瞬间理解JVM中PSYoungGen、ParOldGen、MetaSpace

    Heap
     PSYoungGen      total 2560K, used 321K[0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
      eden space 2048K, 15% used[0x00000007bfd00000,0x00000007bfd50568,0x00000007bff00000)
      from space 512K, 0% used[0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
      to   space 512K, 0% used[0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
     ParOldGen       total 7168K, used 7097K[0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
     object space 7168K, 99%used [0x00000007bf600000,0x00000007bfcee7b8,0x00000007bfd00000)
     Metaspace       used 2647K, capacity 4486K, committed4864K, reserved 1056768K
     class space    used 289K, capacity 386K, committed 512K,reserved 1048576K

    PSYoungGen是eden + from

    使用MAT对Dump文件进行分析实战

    导出Dump文件

     

    MapReduce过程详解及其性能优化

    展开全文
  • JVM原理详解

    千人学习 2019-07-18 11:05:39
    1,通俗讲解JVM运行原理,各组成部分的作用,包括栈、堆等 2,使用visio视图,直观介绍JVM组成 3,介绍线程安全产生的原因 4,面试题中存在的些许JVM题目讲解
  • 教妹学Java(七):究竟什么是JVM

    千次阅读 2020-02-21 16:45:29
    本篇文章通过我和三妹对话的形式来谈一谈“究竟什么是 JVM”。 教妹学 Java,没见过这么有趣的标题吧?“语不惊人死不休”,没错,本篇文章的标题就是这么酷炫,接受不了的同学就别点进来看了,所谓好奇心害死猫;...

    大家好,我是沉默王二,一个和黄家驹一样身高,和刘德华一样颜值的程序员。本篇文章通过我和三妹对话的形式来谈一谈“究竟什么是 JVM”。

    教妹学 Java,没见过这么有趣的标题吧?“语不惊人死不休”,没错,本篇文章的标题就是这么酷炫,接受不了的同学就别点进来看了,所谓好奇心害死猫;能够接受的同学我只能说你赚到了,你不仅能在阅读的过程中感受到思维的乐趣,还真的能学习到知识。下面就由我来介绍一下故事的背景吧。

    我有一个漂亮如花的妹妹(见上图),她叫什么呢?我想聪明的读者朋友们都能猜得出:沉默王三,没错,年方三六。父母正考虑让她向我学习,做一名正儿八经的 Java 程序员。我期初是想反抗的,因为程序员这行业容易掉头发。但家命难为啊,与其反抗,不如做点更积极的事情,写点有趣的文章,教妹妹如何更快地掌握 Java 这门编程语言。毕竟程序员还算得上高薪(都是拿命换的啊)。

    (铺垫结束,正文开始)

    “二哥,最近疫情闹得人心惶惶,无心学习了,怎么办?”

    “三妹啊,你要知道,历史上经历过无数次的动荡与不安,但最后,都挺过去了。有人破坏,有人修复。安安心心学习,疫情过后肯定能够派上大用场,到时候经济复苏,人才亟需,懂吗?”

    “二哥,说话就是不一样,一句话就安抚了我不安的心情。”

    “那还不开始今天的主题?”

    “二哥,上一篇文章中你给我解释了什么是 JDK,JRE 和 JVM,但我想知道 JVM 究竟是什么,它能干什么事。”

    “三妹啊,搬个凳子坐我旁边,听二哥来给你慢慢说啊。”

    01、什么是 JVM

    再来回顾一下。JVM(Java Virtual Machine)俗称 Java 虚拟机。之所以称为虚拟机,是因为它实际上并不存在。它提供了一种运行环境,可供 Java 字节码在上面运行。

    02、JVM 能做什么

    JVM 提供了以下操作:

    • 加载字节码
    • 验证字节码
    • 执行字节码
    • 提供运行时环境

    JVM 定义了以下内容:

    • 存储区
    • 类文件格式
    • 寄存器组
    • 垃圾回收堆
    • 致命错误报告等

    03、JVM 的内部结构

    我们来尝试理解一下 JVM 的内部结构,它包含了类加载器(Class Loader)、运行时数据区(Runtime Data Areas)和执行引擎(Excution Engine)。

    1)类加载器

    类加载器是 JVM 的一个子系统,用于加载类文件。每当我们运行一个 Java 程序,它都会由类加载器首先加载。Java 中有三个内置的类加载器:

    • 启动类加载器(Bootstrap Class-Loader),加载 jre/lib 包下面的 jar 文件,比如说常见的 rt.jar(包含了 Java 标准库下的所有类文件,比如说 java.lang 包下的类,java.net 包下的类,java.util 包下的类,java.io 包下的类,java.sql 包下的类)。

    • 扩展类加载器(Extension or Ext Class-Loader),加载 jre/lib/ext 包下面的 jar 文件。

    • 应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java 类。

    一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试
    ClassNotFoundExceptionNoClassDefFoundError 等异常。

    对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals)。

    三妹啊,是不是有点晕,来来来,通过一段简单的代码了解下。

    public class Test {
    
    	public static void main(String[] args) {
    		ClassLoader loader = Test.class.getClassLoader();
    		while (loader != null) {
    			System.out.println(loader.toString());
    			loader = loader.getParent();
    		}
    	}
    
    }
    

    每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader() 可以获取到此引用;然后通过 loader.getParent() 可以获取类加载器的上层类加载器。

    上面这段代码的输出结果如下:

    sun.misc.Launcher$AppClassLoader@18b4aac2
    sun.misc.Launcher$ExtClassLoader@4617c264
    

    第一行输出为 Test 的类加载器,即应用类加载器,它是 sun.misc.Launcher$AppClassLoader 类的实例;第二行输出为扩展类加载器,是 sun.misc.Launcher$ExtClassLoader 类的实例。那启动类加载器呢?

    按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 getParent() 返回 null。所以没有输出。

    2)运行时数据区

    运行时数据区又包含以下内容。

    • PC寄存器(PC Register),也叫程序计数器(Program Counter Register),是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。

    • JVM 栈(Java Virtual Machine Stack),与 PC 寄存器一样,JVM 栈也是线程私有的。每一个 JVM 线程都有自己的 JVM 栈,这个栈与线程同时创建,它的生命周期与线程相同。

    • 本地方法栈(Native Method Stack),JVM 可能会使用到传统的栈来支持 Native 方法(使用 Java 语言以外的其它语言[C语言]编写的方法)的执行,这个栈就是本地方法栈。

    • 堆(Heap),在 JVM 中,堆是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。

    • 方法区(Method area),在 JVM 中,被加载类型的信息都保存在方法区中。包括类型信息(Type Information)和方法列表(Method Tables)。方法区是所有线程共享的,所以访问方法区信息的方法必须是线程安全的。

    • 运行时常量池(Runtime Constant Pool),运行时常量池是每一个类或接口的常量池在运行时的表现形式,它包括了编译器可知的数值字面量,以及运行期解析后才能获得的方法或字段的引用。简而言之,当一个方法或者变量被引用时,JVM 通过运行时常量区来查找方法或者变量在内存里的实际地址。

    3)执行引擎

    执行引擎包含了:

    • 解释器:读取字节码流,然后执行指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。

    • 即时(Just-In-Time,JIT)编译器:即时编译器用来弥补解释器的缺点,提高性能。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。

    04、鸣谢

    本篇文章为《教妹学Java》专栏的第七篇文章,是不是有趣得很?我相信你能感受的到,这可是全网独一份,我看到已经有人在模仿了。现在定价只需 9.9 元,9.9 元你连一杯奶茶都买不到,但却能买下二哥精心制作的专栏,据说 CSDN 已经考虑涨价了,毕竟已经卖出一百多份了。

    我知道,购买专栏的同学都是冲着二哥的名声来的,毕竟二哥是 CSDN 的明星博主,哈哈。为表谢意,我再附送上个人微信(qing_gee),你有什么问题都可以来咨询。

    上一篇回顾:教妹学Java(六):JDK,JRE和JVM之间有什么区别?

    PS:本篇文章中的示例代码已经同步到 GitHub,地址为 itwanger.JavaPoint,欢迎大家 star 和 issue。

    原创不易,喜欢就点个赞,因为你一个小小的举动,就会让这个世界多一份美好。

    展开全文
  • Java虚拟机(Java Vitural Machine),简称JVM.JVM是安装在操作系统上的,它的初衷是为了屏蔽操作系统与计算机硬件之间的差异,Java为不同的系统提供不同的JVM,所以才能保证Java能一处编译到处运行,JVM的本质也是一款软件...

    JVM
    Java虚拟机(Java Vitural Machine),简称JVM.JVM是安装在操作系统上的,它的初衷是为了屏蔽操作系统与计算机硬件之间的差异,Java为不同的系统提供不同的JVM,所以才能保证Java能一处编译到处运行,JVM的本质也是一款软件.

    JVM的内存结构如下:

    这里只要了解一下的组成部分即可,后续再深入讲解.

    JMM
    提到JMM,很多人第一反应可能就是上面的那张图了,其实他们两者是没有任何关系的.JMM全称是Java Memroy Model,也就是Java内存模型,它是一个理论模型,并不是真实存在的.JMM的目的就是有用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果.

    在讲解JMM之前不得不讲一下当代计算的一个内存模型,我们知道当代计算机处理能力是非常快的,而这些都得益于运算超快的处理器,但是觉大多数的运算任务都不可能只靠处理器"计算"就能完成,处理器至少要与内存交互,如读取数据,存储运算结果等,这个I/O操作时很难消除的(无法仅靠寄存器来完成所有运算任务).由于计算的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度地高速缓存(Cache)来作为内存与处理器之间地缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速及逆行,当运算结束候再从缓存同步回内存之中,这样处理器就无需等待缓慢的内存读写了.
    在这里插入图片描述

    Java内存模型也是与此类似的.
    在这里插入图片描述
    Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件是的主内存名字一样,两者也可以相互类比,但此处仅仅是虚拟机内存的一部分).每条线程都还有自己的工作内存(Working Memory,可与上面的处理器告诉缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对该变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量,线程间变量值的传递均需要通过主内存来完成.

    这里所讲的主内存,工作内存与Java内存区域中的堆,栈,方法去等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来,那从变量,主内存,工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域.从更低层次来说,主内存就是对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能回让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存.

    八大操作:

    内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

    • lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
    • unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
    • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
    • load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
    • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
    • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
    • store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
    • write  (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

    JMM对这八种指令的使用,制定了如下规则:

    • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
    • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存 (可见)
    • 不允许一个线程将没有assign的数据从工作内存同步回主内存
    • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
    • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
    • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
    • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
    • 对一个变量进行unlock操作之前,必须把此变量同步回主内存
    展开全文
  • 一、JUC入门

    2020-07-03 16:44:27
    一、什么是JUC?JUC简介:进程线程构造方法启动线程优先级控制方法静态方法线程组(ThreadGroup)同步对象与锁同步方法和同步块等待锁与释放锁静态变量/方法(Class对象的锁,new对象的锁)监视器...
  • JVM 学习大坑入门

    2020-03-22 14:09:42
    JVM JVM是运行在操作系统上的。 结构 类加载器 虚拟机自带的加载器 启动类加载器(BootStrap) C++ :java 打印出来为null -> 打印 .parent 会出空指针 扩展类加载器(Extension)JAVA -> 打印 .parent 出来...
  • 必知必会的java语言基础。
  • JVM&JUC入门

    2019-11-01 13:21:18
    一、入门JVM 1、类装载器ClassLoader 负责加载class文件,class文件在文件开头有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件...
  • 深入详细讲解JVM原理

    万次阅读 多人点赞 2018-06-04 22:55:47
    一、JVM体系结构: 类装载器ClassLoader:用来装载.class文件 执行引擎:执行字节码,或者执行本地方法 运行时数据区:方法区、堆、Java栈、程序计数器、本地方法栈JVM把描述类数据的字节码.Class文件加载到内存...
  • JVM工作原理

    万次阅读 2019-07-04 09:23:02
    >>数据类型  Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。  基本类型的变量保存原始值,即:他代表的值就是数值本身;而引用类型的变量保存引用值。 ... “引用值”代表了某个对象的引用,而...
  • 什么是JVM

    万次阅读 多人点赞 2018-04-20 16:05:05
    说明:做java开发的几乎都知道jvm这个名词,但是由于jvm对实际的简单开发的来说关联的还是不多,一般工作个一两年(当然不包括爱学习的及专门做性能优化的什么的),很少有人能很好的去学习及理解什么是jvm,以及弄...
  • JVM系列(一) - JVM总体概述

    千次阅读 2018-08-23 15:18:49
    JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。 JVM屏蔽了与具体操作系统平台相关的信息,使...
  • 启动IDEA时报Failed to create JVM错误。 之前的操作是按照网上的破解方法进行,修改配置时是用Windows的记事本编辑并保存的。但一启动IDEA就报错误。按网上说的方法也无法解决。 最后想到可能是文件的格式问题。...
  • Pycharm Failed to create JVM 错误

    万次阅读 2019-03-18 11:38:01
    记得在安装pycharm的时候要勾选 安装jre 的那个选项!就可以了。 所以要重新卸载,重新安装。 或者(不推荐) https://www.arulraj.net/2014/10/fix-pycharm-failed-to-create-jvm.html ...
  • SpringBoot jar包启动设置JVM参数

    万次阅读 2019-06-16 20:17:14
    配置初始化堆和最大堆的大小: java -Xms1024m -Xmx1024m -jar app.jar & 设置jvm参数 建议初始化堆的大小和最大堆的大小设置一致,减少GC。
  • linux 下如何设置JVM 常用参数?

    万次阅读 2019-09-03 16:38:33
    待完善。
  • java 内存模型入门系列教程-00

    万次阅读 2019-03-27 16:23:25
    目录 java 内存模型JSR 133-01 java 内存模型数据结构-02 java 内存模型缓存和重排序-03 java 内存模型顺序一致性-04 java 内存模型volatile关键字-05 java 内存模型happens before,as-if-serial,synchronization-...
  • jvm到底是什么?有什么作用?工作机制如何?

    万次阅读 多人点赞 2018-05-04 11:24:23
    1、jvm定义 JVM就是java虚拟机,它是一个虚构出来的计算机,可在实际的计算机上模拟各种计算机的功能。JVM有自己完善的硬件结构,例如处理器、堆栈和寄存器等,还具有相应的指令系统。 2、jvm作用 JVM是java字节码...
1 2 3 4 5 ... 20
收藏数 724,318
精华内容 289,727
关键字:

jvm