JVM内存分哪几个区,每个区的作用是什么?
程序计数器:
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。在多线程环境下,每个线程都有自己的程序计数器,互不影响。
Java虚拟机栈:
Java虚拟机栈用于存储方法执行的局部变量、操作数栈、动态链接、方法出口等信息。每个方法在执行的同时会创建一个栈帧,栈帧用于存储方法的局部变量和部分运行时数据。
本地方法栈:
本地方法栈与Java虚拟机栈类似,区别在于本地方法栈为Native方法服务,即Java调用Native方法的时候使用的栈。
Java堆:
Java堆是Java虚拟机中内存最大的一块,用于存储对象实例和数组。Java堆是垃圾回收器管理的主要区域,也是垃圾回收的主要区域。
方法区:
方法区用于存储类的结构信息、常量、静态变量、即时编译器编译后的代码等数据。在Java 8及之前的版本中,方法区被称为永久代(Permanent Generation)。
运行时常量池:
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
java堆分为那些区域?
新生代:
新生代是堆内存中存放新创建对象的区域。新生代通常被划分为Eden区和两个Survivor区(通常是From和To)。大部分新创建的对象会被分配到Eden区,经过一段时间的垃圾收集后仍然存活的对象会被移动到Survivor区,经过多次垃圾收集后仍然存活的对象会被移动到老年代。
老年代:
老年代是堆内存中存放长期存活对象的区域。通常情况下,新生代中经过多次垃圾收集仍然存活的对象会被移动到老年代。老年代的对象一般比较稳定,垃圾收集频率较低。
永久代:
永久代用于存放JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Java 8及之前的版本中存在永久代,但在Java 8之后被元数据区(Metaspace)所取代。
元数据区:
元数据区是Java 8及之后版本中取代永久代的一种内存区域,用于存放类的元数据信息、常量池、静态变量等。元数据区的大小可以动态调整,不受固定大小的限制。
Java中垃圾收集的方法有哪些?
复制算法 年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)
效率高,缺点:需要内存容量大,比较耗内存
使用在占空间比较小、刷新次数多的新生区
标记-清除 老年代一般是由标记清除或者是标记清除与标记整理的混合实现
效率比较低,会差生碎片。
标记-整理 老年代一般是由标记清除或者是标记清除与标记整理的混合实现
效率低速度慢,需要移动对象,但不会产生碎片。
如何判断一个对象是否存活?(或者GC对象的判定方法)?
引用计数法:
引用计数法是一种简单的垃圾收集算法,它通过对对象的引用计数进行统计来判断对象是否存活。每当有一个新的引用指向对象时,引用计数加一;当引用失效时,引用计数减一。当引用计数为0时,表示对象不再被引用,可以被回收。
然而,引用计数法无法解决循环引用的问题,当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
可达性分析法:
可达性分析法是Java虚拟机中常用的判断对象存活的方法。该方法通过一组称为"GC Roots"的对象作为起始点,从这些对象开始向下搜索,能够到达的对象被认为是存活的,否则被认为是垃圾。如果一个对象不在GC Roots的引用链上,那么该对象就是不可达的,即可被判定为垃圾对象。
可以作为GC Roots的对象有以下几种?
虚拟机栈中引用的对象、本地方法栈引用的对象、方法区类静态属性引用的对象、方法区常量池引用的对象
什么情况下会产生StackOverflowError(栈溢出)?
递归调用层次过深:
当一个方法递归调用的层次过深时,每次方法调用都会在栈中创建一个新的栈帧,如果递归调用没有终止条件或者终止条件设置不当,栈空间会被耗尽,导致StackOverflowError。
局部变量占用过多栈空间:
在一个方法中定义过多的局部变量,或者某个局部变量占用的空间过大,都会导致栈空间被耗尽,从而触发StackOverflowError。
无限循环调用:
如果存在一个方法或者一组方法之间相互调用,形成了无限循环调用的情况,栈空间会不断增长直到耗尽,导致StackOverflowError。
大量线程同时调用:
当有大量线程同时调用方法,每个线程都会占用一定的栈空间,如果线程数量过多或者每个线程占用的栈空间过大,会导致总的栈空间不足,触发StackOverflowError
什么情况下会产生OutOfMemoryError(堆溢出)?
对象占用内存过大:
当程序中创建的对象占用的内存空间过大,超出了虚拟机堆内存的限制,就会导致内存溢出错误。
内存泄漏:
当程序中存在内存泄漏问题时,即程序中创建的对象无法被及时释放,导致堆内存中的对象越积越多,最终耗尽了可用内存空间,引发内存溢出错误。
持续大量对象创建:
当程序中持续不断地创建大量对象,而这些对象又无法被及时回收,堆内存会被迅速耗尽,导致内存溢出错误。
数据量过大:
当程序需要处理的数据量过大,超出了虚拟机堆内存的承载能力,无法一次性加载或处理所有数据时,也会触发内存溢出错误。
虚拟机堆内存设置不当:
如果虚拟机堆内存的大小设置不当,比如分配的堆内存过小,无法满足程序运行的需求,也会导致内存溢出错误的发生。
什么是线程池,线程池有哪些(创建)?
线程池是一种管理和复用线程的机制,它可以在程序启动时创建一定数量的线程,并将它们保存在池中,根据需要动态地重复利用这些线程,从而减少线程创建和销毁的开销,提高程序的性能和响应速度。
FixedThreadPool(固定大小线程池):
固定大小线程池会创建固定数量的线程,并且池中的线程数始终保持不变。当有任务提交时,如果线程池中有空闲线程,则立即执行;如果没有空闲线程,则任务会被放入队列中等待执行。
CachedThreadPool(缓存线程池):
缓存线程池会根据需要创建新的线程,如果池中没有可用线程,则会创建新线程;如果线程在60秒内没有被使用,则会被终止并从池中移除。适用于执行大量短期异步任务的场景。
SingleThreadExecutor(单线程线程池):
单线程线程池只会创建一个工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。适用于需要顺序执行任务的场景。
ScheduledThreadPool(定时任务线程池):
定时任务线程池可以定期执行任务或者延迟执行任务,可以设置固定的间隔时间或者延迟时间来执行任务。
WorkStealingPool(工作窃取线程池):
工作窃取线程池是Java 8新增的线程池,使用ForkJoinPool实现,每个线程都有自己的工作队列,当一个线程执行完自己队列中的任务后,会去其他线程的队列中窃取任务执行,以提高线程利用率。
为什么要使用线程池?
降低资源消耗:
线程池可以重复利用已创建的线程,避免频繁地创建和销毁线程所带来的资源消耗,减少了系统开销。
提高响应速度:
线程池中的线程可以立即执行任务,无需等待线程创建,能够更快地响应任务请求,提高了程序的响应速度。
提高性能:
通过合理配置线程池的大小和参数,可以更好地管理线程资源,避免线程过多导致的资源竞争和上下文切换,从而提高程序的性能。
控制并发线程数量:
线程池可以限制并发执行的线程数量,防止系统因为线程过多而导致资源耗尽或性能下降的问题,保证系统稳定运行。
提供任务队列:
线程池通常会配备一个任务队列,可以存储等待执行的任务,当线程池中的线程都在执行任务时,新的任务会被放入队列中等待执行,避免任务丢失或阻塞。
统一管理和监控:
通过线程池,可以统一管理和监控线程的状态、执行情况和异常情况,方便对线程进行监控、调优和管理。
线程池底层工作原理
线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。当然也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程。
调用execute()提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的线程执行这个任务
如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存
如果队列已满,并且线程池中工作线程的数量<maximumPoolSize,还是会创建线程执行这个任务
如果队列已满,并且线程池中的线程已达到maximumPoolSize,这个时候会执行拒绝策略,JAVA线程池默认的策略是AbortPolicy,即抛出RejectedExecutionException异常
ThreadPoolExecutor的参数有那些?
corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(存活时间)、unit(存活时间单位)、workQueue(任务队列)、threadFactory(线程工厂)、handler(拒绝策略)
怎么设定核心线程数和最大线程数?
CPU密集型: 核数 +1
IO 密集型:cpu 核心数的 2 倍
拒绝策略有哪些?
AbortPolicy(默认):直接抛出RejectedExecutionException异常,阻止系统正常运行。
CallerRunsPolicy:由调用线程处理该任务。
DiscardPolicy:默默地丢弃无法处理的任务,不予任何处理。
DiscardOldestPolicy:丢弃最早进入队列的任务,然后尝试重新提交当前任务。
常见线程安全的并发容器有哪些?
ConcurrentHashMap 支持高并发的读写操作。 实现线程安全(1.8之前 分段锁、1.8之后 cas+sync)
CopyOnWriteArrayList 、CopyOnWriteArraySet 实现线程安全(写时复制)
ConcurrentLinkedQueue 实现线程安全(基于链表实现的队列,使用无锁算法)
ConcurrentLinkedDeque:线程安全的双端队列。
CAS原理
CAS 是一种乐观锁技术,通过比较内存中的值和期望值是否一致来判断是否进行更新,如果一致则更新,不一致则不更新。
CAS 包含三个操作数:内存值 V、旧的预期值 A 和新值 B。当且仅当 V 的值为 A 时,CAS 会通过原子性地将 V 的值更新为 B。如果 V 的值和 A 不一样,说明在操作期间内存值已经被其他线程修改过,CAS 操作失败,此时需要重新尝试。
synchronized底层实现是什么?lock底层是什么?有什么区别?
synchronized 是 Java 中的一个关键字,用于控制多线程的访问,确保同一时刻只有一个线程可以执行被同步的代码块或方法。底层实现主要依赖于 JVM 的 monitor 机制,通过对象监视器(monitor)来实现同步。
当一个线程想要进入同步代码块或方法时,它必须获取相应的 monitor。如果其他线程已经拥有这个 monitor,那么新的线程会等待,直到当前的线程释放 monitor。
在 Java 对象头中,包含了一个指向 monitor 的指针。每个对象都有一个 monitor,其中保存了锁的信息,比如拥有者、等待者列表等。当一个线程尝试获取对象锁时,会进入到等待队列中,等待锁释放。
Lock 的底层实现通常依赖于monitor机制,这是基于操作系统层面的实现。在Java中,ReentrantLock和synchronized关键字都是基于monitor实现的。
ReentrantLock通过AQS实现,它是一个可以用来构建锁和同步器的框架。ReentrantLock内部维护了一个sync类,它继承了AQS并实现了Lock接口。当你调用ReentrantLock的Lock方法时,它会调用sync内部类的acquire方法,而这个方法会使用CAS操作尝试获取锁。
区别:
sync是一个关键字,lock是一个接口;
sync可以锁代码块,也可以锁方法,而lock只能锁代码块;
sync是非公平锁,而lock支持公平锁和非公平锁;
sync不需要手动释放锁,而lock需要手工释放锁;
当一个线程使用synchronize获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的条件下提供一种退出的机制。
synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式
volatile关键字的理解
Volatile:有序性、可见性
volatile是Java提供的最轻量级的同步机制,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象。同时volatile禁止了指令重排,可以保证程序执行的有序性,但是由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱。
synchronized和volatile有什么区别?
volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是可以锁定变量操作,只有当前线程可以访问操作该变量,其他线程被阻塞住。
volatile仅能用在变量级别,而synchronized可以使用在方法、类级别。
volatile仅能实现可见性,不能保证原子性;而synchronized则可以保证可见性和原子性。
volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。
volatile标记的变量不会被编译器优化,synchronized的操作可以被编译器优化。
Java类加载过程?
Java类加载过程主要分为加载、链接和初始化三个阶段。
加载:查找并加载类的二进制数据。
链接:
验证:确保被加载的类的正确性。
准备:为类分配内存,并初始化静态变量。
解析:将类中的符号引用转换为直接引用。
初始化:为类的静态变量赋予正确的初始值。
什么是类加载器,类加载器有哪些?
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
启动类加载器、扩展类加载器、应用程序类加载器、用户自定义类加载器
双亲委派机制解决了类加载的冲突和重复加载的问题,它通过以下方式确保了类的唯一性和安全性,并为模块化开发提供了基础支持
工作原理是当一个类需要被加载时,它的加载请求会被委托给它的父类加载器。如果父类加载器无法完成加载请求(例如,找不到该类的Class文件),则将请求委派给其父类加载器,直到最顶层的启动类加载器。只有当最顶层的启动类加载器也无法加载时,才会由当前类加载器自己进行加载。
简述java内存分配与回收策略以及Minor GC和Major GC(full GC)
内存分配开头就有;
(1)对象优先在堆的Eden区分配。
(2)大对象直接进入老年代。
(3)长期存活的对象将直接进入老年代。
当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC。Minor GC通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收速度比较快;Full Gc、Major GC 发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor GC这样可以加快老年代的回收速度。
JAVA(中级篇)面试题整理
于 2024-05-31 18:09:06 首次发布