JVM常见知识点

jre, jdk, jvm的关系

 jdk是最小的开发环境,由jre+java工具组成。

jre是java运行的最小环境,由jvm+核心类库组成。

jvm是虚拟机,是java字节码运行的容器,如果只有jvm是无法运行java的,因为缺少了核心类库。

JVM内存模型

  1.  堆 - 对象,共享
  2. 方法区 - 类信息,静态变量,常量池,共享 (Java 8移除了永久代,替换为元空间,静态变量、常量池迁移到堆中)
  3. 虚拟机栈 - 线程执行方法的时候内部局部变量,堆中对象地址等数据
  4. 本地方法栈 - 存放各种native方法的局部变量表之类的信息
  5. 程序计数器 - 记录当前线程执行到哪一条字节码指令位置

对象4种引用

  1.  强引用(内存泄漏主因)
  2. 软引用(只有软引用的话,内存不足将被回收),适合缓存用
  3. 弱引用(只有软引用的话,一旦GC就会回收)
  4. 虚引用(用于跟踪GC状态,管理堆外内存)

对象的构成

一个对象分为3个区域:对象头、实例数据、对齐填充。

  1. 对象头:主要包括两部分:(1)存储自身运行时数据比如hash码、分代年龄、琐标记等(但是不是绝对的,锁状态如果是偏向锁,轻量级锁,是没有hash码的);(2)指向类的元数据指针。(还有可能有第三部分,即数组类型,会多出一块记录数组的长度,因为数组的长度是jvm判断不出来的,jvm只有元数据信息)
  2. 实例数据:会根据虚拟机分配策略来定,分配策略中,会把相同大小的类型放在一起,并按照定义顺序排列(父类的变量也会在) 
  3. 对齐填充:在虚拟机规范中对象必须是8字节的整数,所以当对象不满足这个情况时,就会用占位符填充。

如何判断一个对象是否存活

一般判断对象是否存活有两种算法,一种是引用计数器,另一种是可达性分析。在java中主要是第二种。

Java是根据什么来执行可达性分析的

根据GC ROOTS,GC ROOTS对象有:虚拟机栈中的引用对象,方法区的类变量的引用,方法区中的常量引用,本地方法栈中的对象引用。

JVM类的加载顺序

  1. 加载 - 获取类的二进制字节流,将其静态存储结构转化为方法区的运行时数据结构
  2. 检验 - 文件格式验证,元数据验证,字节码验证,符号引用验证
  3. 准备 - 在方法区中对类的static变量分配内存并设置类变量数据类型默认的初始值,实例变量会在对象实例化时随着对象一起分配在Java堆中
  4. 解析 - 将常量池内的符号引用替换为直接引用的过程
  5. 初始化 - 为类的静态变量赋予正确的初始值(Java代码中被显式赋予的值)

JVM的三种类加载器

  1. 启动类加载器(home)- 加载jvm核心类库,如java.lang.*等
  2. 扩展类加载器(ext)- 父加载器为启动类加载器,从jre/lib/ext下加载类库
  3. 应用程序类加载器(用户classpath)- 父加载器为启动类加载器,从环境变量中加载类

双亲委派机制

  1. 类加载器收到类加载请求
  2. 把这个请求委托给父加载器去完成,一直向上委托,知道启动类加载器
  3. 启动类加载器检查能不能加载,能加载就加载(结束),否则,抛出异常,通知子类加载器进行加载

双亲委派模型有啥作用

保证Java基础类在不同的环境还是同一个Class对象,避免出现自定义类覆盖基础类的情况,导致出现安全问题,还可以避免类的重复加载。(保障类的唯一性和安全性以及保证JDK核心类的优先加载)

如何打破双亲委派模型

  1. 自定义类加载器,继承ClassLoader类重写loadClass方法
  2. SPI (Service Provider interface)
    1. 服务提供接口(服务发现机制)
    2. 通过加载ClassPath下META-INF/services,自动加载文件里所定义的类
    3. 通过ServiceLoader.load/Service.providers方法通过反射拿到实现类的实例

☞ tomcat是如何打破双亲委派模型的:

tomcat有着特殊性,它需要容纳多个应用,需要做到应用级别的隔离,而且需要减少重复性加载,所以可划分为:/common容器和应用共享的类信息,/service容器本身的类信息,/share应用通用的类信息,/WEB-INF/lib应用级别的类信息。整体可分为:bootstrapClassLoader->ExtensionClassLoader->ApplicationClassLoader->CommonClassLoader->CatalinaClassLoader(容器本身的加载器)/ShareClassLoader(共享的)->WebAppClassLoader。虽然第一眼是满足双亲委派模型的,但实际上不是,因为双亲委派模型要先交给父类装载,而tomcat是优先判断是否是自己负责的文件位置,从而加载的。

☞ SPI应用

  1. 应用于JDBC获取数据库驱动连接过程就是应用这一机制
  2. apache最早提供的common-logging只有接口,没有实现,发现日志的提供商通过SPI来具体找到提供商实现类

双亲委派机制的缺陷

  1.  双亲委派核心的越基础的类由越上层的加载器进行加载,基础的类总是作为被调用代码调用的API,无法实现基础类调用用户的代码
  2. JNDI服务它的代码由启动类加载器去加载,但是它需要调用独立厂商实现的应用程序。解决办法:线程上下文类加载器(Thread Contex ClassLoader),JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类记载器去完成加载动作。Java中所有涉及SPI的加载动作基本上都采用这种方式,如JNDI, JDBC

导致FullGC的原因

  1. 老年代空间不足
  2. 永久代(方法区)空间不足
  3. 显式调用system.gc()

堆外内存的优缺点

Ehcache中的一些版本,各种NIO框架,Dubbo,Memcache等中会用到,NIO包下ByteBuffer来创建堆外内存,堆外内存就是不受JVM控制的内存。

☞ 相比堆内内存,堆外内存的优势

  1. 减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作;
  2. 加快了复制的速度,因为堆内在flush到远程时,会先复制到直接内存(非堆内存),然后再发送,而堆外内存相当于直接省略掉了复制这项工作;
  3. 可以扩展更大的内存空间,比如超过1TB甚至比主内存还大的空间。

☞ 堆外内存的缺点

堆外内存难以控制,如果内存泄漏,那么很难排查,通过-XX: MaxDirectMemorySize来指定阈值,当达到阈值时,调用system.gc来进行一次full gc堆外内存这种方式不适合村粗很复杂的对象。

JVM七种垃圾收集器

  1. Serial收集器(复制算法,单线程,新生代)
  2. ParNew收集器(复制算法,多线程,新生代)
  3. Parallel Scanvenge收集器(复制算法,多线程,新生代,高吞吐量)
  4. Serial Old收集器(标记-整理算法,老年代)
  5. Parallel Old收集器(标记-整理算法,老年代,注重高吞吐量的场景下,jdk8默认采用Parallel Scavenge + Parallel Old的组合)
  6. CMS收集器(标记-清除算法,老年代,垃圾回收线程几乎能做到与用户线程同时工作,吞吐量低,内存碎片,以牺牲吞吐量为代价来获得最短回收停顿时间 jdk1.8默认垃圾收集器Parallel Scavenge+Parallel Old,jdk1.9默认垃圾收集器G1)
  7. G1收集器(新生代+老年代,在多CPU和大内存的场景下有很好的性能)

 ☞ CMS使用场景

  1. 应用程序堆停顿比较敏感
  2. 在JVM中,有相对存活时间较长的对象(老年代比较大)更适合使用CMS

☞ CMS垃圾回收过程

  1. 初始标记 - 找到gc root (stop the world)
  2. 并发标记(三色标记算法)- 三色标记算法处理并发标记出现对象引用变化情况:黑-自己+子对象标记完成;灰:自己完成,子对象未完成;白:未标记。多线程下并发标记会产生漏标问题,所以CMS必须重新标记一遍
  3. 重新标记(stop the world)
  4. 并发清理

☞ G1垃圾回收过程

  1. 初始标记 - 标记出gc roots直接关联的对象,这个阶段速度较快,需要停止用户线程,单线程执行
  2. 并发标记 - 从gc root开始对堆中的对象进行可达性分析,找出存活对象,这个阶段耗时较长,但是可以和用户线程并发执行
  3. 最终标记 - 修正在并发标记阶段因用户程序执行而产生的标记记录
  4. 筛选回收 - 筛选回收阶段会对各个Region的回收价值和程本进行排序,根据用户所期望的GC停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是Garbage First,第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程(stop the world)。

你可能感兴趣的:(Java)