深入理解java虚拟机第三-七章学习笔记

深入理解java虚拟机第三-七章学习笔记

引用计数算法

引用计数算法就是某个对象每增加一个被引用,就计数加一,如果某个对象的引用计数为零,那就是没有被引用了。但显然这种算法没法解决A对象引用B对象,B对象也引用A对象,但除此以外就再也没有任何对象yinyA或者B对象的问题。

可达性分析算法

可达性分析算法是像树一样,从一系列的根节点,称为“GC Roots”开始向下搜索,如果遍历不到某个对象,则这个对象以及没有被引用。而GC Roots包括方法区里的静态或者终态的引用对象和本地方法栈和虚拟机栈的引用对象。

finalize()方法

当对象通过可达性分析发现没有被引用的时候,会被作第一次标记,然后判断是否需要执行对象的finalize方法。判断是否要执行finalize方法的标准是如果对象没有覆盖finalize方法或者以及执行过finalize方法,视为不需要执行finalize方法。由于finalize方法的执行可能会导致阻塞,所以并不会判断一个对象需要执行finalize方法然后就直接执行finalize方法。而是先判断对象是否需要执行finalize方法,如果需要,对象会被放进一个F-Queue队列里,由一个虚拟机自动创建的低优先级的线程复制执行finalize方法。由于线程级别低,还可能会被前面的对象的finalize方法阻塞,所以虚拟机是不会也无法确保每个被回收的对象能按时执行其finalize方法。在执行完finalize方法后,对象将会再次被检查是否有被引用,如果有就从队列中移除,否则对象就被回收了。因此,在虚拟机正式回收对象前执行的finalize方法是对象最后的“自我拯救”的方法。只要在这个方法里使得任意一个存活对象引用了自己,对象就暂时免于一死。但是finalize方法只会被执行一次,因此对象也只有一次“自我拯救”的机会。

垃圾收集算法

标记-清除算法

标记-清除算法如其名,就是标记出需要已经死亡的对象,然后清理
## 复制算法
复制算法的简单版是将内存分为等大小的两半。一开始对象的分配只在其中一边进行,当需要清理的时候,把存活的对象复制到另外一边的内存,然后直接清理原本那半内存就好。但是这样内存的利用率就只有50%。但实际上,新生代98%的对象都是“朝生夕死”的。所以改进版的复制算法将内存分为等大小的是个部分。其中两个部分各自作为Survivor1和Survivor2,剩下的八分作为Eden。一开始对象的分配只在Eden进行,当需要清理时,将Survivor1和Eden的存活对象复制到Survivor2里。然后清理Survivor1和Eden。这样利用率就达到90%。当然还有个问题,就是万一Survivor2的内存不够存放Survivor1和Eden的存活对象时,就需要向老年代借内存了。
## 标记-整理算法
标记-整理算法是根据老年代的特点设计的。清理时将存活对象都移动到一端,然后直接清理端边界以外的内存即可。
## 分代收集算法
分代收集算法根据对象的存活时间将对象分为新生代和老年代,根据不同代的特点选择以上的合适的算法。

枚举根节点,安全点,安全区域

在枚举根节点,检查对象的引用关系时,不能一边搜索对象的引用关系还在变。所以需要停止掉全部用户线程,sun将这个叫做Stpo THe World。

要执行GC的时候,并不是任意一个地方都可以停下了进行GC。而可以停下来执行GC的代码点叫做安全点。所以安全点既不能过多,也不能过少。而安全区域则是安全点的拓展,在某一段代码里后可以执行GC。

垃圾收集器

分代 单线程 多线程 算法
新生代 Serial perNew/Parallel Scavenger 复制算法
老年代 Serial Old Parallel Old/CMS 标记-整理算法

G1中将内存分为多个部分(Region),所以在G1中并没有新生代和老年代的概念,也可以理解为G1是新生代和老年代通吃的。

内存分配和回收策略

  1. 对象优先分配到Eden
  2. 大对象直接进入老年代
  3. 长期存活对象将会进入老年代
  4. 如果Survivor中相同年龄的对象的大小大于Survivor的一半,年龄大于等于改年龄的对象直接进入老年代

第四章虚拟机性能监控与故障处理工具和第五章调优案例分析与实战跳过

第六章类文件结构枯燥乏味好难懂,无奈跳过。

类的加载

类从被加载到虚拟机到卸载出内存,整个生命周期包活:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析统称为连接。加载,验证,准备,初始化,卸载这五个阶段的开始的先后是按顺序的,解析则不然。并且这五个阶段只是开始的时间按顺序,他们之间的执行过程可以相互加成混合进行。什么时候进行加载,虚拟机规范并没有规定,所以不同虚拟机可能各不一样。而初始化虚拟机规范则规定有且只有以下五种情况是一定要进行类的初始化,称为主动引用,否则其余情况都不会触发类的初始化,称为被动引用。

  1. 通过new以及还有三个不知道什么指令实例化对象时,如果类没有被初始化,则初始化其静态字段(终态已经在编译的时候被放进常量池,所以除外)
  2. 通过java.lang.reflect反射调用类
  3. 初始化一个类时,其父类还没初始化
  4. 虚拟机启动时程序入口类(main方法的那个类)先初始化
  5. 不知道什么句柄,看不懂

加载

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 通过字节流所代表的静态储存结构转化为方法区的运行时数据结构(简单来说就是转换字节流的数据结构然后放到方法区)
  3. 在内存中生成一个代表此类的java.lang.Class对象,作为方法区里这个类的访问入口。

验证

  1. 文件格式验证,验证是不是class文件,是不是虚拟机能处理的class版本,以及数据的编码结构完整性是否正常
  2. 元数据验证,验证各个字段如继承,覆盖,抽象之类是否合法
  3. 字节码验证,验证代码逻辑是否“合理”
  4. 符号引用验证,验证各个引用是否能真能引用到需要的对象

准备

为类的静态和终态变量分配内存。静态变量的值会初始化为零值,而终态变量会初始化为代码指定值。而静态变量值要修改为代码指定值需要的到初始化阶段。

解析

看不懂,跳过

初始化

如上所说,会给静态变量赋值为代码指定值

类加载器

类加载器读取字节码,然后创建一个class对象,多个类加载器之间又不会共用class对象。可见,如果有两个类加载器,就算读取同样的字节码,也会创建出两个不同class对象,使用==也好,equals()来比较结果是False。instanceof也是False。因此,某个类加载器所创建的某个class对象才能确定唯一的class对象。

双亲委派模型

先说明,这里的双亲(parent)并不是指上头有两个,它像java的单继承关系(但不是通过继承,是通过组合来复用父类加载器的代码),其实就要不只有爸要不只有妈。

类加载器分三类:

  1. 启动类加载器,由C++写成,加载<JAVA_HOME>/lib下的,按名字规定的类,因此自己放个类到那里是不会被识别加载的
  2. 拓展类加载器,加载<JAVA_HOME>/lib/ext下的类
  3. 应用程序类加载器,负责用户路径(ClassPath)的类加载,如果没有被修改过,程序默认就是使用这个类加载器

所以,自定义类加载器会复用应用程序类加载器,应用程序类加载器复用拓展类加载器,拓展类加载器复用启动类加载器。

双亲委派模型的工作过程是,一个类加载器接受到一个类加载请求,会检查这个类是否以及加载过,如果没有,则会查看自己有没有父加载器,如果有则交给父加载器,否则交给启动类加载器。如果上头的类加载器加载不了才会尝试自己加载。这样子能确保Java的继承体系正常。例如要加载java.lang.Object类,如果 类加载器自己就加载了,就可能有多个java.lang.Object的class,全部类都是同一个java.lang.Object的子类的体系就会被破会。而先交给上头类加载器的或,java.lang.Object这种类就会在最顶上唯一地加载。

要实现自己的类加载器,只要继承Java.lang.ClassLoader,实现(还是覆盖?)findClass()方法即可。上面的查看类是否以及加载,交给上头类加载器之类的逻辑在loadClass()方法里以及写好,如果上头类加载器加载不了会调用被实现了的findClass()方法。


深入理解java虚拟机第三-七章学习笔记
https://cellargalaxy.github.io/posts/java/2.深入理解java虚拟机第三-七章学习笔记/
作者
cellargalaxy
发布于
2018年1月30日
许可协议