美文网首页
Java虚拟机

Java虚拟机

作者: 往返于无声 | 来源:发表于2020-03-10 17:16 被阅读0次

    JVM 组成部分

    1. 类加载器
    2. 执行引擎
    3. 内存区
    4. 本地方法调用

    类加载器

    类加载过程.png

    双亲委派模型

    类的加载过程采用双亲委派机制,这种机制能保证 安全性

    1. Bootstrap ClassLoader
    2. Extension ClassLoader
    3. Application ClassLoader
    4. User-Defined ClassLoader

    注:相同的class文件被不同的ClassLoader加载后是两个不同的类。

    工作流程

    当前 ClassLoader 首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。当前 ClassLoader 的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到 bootstrap ClassLoader。

    内存模型

    1. 程序计数器(私有)

      为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储

      1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
      2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪
    2. Java虚拟机栈(私有)

      描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。

    3. 本地方法栈(私有)

      虚拟机在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务

    4. 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap。Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

    5. 方法区(元空间)

      它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(方法区也被称为永久代)。运行时常量池是方法区的一部分,jdk1.8之后移入堆中。

      Jdk1.8之后方法区被元空间取代,元空间使用直接内存

    对象分配规则

    • 对象优先分配在Eden区,若Eden区没有足够的空间时,虚拟机执行一次Mirror GC。
    • 大对象直接进入老年代。
    • 对象经过一次Mirror GC 后进入suvivor区,之后每进行一次Mirrir GC 则对象的年龄加1,到达阈值后进行老年代。
    • 动态判断对象年龄。若Suvivor区中同一年龄的对象的大小总和大于Suvivor区大小的一半,则等于或大于 该年龄的对象都直接进入老年区。
    • 每次Mirroc GC 时,计算从suvivor区进入老年区的对象大小,如果这个值大于老年区的剩余值则进行一次Full GC

    Java虚拟机栈会出现的两种异常

    1. StackOverFlowError

      若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异常。

    2. OutOfMemoryError

      若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 异常。

    方法区和永久代的关系

    方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。


    JVM内存图.png

    移除方法区的原因

    永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间直接使用内存,不会受到内存大小的限制(实际内存限制)

    常量池一些例子

    String s1 = new String("计算机");
    String s2 = s1.intern();
    String s3 = "计算机";
    System.out.println(s2); // 输出“计算机”
    System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象,
    System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象
    
    String str1 = "str";
    String str2 = "ing";         
    String str3 = "str" + "ing"; //常量池中的对象
    String str4 = str1 + str2;   //在堆上创建的新的对象
    

    Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;这 5 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。
    两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。

    Integer i1 = 33;
    Integer i2 = 33;
    System.out.println(i1 == i2);// 输出 true
    Integer i11 = 333;
    Integer i22 = 333;
    System.out.println(i11 == i22);// 输出 false
    Double i3 = 1.2;
    Double i4 = 1.2;
    System.out.println(i3 == i4);// 输出 false
    

    Integer i4 = new Integer(40);
    Integer i5 = new Integer(40);
    Integer i6 = new Integer(0);
    System.out.println("i4=i5+i6   " + (i4 == i5 + i6));  //True 
    System.out.println("40=i5+i6   " + (40 == i5 + i6));  //True
    

    解释:

    语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。


    Question : String s1 = new String("abc");这句话创建了几个字符串对象?

    将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。

    对象的创建过程

    对象的创建过程.png
    1. 类加载检查

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

    2. 分配内存

      在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 指针碰撞空闲列表 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理(标记整理复制算法)功能决定。

    3. 初始化零值

      内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

    4. 设置对象头

      初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

    5. 执行 init 方法

      在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,init 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 init 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

    对象的内存布局

    • 对象头
      • 存储对象自身的运行时数据,如哈希码、GC分代年龄,锁状态标志,线程持有的锁。
      • 类型指针,指向类元数据的指针。
    • 实例数据
    • 对齐填充(不是必要的)

    垃圾回收算法

    1. 标记清除算法

      首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。标记清除后会产生大量不连续的碎片。

    2. 复制算法

      分为两块内存,一块内存为空,另一块存放对象。当发生垃圾回收时,将存活的对象移入空内存,并将原内存整块清空。

    3. 标记整理算法

      标记存活对象,让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。

    4. 分代收集算法

      当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

    堆内存常见的分配策略

    对象优先在eden区分配,大对象直接进入年老代,长期存活的对象将进入年老代。对象在新生代中 eden 区分配,当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。若在eden区的对象无法移入suvivor区(太大),则将被直接移入年老代。大对象直接进入年老代, 大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。suvivor to区总是为空,当满了之后,将该区所有对象移动到年老代。

    对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

    动态的年龄判定

    为了更好的适应不同程序的内存情况,虚拟机不是永远要求对象年龄必须达到了某个值才能进入年老代,如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入年老代,无需达到要求的年龄。

    内存管理命令

    • -XX:+PrintGCDetails

      查看内存使用情况

    • -Xms

      指定JVM初始分配的内存。默认为物理内存的1/64。

    • -Xmx

      指定JVM最大分配的内存。默认为物理内存的1/4。

      服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小。

    • -XX:PermSize

      设置非堆内存初始值。

    • XX:MaxPermSize

      设置最大非堆内存的大小。

    • -Xmn2G

      设置年轻代大小为2G。

    • -XX:SurvivorRatio

      设置年轻代中Eden和Suvivor比值。

    内存分代假设

    90%的对象熬不过第一次垃圾回收,而老的对象(经历了好几次垃圾回收的对象)则有98%的概率会一直活下来。

    判断对象是否存活

    1. 引用计数法

      每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。

      缺点 无法解决循环引用产生的问题。

    2. 可达性分析法

      通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

      作为GC Roots的对象包括下面几种:

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

    在可达性分析算法中不可达的对象,也并非是“非死不可”的

    这时候它们暂时处于“缓刑”阶段。要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。程序中可以通过覆盖finalize()来一场"惊心动魄"的自我拯救过程,但是,这只有一次机会。

    判断常量是废弃常量

    没有任何Stirng对象引用该字符串常量

    判断一个类是无用的类

    1. 所有该类的实例被回收。

    2. 加载该类的ClassLoader被回收。

    3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    垃圾收集器

    1. Serial 收集器

      Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。这个收集器是一个单线程 收集器。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( Stop The World ),直到它收集结束。

      优点: 简单而高效。

      新生代使用复制算法,老年代使用标记-整理算法。

      -XX:+UseSerialGC 串行收集器

    2. ParNew 收集器

      ParNew收集器 ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行。

      它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

      -XX:+UseParNewGC ParNew收集器
      -XX:ParallelGCThreads 限制线程数量

    3. Parallel scanvenge 收集器

      Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 默认

    4. Serial Old 收集器

      Serial 老年版收集器

    5. Parallel Old 收集器

      Parallel scanvenge 老年版收集器。默认

    6. CMS 收集器

      一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
      是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。基于“ 标记-清除 ”算法实现。

      工作过程
      1. 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快。Stop the world
      2. 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
      3. 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短(只CMS线程工作)。 Stop the world
      4. 并发清除: 开启用户线程,同时 GC 线程开始对为标记的区域做清扫。

      Ps: CMS 的并行收集 只在收集老年代能够起效,而在回收年轻代的时候CMS是要暂停整个应用的(Stop-the-world)。

      缺点: 对CPU资源敏感,无法处理浮动垃圾,使用 标记-清除算法 会有大量空间 碎片 产生。

    1. G1 收集器

      G1 (Garbage-First) 是一款面向服务器 的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器, 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
      使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
      G1 从整体来看是基于标记-整理算法 实现的收集器;从局部上来看是基于复制算法 实现的。

      垃圾收集过程

      4个步骤

      • 初始标记
      • 并发标记
      • 最终标记
      • 筛选回收

      G1在堆内存中并发地对所有对象进行标记,决定对象的可达性。经过全局标记,G1了解哪些区域几乎是空的,然后优先收集这些区域,这就是GarbageFirst的命名由来。G1将垃圾收集和内存整理活动专注于那些几乎全是垃圾的区域,并建立停顿预测模型 来决定每次GC时回收哪些区域,以满足用户设定的停顿时间。
      对于区域的回收通过复制算法实现。在完成标记清理后,G1将这几个区域的存活对象复制到一个单独区域中,实现内存整理和空间释放。这一过程通过多线程并行进行来降低停顿时间,提高吞吐量。通过这样的方式,G1在每次GC过程中持续清理碎片,控制停顿时间满足用户要求。G1根据用户指定的暂停时间指标去选择收集区域的数量。G1适合内存稍大的应用(4G以上)。

      G1收集器将Java堆均分成大小相同的区域,1M到32M,最多2000个区域,最大支持堆内存64G。G1可同时用在新生代和永久代。

      优点 空间规整,可预测停顿。

    垃圾收集器.png
    什么时候将应用迁移到G1
    1. GullGC发生频繁或总时间过长
    2. 对象分配率或对象升级至老年代的比例波动较大
    3. 较长的垃圾收集或内存整理停顿(大于0.5秒)

    JVM性能调优

    当调优一个Java应用时,把焦点放在两个主要的目标上:响应能力吞吐量

    • 关注响应能力的应用,长暂停时间是不可接受的
    • 关注吞吐量的应用,长暂停时间可接受

    JDK 监控和故障处理工具

    jps

    jstat 用于收集 HotSpot 虚拟机各方面的运行数据

    jinfo 显示虚拟机配置信息

    jmap 生成堆转储快照

    jhat

    jstack

    vm options

    JVM 参数

    • 显式指定堆内存

      -Xms<heap size>[unit]

      -Xmx<heap size>[unit]

      -Xms2G -Xmx5G

    • 显式指定新生代内存

      尽可能将对象分配在新生代是明智的做法,FullGC成本高

      -XX:NewSize=<young size>[unit]

      -XX:MaxNewSize=<young size>[unit]

      -XX:NewSize=256m
      -XX:MaxNewSize=1024M
      

      -Xmn256m NewSize和MaxNewSize设为一致

      -XX:NewRatio=1 新生代与老年代的比例
      
    • 显式指定永久代/元空间的大小

      jdk1.8之前
      -XX:PermSize=N
      -XX:MaxPermSize=N
      
      jdk1.8
      -XX:MetaspaceSize=N
      -XX:MaxMetaSpaceSize=N 如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
      
    • 垃圾收集相关

      -XX:+UseSerialGC
      -XX:+UseParallelGC
      -XX:+UseParNewGC
      -XX:+UseG1GC
      

    注: 参考 Java Guide https://gitee.com/SnailClimb/JavaGuide/tree/master

    相关文章

      网友评论

          本文标题:Java虚拟机

          本文链接:https://www.haomeiwen.com/subject/ibkgdhtx.html