目的
最近一周通读了一遍《深入理解Java虚拟机:JVM高级特性和最佳实践》(第三版),在读书的过程记录了一些大量知识点,现在输出一篇文章,将书读薄。(都是基于HotSpot虚拟机的)
JVM内存模型
- 程序计数器:记录当前程序执行的位置,便于线程切换更够恢复到当前的问题位置(线程私有,每个线程独立拥有一个程序计数器)
- 堆: 几乎是Java所有对象实例分配到的位置,也是垃圾收集器管理的区域 (线程共享)
- 方法区(jdk8之后,包含了运行时常量池):存储类型信息,常量,静态变量 (线程共享)
- Java虚拟机栈:表述Java方法执行的内存模型,每个方法被执行的时候,Jvm都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每个方法的执行过程,就相当于是栈帧的入栈和出栈的过程。(线程私有)
- 本地方法栈:调用Native方法的时候,用到的栈。(线程私有)
垃圾收集
对象是否存活算法
-
引用计数法 :当一个对象被另外一个对象引用是,将应用计数器+1;当取消引用的使用,该对象的引用计数器-1。如果这个引用计数器为0,说明没有对象引用了,该对象就可以被回收了。
- 优点:实现简单,效率很高
- 缺点:不能满足一些复杂的场景,比如果循环引用的场景
-
可达性分析算法:从GC ROOTS出发,根据引用关系往下搜索,在搜索的过程中走过的路径上的这些对象。被判断为是存活的,其他的对象被认为是可以被回收的的。
- 什么是GC ROOTS?
- 虚拟机栈用的引用对象,每个线程被调用的方法堆栈中使用的参数、局部变量、临时变量
- 方法区中的静态引用对象
- 方法区中的常量引用对象
- 本地方法栈中的引用对象
- synchronized持有的对象
class Example{ private static Long count = 0L; // count 属于方法区上的静态引用 private final static CHINA = "中国"; // CHINA 方法区常量池中的常量引用 public void test(String name,int age ){ // name和age属于方法栈中的参数 int a = age; // a 虚拟机栈中的临时变量 int i = 1; //i 属于虚拟机栈中的局部变量 synchronized(this){ // synchronized持有的对象 ... } } }
上面这些被注释的对象的引用都属于是GC ROOTS
- 优点:可以满足所有的场景
- 缺点:实现较为复杂,效率低 (所以这就是为什么收集器要不断优化的原因
- 什么是GC ROOTS?
收集算法
- 思想:分代收集理论
- 绝大部分的对象都是朝生夕死(针对对象生命周期的不同,需要采用不同的收集算法)
- 熬过越多次的垃圾回收的对象,就越难以消灭
- 跨代引用相对于同代引用来说是极少数的
- 标记-清除算法
- 过程:分为两个阶段;标记和清除阶段,标记的过程就是通过可达性分析,判断对象是否存活,将存活的对象进行标记。然后在将未标记的对象进行回收。
- 优点: 实现简单
- 缺点:1、会产生内存碎片,导致下次大对象无法找到合适的内存空间,而触发再一次GC 2、效率不可控,如果对象数量越大做一次标记的时间会越长。
- 标记-复制算法
- 过程:将内存分为两块相同的内存空间,一块用于内存分配,一块保留。当需要分配内存的那个空间需要GC的时候,先根据可达性分析标记存活的对象,然后将这些对象复制到另外一块内存空间中,然后将这块内存空间整体回收。
- 优点:1、不会产生内存碎片。2、运行效率高
- 缺点:1、实际的内存可被分配的只有二分之一 2、当存活的对象多时,对象复制的过程效率就低
- 标记-整理算法
- 过程:和标记-清除算法一样,但是在清除完成之后,会执行整理的过程。将对象移动到内存空间的一端,避免产生内存碎片。
- 优点:不会产生内存碎片,提高了系统的吞吐量
- 缺点:移动存活或对象,是一件负重的操作,必须要停止用户线程(Stop The World)
经典的垃圾收集器
- 新生代垃圾收集器
- Serial 单线程工作的收集器
- 新生代采用标记-复制算法,需要暂停用户线程Stop The World
- 老年代采用标记-整理算法,需要暂停用户线程
- 性能很低,属于远古产品,已经被潮流抛弃
- ParNew Serial的多线程版本,可以和CMS配合使用
- Parallel Scavenge 关注提升系统的吞吐量
- 新生收集器 采用标记-复制算法
- Serial 单线程工作的收集器
- 老年代垃圾收集器
- CMS 关注以降低程序响应时间,非常使用于Java的Web服务上。
- 初始标记:标记GC ROOTS能直接关联到的对象,速度很快。需要Stop The World
- 并发标记: 从GC ROOTS的直接对象开始遍历整个对象图,这个过程是很耗时的,但是可以和用户线程一起并发运行。
- 重新标记:修正在并发标记中因用户程序导致的标记记录的变化,这个阶段需要较长时间的停顿,但是远小与并发标记的过程。需要Stop The World
- 并发清除:清除掉被标记判断为已死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以用户线程并发运行的。
- 优点:将耗时的过程做到了和用户线程并发执行,降低了程序的响应时间。
- 缺点:1、增加了CPU的负载 2、会产生内存碎片 3、会产生浮动垃圾,在并发清楚的过程中会产生垃圾,只能靠下一次的GC清除
- Serial Old Serial收集器的老年代版本
- Parallel Old Parallel Scavenge收集器的老年代版本,支持多线程的并发收集。基于标记-整理算法实现。
- CMS 关注以降低程序响应时间,非常使用于Java的Web服务上。
- 混合垃圾收集器
- G1
JVM常用的一些工具
- jps 查询JVM进程状态
- jstat 虚拟机统计信息监控 jstat -gcutil
- jinfo 查询jvm隐式的参数
- 可视化工作故障处理
- JConsle
- VisualVM
- 可插拔的安装辅助插件
- 生成、浏览堆快照文件
- 分析程序性能
- BTrace动态日志追踪
- 基于JDK的Instarument接口开发,允许第三方程序以代理的方式访问JVM内部数据
虚拟机类加载机制
- Java虚拟机class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称为虚拟机的类加载机制。
- 类加载过程
- 加载
- 通过一个类的全限定名称来获取定义此类的二进制字节流
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
- 验证
- 准备
- 解析
- 初始化
- 加载
类加载时机
- 创建类的实例,new 一个对象
- 访问某个累的实例变量,或者对该类的静态变量赋值
- 调用该类的静态方法
- 反射 Class.forName(“com.zxy.xx”)
- 初始化一个类的子类
- JVM的启动类
需要注意的一点:
对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。
类加载器
- BootStrap类加载器:它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
- Extension类加载器:它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
- Application类加载器:加载class path下的jar包和类
- 自定义类加载器
双亲委派模型的工作过程
- 如果一个类加载器收到了类加载的请求,它不会马上自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此所有的类加载器都会传递到最顶层的类加载器中,只有当父类加载器反馈自己无法完成这个加载请求的时候,子类加载器才会自己去加载这个类。
- 优点:保证Java程序安全稳定运行、防止系统中出现多份相同的类字节码