Java基础知识和JVM补充
I/O
read ->【准备数据 -> 数据就绪 -> 拷贝数据】 (内核) -> read返回
BIO(Blocking i/o)
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。如果BIO要处理多个客户端得i/o就只能创建多个线程来处理。
read ->【准备数据 -> 数据就绪 -> 拷贝数据】 (内核) -> read返回 , 全程阻塞
NIO (Non-blocking/New I/O)
NIO 可以看作是I/O 多路复用模型,属于同步非阻塞 IO 模型。同步非阻塞模型即是可以反复调用read,不会阻塞,用户可以一直发起read调用,而内核在数据拷贝用户空间任然是阻塞的。
read -> 准备数据 -> 数据就绪 -> {拷贝数据 -> read返回} 阻塞
但这个任然存在问题,不断进行轮询数据是否准备好,会消耗大量的CPU资源。
i/o多路复用对其进行了改进,具体做法就是,将准备数据到数据准备就绪阶段进行拆分,用户发送调用,系统准备好之后就返回一个信号,接着用户开始read,内核read返回。
具体Java中的NIO模型,主要有三个部分:Channel,Buffer, Selector。
AIO 是NIO的升级版,是异步 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
**
JVM
JVM是运行java程序的虚拟机软件,JVM直接运行在系统中。
一个java程序从开发到运行的全过程
程序员编写.java文件(源代码文件) ⇒ 计算机使用javac.exe程序
将.java文件编译成.class文件(字节码文件) ⇒ 计算机使用java.exe程序
将.class文件送到JVM中运行 ⇒ 运行的过程中随时向核心类库中调用Java编写好的程序来支撑自己编写程序的运行
Java虚拟机包含什么?运行时数据区包含什么
JVM虚拟机包含类加载器、运行时数据区以及执行引擎;运行时数据区有包含方法区、堆、本地方法栈、栈、程序计数器。后面三个为线程独有。
堆分为几个部分?
堆主要存放对象实例和数组。分为新生代(1/3)、老年代(2/3)、大对象区、元空间(原来的老年代)(放入本地内存中)
一个创建对象的过程
- 类加载检查:检查指令参数是否能在常量池(方法区)定位到一个类的符号引用。并检测其类是否被加载过、解析和初始化
- 分配内存:从堆中分配内存空间
- 初始化零值:除了对象头,都初始化零
- 进行必要设置:对象头
- 执行init方法:直接构造函数,new
补充知识:
一个对象包括对象头(Object Header)+实例数据(Instance Data) +对齐填充(Padding)。
-
Mark Word :用于存储对象的运行时数据,包括对象的哈希码、GC状态、锁状态等。这部分信息可以帮助垃圾回收器(GC)管理堆内存及线程安全。
-
类型指针(Class Pointer) :指向对象的类元数据,记录对象所属的类信息,包括类的结构、方法和字段的描述信息。
一个类的加载过程?
JVM 类加载过程分为,加载
、链接
、初始化
、使用
和卸载
这五个阶段,在链接中又包括:验证
、准备
、解析
。
加载:Java 虚拟机规范对 class 文件格式进行了严格的规则,但对于从哪里加载 class 文件,却非常自由。Java 虚拟机实现可以从文件系统读取、从JAR(或ZIP)压缩包中提取 class 文件。除此之外也可以通过网络下载、数据库加载,甚至是运行时直接生成的 class 文件
链接:
- 验证: 确保被加载类的正确性,验证字节流是否符合 class 文件规范
- 准备:为类的静态变量分配内存并设置变量初始值等。final修饰在编译时就会分配。
- 解析:解析包括解析出常量池数据和属性表信息。将符号引用替换为正常引用。
初始化:类加载完成的最后一步就是初始化,目的就是为标记常量值的字段赋值,以及执行 <clinit>
方法的过程。JVM虚拟机通过锁的方式确保 clinit 仅被执行一次
使用:程序代码执行使用阶段
卸载:程序代码退出、异常、结束等
类加载器有几种?
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。每个 Java 类都有一个引用指向加载它的 ClassLoader
。类加载器的主要作用就是加载 Java 类的字节码( .class
文件)到 JVM 中(在内存中生成一个代表该类的 Class
对象)。
JVM内置了三种类加载器
BootstrapClassLoader
(启动类加载器,JVM内部实现):最顶层的加载类,主要用来加载 JDK 内部的核心类库(%JAVA_HOME%/lib
目录下的rt.jar
、resources.jar
、charsets.jar
等 jar 包和类)以及被-Xbootclasspath
参数指定的路径下的所有类。ExtensionClassLoader
(扩展类加载器):主要负责加载%JRE_HOME%/lib/ext
目录下的 jar 包和类以及被java.ext.dirs
系统变量所指定的路径下的所有类。AppClassLoader
(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。
什么是双亲委派机制?
ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。意思就是当前级别启动器优先由父类进行加载。
并且类加载器之间的父子关系多使用组合而不是继承来实现。
源代码如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
//用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);
//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}
由此可以得到双亲委派机制的执行过程
- 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载
- 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器
loadClass()
方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器BootstrapClassLoader
中。 - 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的
findClass()
方法来加载类)。 - 如果子类加载器也无法加载这个类,那么它会抛出一个
ClassNotFoundException
异常
使用双亲委派机制的好处?
避免了类的重复加载,避免核心类库被修改导致的各种异常以及安全问题
打破双亲委派机制方法?
自定义类加载器,然后重写 loadClass()
。例如Tomcat
Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader
来打破双亲委托机制。
什么是Java的垃圾回收机制,如何触发?
垃圾回收(Garbage Collection,Gc)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。主要针对堆。发生以下情况时会进行回收
- 内存不足
- 手动请求
- JVM参数
- 对象数量和内存使用达到阈值
判断垃圾的方法有哪些?
引用计数法和可达性分析算法
引用计数法:
- 原理:为每个对象分配一个引用计数器,当有引用的时候,计数器+1,当引用失效时,计数器-1.计数器为0,代表对象不被引用,可以被回收。
- 缺点:不能解决循环引用的问题(即多个对象相互引用)
可达性分析算法:
- 原理:从一组GC Roots的对象出发,向下追溯它们的引用对象,及其引用的其它对象。如果一个对象到GC Roots没有任何引用链接,就认为不可达,能够被回收
- GC Roots对象有 虚拟机栈引用的对象、 方法区在类静态属性引用的对象、本地方法栈中引用的对象、活跃线程的引用。
垃圾回收算法是什么,是为了解决什么问题?
垃圾回收算法是用于自动管理内存的一种机制,其主要目标是回收不再使用或无法访问的对象所占用的内存空间,从而防止内存泄漏、降低内存使用量并提高系统性能。
能够解决
- 内存泄漏: 一些对象分配了内存无法释放导致的浪费
- 内存溢出:程序需要的内存超过的可用内存
垃圾回收算法有哪些?
标记-清除算法:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
会导致问题如下:
- 效率问题:标记和清除两个过程效率都不高。
- 空间问题:标记清除后有大量内存碎片
复制算法:为了解决算法效率和内存碎片问题。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
会导致问题如下:
- 可用内存变小为原来的一半
- 不适合老年代:如果存活对象数量比较大,复制性能会变得差
标记-整理算法:根据老年代的特点提出的一种标记算法。标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
问题:
- 效率不高
分代收集算法(虚拟机的垃圾收集):据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
- 在新生代中,每次收集都会有大量对象死去,所以可以选择“复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
- 而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
HotSpot 为什么要分为新生代和老年代?
为了通过不同的垃圾回收策略高效管理内存、优化程序性能、减少停顿时间,并有效应对对象的生命周期特性。这样的设计使得 JVM 能够在高负载条件下保持良好的性能表现。
- 新生代的回收 :新生代使用的是复制算法(Copying),通常会进行频繁的、快速的垃圾回收。由于新生代中对象的存活率较低,回收效率较高,回收时只需要检查少量对象。这使得新生代的回收速度快,并且内存碎片问题较少
- 老年代的回收: 老年代中的对象生命周期较长,采用的垃圾回收算法一般更为复杂(如标记-清除或标记-整理),回收的频率较低,目的是减少停顿时间和分配消耗。因此对于老年代的回收会设置较长的间隔。
JAVA的垃圾收集器有哪些?
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
Serial(串行)收集器: **新生代采用复制算法,老年代采用标记-整理算法。**只会使用一条垃圾收集线程去完成垃圾收集工作,并且暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。
ParNew 收集器**(并发)**:除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。新生代采用标记-复制算法,老年代采用标记-整理算法
Parallel Scavenge 收集器( JDK1.8 默认收集器):关注吞吐量(高效率的利用 CPU)
- -XX:+UseParallelGC: 使用 Parallel 收集器+ 老年代串行
- -XX:+UseParallelOldGC: 使用 Parallel 收集器+ 老年代并行
Serial OLD:Serial 收集器的老年代版本
- 作为 CMS 收集器的后备方案
- 在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用
Parallel Scavenge OLD: P arallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。 并发收集,低停顿。标记清除算法
- 初始标记:短暂停顿,标记直接与root相连的对象
- 并发标记:同时开启GC和用户线程,用一个闭包去记录可达对象。会跟踪记录用户对象发生的引用更新
- 重新标记:修正并发标记期间因为用户运行导致标记变动的对象的标记对象
- 并发清除:开启用户线程,同时GC对未标记区域进行扫除
问题:
- CPU资源敏感
- 无法处理浮动垃圾(被对象的引用所删除(不再可达),但还尚未被垃圾回收器回收的对象。)
- 会有碎片空间
G1收集器(JDK9默认):G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
- 初始标记:短暂停顿,标记直接与GC Roots相连的对象
- 并发标记:与应用并发运行,标记所有可达对象。
- 最终标记:处理并发标记阶段结束后残留的少量未处理的引用变更
- 筛选回收:根据标记结果,选择回收价值高的区域,复制存活对象到新区域,回收旧区域内存。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region。G1收集器会试图首先回收那些利用率较低、垃圾较多的区域
ZGC :ZGC 也采用标记-复制算法。
引用类型有哪些?
强引用(Strong Reference) :如果一个对象具有强引用,Java垃圾收集器不会回收它,即使系统内存不足时也是如此。它的典型用法是通过直接赋值给对象变量。
Object obj = new Object();
软引用(Soft Reference) :描述一些有用但不是必需的对象。当系统内存不足时,垃圾收集器会回收这些对象。软引用可以通过 java.lang.ref.SoftReference
类创建。
弱引用(Weak Reference) :弱引用描述一些非必需对象。只有当系统进行垃圾收集时,弱引用指向的对象会被回收。可以使用 java.lang.ref.WeakReference
类来创建弱引用。
虚引用(Phantom Reference) :它并不会决定对象的生命周期。使用 java.lang.ref.PhantomReference
类可以创建虚引用。虚引用的存在仅用于在对象被垃圾回收时收到一个系统通知。
弱引用具体场景?
- 缓存系统
- 对象池
- 避免内存泄漏
minorGC、majorGC、fullGC的区别?
Minor GC 指的是对年轻代内存区域(Young Generation)的垃圾收集。
Major GC 指的是对老年代内存区域(Old Generation)的垃圾收集。
Full GC 是对整个堆内存区域的垃圾收集,包括年轻代和老年代和元空间。
参考
https://xiaolincoding.com/interview/java.html
gpt