【每日鲜蘑】学习JAVA必须知道的知识

6,565 阅读7分钟

简单梳理Java知识时,整理的笔记,分享一下,如有错误,还请指正,谢谢🙏

跨平台运行

字节码是不区分系统的,而 JDK 是区分系统的。各个平台编译的字节码都符合 JVM 的规范,从而做到一次编译到处运行。

Java跨平台的优势已经消失,其他语言可以通过Docker来实现跨平台。而Java虚拟机在Docker中运行却成为劣势,主要是容器体积和启动时间问题,当然也有一些黑科技能够减轻这些问题带来的影响,比如jlink,AOT等等

编译过程

编译由 Java 源码编译器(javac)来完成。

过程:源代码---【词法分析器】---Token 流---【语法分析器】---语法树---【语义分析器】---注解抽象语法树---【字节码生成器】---JVM 字节码

三个过程

  • 解析与填充符号表
  • 注解处理
  • 分析和字节码生成

语法糖【泛型】

泛型只会在 Java 源码中存在,编译过后会被替换为原来的原生类型(Raw Type,也称为裸类型)了。这个过程也被称之为“泛型擦除”。

  • 代码更加简洁【不用强制转换】
  • 程序更加健壮【只要编译时期没有警告,那么运行时期就不会出现 ClassCastException 异常】
  • 可读性和稳定性【在编写集合的时候,就限定了类型】

类的加载

不是所有的类都会被加载到 JVM 的,Java 类的加载是动态的,只有在需要用到的时候才会被加载,这样节省了内存开销。

默认的类加载器

  1. 启动类加载器:【Bootstrap ClassLoader】负责加载$JAVA_HOME 中 jre/lib/rt.jar里所有的 class,由 C++实现,不是 ClassLoader 子类。
  2. 扩展类加载器【Extension ClassLoader】负责加载 java 平台中扩展功能的一些 jar 包,包括$JAVA_HOME 中 jre/lib/*.jar 或-Djava.ext.dirs 指定目录下的 jar 包。
  3. 应用类加载器【Application ClassLoader】负责记载classpath中指定的 jar 包及目录中 class。
  4. 用户自定义类加载器【User ClassLoadder】

双亲委派【解决安全性问题】

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。

  • 好处: 防止内存中出现多份同样的字节码(安全性角度)

类加载器在成功加载某个类之后,会把得到的 java.lang.Class 类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载

通过自定义 ClassLoader,并重写父类的 loadClass 方法可以打破双亲委派的机制,而 Java 的 SPI(服务提供发现机制)也是打破双亲委派机制的。

详细过程

  1. 加载,查找并加载类的二进制数据,在 Java 堆中也创建一个 java.lang.Class 类的对象
  2. 连接,连接又包含三块内容:验证、准备、初始化。
    • 1)验证,文件格式、元数据、字节码、符号引用验证;
    • 2)准备,为类的静态变量分配内存,并将其初始化为默认值;
    • 3)解析,把类中的符号引用转换为直接引用
  3. 初始化,为类的静态变量赋予正确的初始值。

JIT 即时编译器

【Just In Time】将热点代码的字节码重新编译优化,让 CPU 直接执行,这样的执行效率会更高。HotSpot 是基于计数器来检测热点代码的而非采样。

GraalvmAOT相比JIT而言更具有时代性,直接编译成Native Image, 提供了更小的体积和更快的启动速度。

类加载完后

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。

JVM 的垃圾回收算法

通过“可达性分析算法”分析哪些对象是垃圾,然后通过垃圾回收算法进行回收。

  • Serial 收集器,串行收集器是最古老,最稳定以及效率高的收集器,但可能会产生较长的停顿,只使用一个线程去回收。
  • ParNew 收集器,ParNew 收集器其实就是 Serial 收集器的多线程版本
  • Parallel 收集器,Parallel Scavenge 收集器类似 ParNew 收集器,Parallel 收集器更关注系统的吞吐量
  • Parallel Old 收集器,Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程“标记-整理”算法
  • CMS 收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它需要消耗额外的 CPU 和内存资源,在 CPU 和内存资源紧张,CPU 较少时,会加重系统负担。CMS无法处理浮动垃圾。CMS 的“标记-清除”算法,会导致大量空间碎片的产生
  • G1 收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征

JVM 内存模型

JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。

栈管运行,堆管存储。

JVM 内存会划分为堆内存和非堆内存,堆内存中也会划分为年轻代和老年代,而非堆内存则为永久代(方法区、元空间)。

年轻代又会分为 Eden 和 Survivor 区。Survivor 也会分为 FromPlace 和 ToPlace,toPlace 的 survivor 区域是空的。Eden,FromPlace 和 ToPlace 的默认占比为 8:1:1。

当然这个东西其实也可以通过一个 -XX:+UsePSAdaptiveSurvivorSizePolicy 参数来根据生成对象的速率动态调整

内存泄漏&内存溢出

  • 内存泄漏:对象可达,但是对象不会被使用。
  • 内存溢出:装载类的空间不够、内存空间不足、内存泄漏等等都可能导致溢出。

线程栈

JVM 规范让每个 Java 线程拥有自己的独立的 JVM 栈,也就是 Java 方法的调用栈。

当方法调用的时候,会生成一个栈帧。栈帧是保存在虚拟机栈中的,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息

线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。

JVM 年轻代到年老代的晋升过程的判断条件是什么

  • 部分对象会在 From 和 To 区域中复制来复制去,如此交换 15 次(由 JVM 参数 MaxTenuringThreshold 决定,这个参数默认是 15),最终如果还是存活,就存入到老年代;
  • 如果对象的大小大于 Eden 的二分之一会直接分配在 old,如果 old 也分配不下,会做一次 majorGC,如果小于 eden 的一半但是没有足够的空间,就进行 minorgc 也就是新生代 GC;
  • minor gc 后,survivor 仍然放不下,则放到老年代;
  • 动态年龄判断 ,大于等于某个年龄的对象超过了 survivor 空间一半 ,大于等于某个年龄的对象直接进入老年代;

JVM 出现 fullGC 很频繁,怎么去线上排查问题

  • 是不是频繁创建了大对象(也有可能 eden 区设置过小)(大对象直接分配在老年代中,导致老年代空间不足--->从而频繁 gc)
  • 是不是老年代的空间设置过小了(Minor GC 几个对象就大于老年代的剩余空间了) 1. 如果一次 full GC 后,剩余对象不多,那么说明 Eden 的空间设置太小,导致大量短生命周期的对象被分配到了老生代。 2. 如果一次 full GC 后,老生代的变化不大,那么是老生代分配空间太小了。

JVM 垃圾回收机制,何时触发 MinorGC 等操作

  • 当 young gen 中的 eden 区分配满的时候触发 MinorGC(新生代的空间不够放的时候).