1. JVM中有哪些垃圾回收算法
Java中的垃圾回收算法有以下几种
1. 标记-清除算法
- 工作原理:首先遍历堆中的对象,标记出所有存活的对象,接着清除未标记的对象。
- 优点:实现简单,能够处理堆中的所有对象
- 缺点:标记和清除的过程会产生内存碎片,影响后续内存分配的效率
2. 标记-整理算法
- 工作原理:首先标记出所有存活的对象,然后将存活的对象整理到一边,最后清除未标记的对象
- 优点:避免了内存碎片问题
- 缺点:整理阶段需要移动对象,会导致额外的开销
3. 复制算法
- 工作原理:将内存分为两部分,每次只使用其中一半,垃圾回收时将存活的对象从一半复制到另一半,清除原区域的所有对象。
- 优点:无需处理内存碎片,分配效率高
- 缺点:需要双倍的内存空间,浪费了一半的空间
2. JVM的TLAB是什么
TLAB是JVM中为每个线程分配的一小块堆内存,用于加速对象的分配操作。每个线程都有自己的TLAB,大大加速了内存分配的同时避免了多线程竞争共享堆内存时的同步开销。
工作原理:
- 每个线程在执行过程中有先从自己的TLAB中分配内存
- 当TLAB内存耗尽时,线程会重新向Eden区申请一个新的TLAB,或者直接从Eden区分配内存
- 对象超过一定大小时(大对象),不会在TLAB中分配,而是直接在Eden区分配
3. Java是如何实现跨平台的
Java程序在编译后生成字节码(.class文件),而不是直接生成特定于某一操作系统的机器代码。
在不同操作系统上都有各自实现的JVM,负责将字节码翻译为特定平台的机器代码并执行。使得同一份Java字节码可以在任何支持的JVM平台上运行。
4. 编译执行和解释执行的区别是什么?JVM使用哪种方式
编译执行:指程序在执行之前,首先通过编译器将源代码编译为机器代码。然后直接在CPU上运行。如C、C++。
- 优点:编译后的程序运行速度快,因为机器代码是针对目标平台直接生成的,且不需要在运行时在进行翻译。
- 缺点:程序必须针对每个平台重新编译,跨平台性差;另外,编译后生成的机器代码难以调试和逆向工程。
解释执行:解释执行指源代码不经过编译器的预先编译,而是在运行时通过解释器逐行翻译并执行。常见的解释语言如python、ruby。
- 优点:跨平台性好,因为代码在每个平台上都是通过相应平台的解释器来运行的,且开发周期更短。
- 缺点:运行速度较慢,因为每次执行时都需要进行动态翻译和解释。
JVM采用 编译执行
和 解释执行
相结合的方式:
- 解释执行:JVM会逐行解释执行字节码,尤其是程序初次运行时,这种方式有助于程序的跨平台性。
- 即时编译(JIT):JVM引入了即时编译器,在程序运行时将热代码编译为本地机器码,避免反复解释,提升性能。
5. JVM的内存区域如何划分
Java虚拟机运行时数据区域划分为方法区、堆、虚拟机栈、本地方法栈、程序计数器。
- 方法区
- 存储类信息、常量、静态变量和即时编译器编译后的代码
- 属于线程共享区域,所有线程共享方法区内存
- 在JDK8 之前,HotSpot使用永久代(PermGen)实现方法区,JDK8之后被元空间取代,元空间使用的是直接内存。
- 堆
- 用于存放所有线程共享的对象和数组,是垃圾回收的主要区域
- 虚拟机栈
- 每个线程创建一个栈,用来保存局部变量、操作数栈、动态链接、方法出口信息等。
- 局部变量表中存储的是基本数据类型以及对象引用
- 栈是线程私有的,生命周期和线程相同
- 本地方法栈
- 为本地方法服务,使用JNI调用的本地代码在此区域分配内存
- 和虚拟机栈类似,也是线程私有的
- 程序计数器
- 是一个小的内存区域,保存当前线程执行的字节码指令的地址或者行号
- 每个线程都有一个独立的程序计数器,属于线程私有
还有一个直接内存,他属于JVM之外的内存区域:
- 由NIO库通过ByteBuffer直接分配的内存
- 直接内存的大小不受堆内存限制,但会收到本机内存的限制
6. Java中堆和栈的区别是什么
栈:主要用于存储局部变量和方法的调用信息(如返回地址、参数等)。在方法执行期间,局部变量被创建在栈上,并在方法结束时被销毁。
堆:用于存储对象实例和数组,每当使用new关键字创建对象时,jvm都会在堆上为该对象分配内存空间。
从其他方面进一步区分
- 生命周期:JVM里面的垃圾回收主要是对堆空间的处理,而栈空间是不会被回收的,所以栈空间的生命周期都非常短,比如一次方法的调用,调用的时候存入,执行完成就弹出。而堆空间是需要通过GC进行回收的,所以堆空间的生命周期会相对较长。
如果是引用数据类型,比如A a = new A(); 这种a分配到栈空间是一个地址,指向堆中的实例化的A。
如果A中定义了一个属性 B b = new B();这个b并不会存在栈空间,而是直接放在堆空间,存储的是实例化的B的地址。
7. 什么是Java中的直接内存
Java中的直接内存是由操作系统分配的内存区域,不受JVM堆内存管理限制。直接内存通过java.nio包中的ByteBuffer.allocateDirect()方法分配,可以绕过JVM垃圾回收机制,直接与本地系统内存交互。
1. 直接内存的优势
由于直接内存不需要在堆上进行分配和复制数据,因此和操作系统的IO操作时可以减少一次复制
。提升性能,在文件读写和网络传输场景直接内存有很大的优势。
2. 性能优化策略
- 使用缓存机制:可以缓存ByteBuffer.allocateDirect()分配的缓冲区,减少频繁的直接内存分配。
- 合理设置JVM参数:堆外内存不归JVM设置的堆大小限制,但是可以通过设置-XX:MaxDirectMemorySize来设置内存的最大使用量,避免不必要的内存消耗。
3. 直接内存和堆内存的区别
- 分配位置:堆内存由JVM管理,受GC控制;直接内存由操作系统分配,使用本地内存
- 访问速度:直接内存的访问速度在特定场景下更快,因为减少了堆内存到本地内存的复制。
- 回收机制:堆内存由垃圾回收器挥手,而直接内存的回收需要通过调用ByteBuffer的cleaner方法进行清理。
8. 什么是Java中的常量池
Java中的常量池是一块用于存储运行时的常量或者符号的区域。主要存在于两种地方
- 运行时常量池:在每个类或接口的class文件中存储编译时生成的常量信息,并在类加载时进入JVM方法区。
- 字符串常量池:用于存储字符串字面量,位于堆内存中的一块特殊区域,通过String类中的intern方法可以讲字符串加入到字符串常量池。
常量池的作用:
常量池用于减少重复对象的创建,节省内存并提高效率
。在Java编译过程中,一些常用的常量如字符串、基本类型等会存储在常量池中,避免重复创建相同的常量。
1. 字符串常量池和堆内存
在Java中,字符串的创建方式有两种
- 直接使用字面量:String s = “Hello”;会将 "Hello"存储在常量池中,如果常量池中已存在 “Hello”,则不会重复创建。
- 使用new关键字:String s = new String(“Hello”);会在堆中创建一个新的String对象,而不涉及常量池。
9. Java的类加载器
Java的类加载器是JVM中用于动态加载类文件的组件。他将.class文件中的字节码加载到内存中,并将其转换为class对象,以供JVM执行。
类加载器的作用:
- 动态加载类:在运行时根据需要加载类,而不是在编译时加载所有类。
- 隔离不同的类命名空间:通过不同的类加载器,可以隔离同名类,使得他们不会相互冲突。
类加载器的层次结构
JDK8的时候一共有三种类加载器
- 启动类加载器:属于虚拟机自身的一部分,用C++实现的,是所有类加载器的父亲。
- 扩展类加载器
- 应用程序类加载器:Java实现,独立于虚拟机,主要负责加载用户类路径上的类库。如果,我们没有实现自定义的类加载器那这个加载器就是我们程序中的默认加载器。
9. Java类加载过程
类加载指的是把类加载到JVM中,把二进制流存储到内存中,之后经过一番解析,处理转化成可用的class类。二进制流来源于class文件,或通过字节码工具生成的字节码或来自于网络,只要符合格式的二进制流。JVM来者不拒。
类加载过程分为
- 加载
- 连接
- 初始化
连接还能拆分为:验证、准备、解析三个阶段。
所以,总的来看,类加载过程分为5个阶段
- 加载:将二进制流读入内存中,生成一个Class对象。
- 验证:主要是验证加载进来的二进制流是否符合一定格式,是否规范,是否符合当前JVM版本等等之类的验证。
- 准备:为静态变量赋初始值,也即为他们在方法区划分内存空间。这里注意是静态变量,并且是初始值,比如int的初始值是0.
- 解析:将常量池的符号引用转化成直接引用。符号引用可以理解为一个字面量,直接引用指的是一个真实引用。
- 初始化:执行一些静态代码块,为静态变量赋值,这里的赋值才是代码里面的赋值。准备阶段只是设置初始值占个坑。
10. 什么是Java中的JIT
Java中的JIT编译器是一种在 程序运行时将字节码转换为机器码的技术
。在Java程序运行的时候,发现热点代码时,将这段代码编译成机器码,减少解释执行的开销。
11. 什么是Java的AOT
Java的AOT是一种在程序运行之前,将Java字节码直接编译为本地机器码的技术。JIT是在Java运行时将一些代码编译成机器码,而AOT是在代码运行之前就编译成机器码。