JVM进阶之路(二)
一.垃圾回收
1.对象存储位置流程:
先在栈中申请 然后申请堆中的threadlocal 然后堆
2.垃圾回收算法
标记清除、复制、标记整理、分代回收
标记清除算法:
对垃圾对象进行标记然后直接清除。
两个不足:
- 效率不高
- 空间会产生大量碎片
复制算法:
把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。
解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。
标记-整理算法:
不同于针对新生代的复制算法,针对老年代的特点,创建该算法。主要是把存活对象整理移到内存的一端。
分代回收:
根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。
新生代:
每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。
老年代:
老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 —— 清除 或者 标记 —— 整理 算法回收。
3.垃圾回收器
除了上图的垃圾回收器之外,还有ZGC、shenandoah和epsilon
垃圾回收器中:CMS和G1是并发清除,parNew、Se和so单线程清除,ps和po多线程清除
CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记清除 算法实现。
运作步骤:
初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
并发标记(CMS concurrent mark):进行 GC Roots Tracing
重新标记(CMS remark):修正并发标记期间的变动部分
并发清除(CMS concurrent sweep)
缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记清除 算法带来的空间碎片
CMS的并发标记使用三色标记法、重新标记是使用增量更新法
G1比CMS好就是使用逻辑分代,物理不分,将内存分成多个小块,每个都是region,这样不存在碎片化。而且G1的重新标记是快照更新算法。
二.类加载
1.类的生命周期( 7 个阶段):
其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段可以在初始化之后再开始(运行时绑定或动态绑定或晚期绑定)。
以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):
- 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候。
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
- 当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。
2.类的具体整个流程
加载:
- 通过一个类的全限定名来获取定义次类的二进制流(ZIP 包、网络、运算生成、JSP 生成、数据库读取)。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法去这个类的各种数据的访问入口。
数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的,数组创建过程如下:
- 如果数组的组件类型是引用类型,那就递归采用类加载加载。
- 如果数组的组件类型不是引用类型,Java 虚拟机会把数组标记为引导类加载器关联。
- 数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。
内存中实例的 java.lang.Class 对象存在方法区中。作为程序访问方法区中这些类型数据的外部接口。
加载阶段与连接阶段的部分内容是交叉进行的,但是开始时间保持先后顺序。
验证:
是连接的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机要求。
文件格式验证、元数据验证、字节码验证、符号引用验证
准备:
这个阶段正式为类分配内存并设置类变量初始值,内存在方法去中分配(含 static 修饰的变量不含实例变量)。
解析:
这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
(1)符号引用
符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量。
(2)直接引用
直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和迅疾的内存布局实现有关
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行,分别对应于常量池的 7 中常量类型。
初始化:
前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。
3.类加载器
通过一个类的全限定名来获取描述此类的二进制字节流。
1.双亲委派模型:从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)
- 启动类加载器,加载 lib 下或被 -Xbootclasspath 路径下的类
- 扩展类加载器,加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类
- 引用程序类加载器,ClassLoader负责,加载用户路径上所指定的类库。
除顶层启动类加载器之外,其他都有自己的父类加载器。
工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载。
2.破坏双亲委派模型
keyword:线程上下文加载器(Thread Context ClassLoader)