Java虚拟机

Posted by Cfeng on August 25, 2019

运行时数据区域

  1. 程序计数器
    记录正在执行的虚拟机字节码指令的地址
  2. Java 虚拟机栈
    每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。
  3. 本地方法栈
    本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。

  4. 所有对象都在这里分配内存,是垃圾收集的主要区域(”GC 堆”)
    在新生代创建对象,若经过多次垃圾回收(默认值为15)仍存活,则进入老生代
  5. 方法区
    用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
  6. 运行时常量池
    JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
  7. 直接内存
    在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。

常量池

String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
String str2 = new String("abcd");//堆中创建一个新的对象
String str3 = new String("abcd");//堆中创建一个新的对象
System.out.println(str1==str2);//false
System.out.println(str2==str3);//false

这两种不同的创建方法是有差别的。

  • 第一种方式是在常量池中拿对象;
  • 第二种方式是直接在堆内存空间创建一个新的对象。

记住一点:只要使用 new 方法,便需要创建新的对象。

String 类型的常量池比较特殊。它的主要使用方法有两种:

  • 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
  • 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。
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; //在堆上创建的新的对象	  
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

Integer例子

Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2   " + (i1 == i2)); //true
System.out.println("i1=i2+i3   " + (i1 == i2 + i3)); //true
System.out.println("i1=i4   " + (i1 == i4)); //false
System.out.println("i4=i5   " + (i4 == i5)); //false
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 进行数值比较。

垃圾收集

  1. 判断一个对象是否可被回收:
    • 为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法
    • 可达性分析算法.以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
    • 方法区的回收.主要是对常量池的回收和对类的卸载
  2. 垃圾收集算法:
    • 标记 - 清除。标记后清理
    • 标记 - 整理。标记存活对象,使其往一段移动,然后清理其余对象
    • 复制 。将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理
    • 分代收集。新生代使用:复制算法。老年代使用:标记 - 清除 或者 标记 - 整理 算法
  3. 垃圾收集器
    1. Serial 收集器.单线程.新生代采用复制算法,老年代采用标记-整理算法.简单而高效(与其他收集器的单线程相比).在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” )
    2. ParNew 收集器.Serial 收集器的多线程版本,能与 CMS 收集器配合使用。
    3. Parallel Scavenge 收集器.多线程。关注点是吞吐量(高效率的利用 CPU)
    4. Serial Old 收集器.Serial 收集器的老年代版本,单线程,作为 CMS 收集器的后备方案
    5. Parallel Old 收集器.Parallel Scavenge 收集器的老年代版本
    6. CMS 收集器.以获取最短回收停顿时间为目标的收集器。因而非常符合在注重用户体验的应用上使用。垃圾收集线程与用户线程(基本上)同时工作
      • 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
      • 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
      • 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
      • 并发清除:不需要停顿。

      并发收集、低停顿
      三个明显的缺点:

      • 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
      • 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
      • 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
    7. G1 收集器.向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
      从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
      • 初始标记
      • 并发标记
      • 最终标记
      • 筛选回收

内存分配与回收策略

  • Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
  • Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
  1. 对象优先在 Eden 分配。当 Eden 空间不够时,发起 Minor GC。
  2. 大对象直接进入老年代.大对象是指需要连续内存空间的对象
  3. 长期存活的对象进入老年代
  4. 动态判定对象年龄
  5. 空间分配担保.老年代最大可用的连续空间是否大于新生代所有对象总空间

类加载过程

类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。

加载、验证、准备、解析和初始化

  1. 加载.加载是类加载的一个阶段
    通过类的完全限定名称获取定义该类的二进制字节流。
    将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
    在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。
  2. 验证.
    确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  3. 准备
    类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存.没有final则初始化为0
    实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。
  4. 解析
    将常量池的符号引用替换为直接引用的过程。
  5. 初始化
    初始化阶段是执行类构造器 ()方法的过程。

类加载器

分类:

  1. BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。
  2. ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。需要import的那些,但不需要自己添加的
  3. AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。需要自己网上下载的jar包,然后import