Java基础(6)—Java虚拟机 JVM

image.png

JAVA基础知识学习

JAVA虚拟机

基础知识:

方法区(运行时常量池):

用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机把它当成永久代来进行垃圾回收。

运行时常量池是方法区的一部分。类加载后,Class 文件中的常量池(用于存放编译期生成的各种字面量和符号引用)就会被放到这个区域。在运行期间也可以用过 String 类的 intern() 方法将新的常量放入该区域。

堆:

存储对象实例,以及给数组分配的内存区域。这块区域是垃圾收集器管理的主要区域("GC 堆 ")。现在收集器基本都是采用分代收集算法,Java 堆还可以分成:新生代和老年代(新生代还可以分成 Eden 空间、From Survivor 空间、To Survivor 空间等)。不需要连续内存,可以通过 -Xmx 和 -Xms 来控制动态扩展内存大小,如果动态扩展失败会抛出 OutOfMemoryError 异常。

虚拟机栈:

每个线程都有自己的虚拟机栈,生命周期和线程相同。虚拟机栈描述方法执行的内存模型,以栈帧为单位,每个栈帧存储和方法运行有关的局部变量表、操作数栈、动态链接、方法返回地址等信息。

该区域可能抛出以下异常:

1.当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;

2.栈进行动态扩展时如果无法申请导足够内存,会抛出 OutOfMemoryError 异常。

本地方法栈:

与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。

程序计数器:

记录正在执行的虚拟机字节码指令的地址(如果正在执行的是Native方法则为空)。

线程隔离:虚拟机栈、本地方法栈、程序计数器。

线程共享:方法区、堆。

hotspot虚拟机对象:

对象的创建:

1.普通对象的创建过程:虚拟机遇到一条new指令时,首先检查这个指令的参数(类的类型)是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析、初始化过。如果没有,就执行类加载过程。

2.数组对象的创建:虚拟机遇到一条new array字节码指令会在内存中直接分配一块区域。

3.Class对象的创建:在虚拟机加载类的时候,通过类的全限定名获取此类的二进制字节流,再通过文件验证后把字节流代表的静态结构转化为方法区的运行时数据结构,并且在内存中生成一个代表这个类的Class对象,存在方法区中,作为这个类的各种数据的访问入口。

对象的内存布局:

对象在内存中的布局分为三块区域:对象头、实例数据、对齐填充

对象头

存储对象自身的运行时数据,包括哈希码,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID,偏向时间戳等;对象头的另外一部分是类型指针,即对象指向它在方法区中的类元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例。如果对象是一个数组,对象头还有一块用于记录数组长度的数据。

实例数据

对象真正存储的有效信息,是在类中定义的各种类型的字段内容。

对齐填充

虚拟机要求对象的大小必须是8字节的整数倍,对齐填充起占位符的作用,保证对象大小为8字节的整数倍。

对象的访问定位:

Java程序通过栈上的引用数据操作堆中的具体对象,对象访问方式有两种:句柄访问、直接指针访问

句柄访问

Java堆划分出一块区域用作句柄池,引用中存储对象的句柄地址,句柄中实际包含着对象实例数据和对象类型数据各自的具体地址信息。

直接指针访问

栈中的引用直接指向对象在堆中的地址,对象在头数据中指向方法区中其类元数据的地址。

好处:

使用句柄:引用中存储的是稳定的句柄地址,在对象被移动(垃圾回收导致对象的移动)时只会改变句柄中的实例数据指针。

使用直接指针:速度更快。

内存分配与回收策略:

优先在Eden分配:

大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC;

大对象直接进入老年代:

提供 -XX:PretenureSizeThreshold 参数,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制;

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

JVM 为对象定义年龄计数器,经过 Minor GC 依然存活且被 Survivor 区容纳的,移动到 Survivor 区,年龄加 1,每经历一次 Minor GC 不被清理则年龄加 1,增加到一定年龄则移动到老年区(默认 15 岁,通过 -XX:MaxTenuringThreshold 设置);

动态对象年龄判定:

若 Survivor 区中同年龄所有对象大小总和大于 Survivor 空间一半,则年龄大于等于该年龄的对象可以直接进入老年代;

空间分配担保:

在发生 Minor GC 之前,JVM 先检查老年代最大可用连续空间是否大于新生代所有对象总空间,成立的话 Minor GC 确认是安全的;否则继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,大于的话进行 Minor GC,小于的话进行 Full GC。

Full GC的触发条件:

对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

调用System.gc():

此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存,可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()。

老年代空间不足:

老年代空间不足的常见场景为大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出如下错误: Java.lang.OutOfMemoryError: Java heap space 。为避免以上两种状况引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

空间分配担保失败:

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。

JDK 1.7及以前的永生代空间不足:

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation 可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space。 为避免 PermGen 占满造成 Full GC 现象,可采用的方法为增大 PermGen 空间或转为使用 CMS GC。

在 JDK 1.8 中用元空间替换了永久代作为方法区的实现,元空间是本地内存,因此减少了一种 Full GC 触发的可能性。

Concurrent Mode Failure:

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

类加载机制:

类的生命周期:

39e58e00-04c2-11e9-8508-5254007b2a54.png

类初始化时机:

对一个类进行主动引用:

1.遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。

2.使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。

3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4.当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;

5.当使用 jdk1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

对一个类进行被动引用(所有引用类的方式都不会触发初始化):

1.通过子类引用父类的静态字段,不会导致子类初始化。

System.out.println(SubClass.value); // value 字段在 SuperClass 中定义

2.通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。

SuperClass[] sca = new SuperClass[10];

3.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

System.out.println(ConstClass.HELLOWORLD);

类加载过程:

加载:

加载是类加载的一个阶段,注意不要混淆。加载过程完成以下三件事:

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

2.将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。

3.在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。

验证:

确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

主要有以下 4 个阶段:

1.文件格式验证。

2.元数据验证(对字节码描述的信息进行语义分析)。

3.字节码验证(通过数据流和控制流分析,确保程序语义是合法、符合逻辑的,将对类的方法体进行校验分析)。

4.符号引用验证。

准备:

类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。

public static int value = 123;     //实例变量,初始化为0

如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。

public static final int value = 123;    //常量,初始化为 123。

解析:

将常量池的符号引用替换为直接引用的过程。

初始化:

初始化阶段即虚拟机执行类构造器 () 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

()方法具有以下特点:

1.是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。

2.与类的构造函数(或者说实例构造器 ())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 () 方法运行之前,父类的 () 方法已经执行结束。因此虚拟机中第一个执行 () 方法的类肯定为 java.lang.Object。

3.由于父类的 () 方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作。

4.() 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 () 方法。

5.接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但接口与类不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 () 方法。

6.虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都会阻塞等待,直到活动线程执行 () 方法完毕。如果在一个类的 () 方法中有耗时的操作,就可能造成多个进程阻塞,在实际过程中此种阻塞很隐蔽。

类加载器:

类与类加载器:

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof() 关键字对做对象所属关系判定等情况),只有在这两个类时由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

类加载器分类:

启动类加载器(Bootstrap ClassLoader):

此类加载器负责将存放在 \lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用 null 代替即可。

扩展类加载器(Extension ClassLoader):

这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 \lib\ext或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。

应用程序类加载器(Application ClassLoader):

这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型:

类加载器的双亲委派模型是指从顶层到底层分别是启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器。类加载器之间的父子关系不是通过继承来实现,而是通过组合来实现。

image.png

工作过程:

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。

好处:

使用双亲委派模型来组织类加载器之间的关系,使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存放再 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型,由各个类加载器自行加载的话,如果用户编写了一个称为java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将变得一片混乱。如果开发者尝试编写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现可以正常编译,但是永远无法被加载运行。

实现:

protected synchronized 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) {
      c = findClass(name);
  }
}
if(resolve) {
  resolveClass(c);
}
return c;
}

JVM参数:

GC优化配置:

配置 描述
-Xms 初始化堆内存大小
-Xmx 堆内存最大值
-Xmn 新生代大小
-XX:PermSize 初始化永生代大小
-XX:MaxPermSize 永生代最大容量

GC类型设置:

配置 描述
-XX:+UseSerialGC 串行垃圾回收器
-XX:+UseParallelGC 并行垃圾回收器
-XX:+UseConcMarkSweepGC 并发标记扫描垃圾回收器
-XX:ParallelCMSThreads= 并发标记扫描垃圾回收器=未使用的线程数量
-XX:+UseG1GC G1垃圾回收器

上一篇:Java基础(5)—垃圾回收机制 GC

精彩内容不够看?更多精彩内容,请到微信搜索 “危君子频道” 订阅号,每天更新,欢迎大家关注订阅!

image

你可能感兴趣的:(Java基础(6)—Java虚拟机 JVM)