Central Topic
2024-07-10 20:14:50 9 举报
AI智能生成
11
作者其他创作
大纲/内容
技术栈总结
ArrayList
扩容规则
1、new ArrayList() 默认空数组<br>2、初始化时为10容量<br>3、add(),空数组扩容为10,再次扩容为上次的1.5倍,,创建新数组将旧数组Copy过去<br>4、addAll(),数组为空,扩容为Math.max(10,元素个数),不为空时,扩容为Math.max(容量*1.5,元素个数),取最大的值
特性
1. 基于数组,需要连续内存<br>2. 随机访问快(指根据下标访问)<br>3. 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低<br>4. 数据局部性:由于ArrayList 的元素是存储在连续的内存位置上,访问这些元素时可以利用到 CPU 的数据缓存。这是因为现代 CPU 通常会将连续的内存块加载到高速缓存中,以提高数据访问速度
LinkedList
特性
1、基于双向链表,无连续内存<br>2、随机访问慢(需要遍历链表)<br>3、头尾插入删除性能高
Iterator
遍历<br>本质是Iteratoor
ArrayList
迭代器通过构造方法初始化会记录数组大小<br>modCount集合大小<br>expectedModCount需要遍历多少次<br>通过checkForComodification()检查,如果不相等则抛出异常
HashMap
1.7版本
总结
1、数组+链表,初始容量16<br>2、采用头插法,在并发的情况下容易发生死链<br>3、put值,根据key键,hashCode取值,在经过二次hash,二次hash是为了让数据分布更均匀,最后在根据位运算&(容量(capacity )-1)
1.8版本
总结
1、数组+链表+红黑树,初始容量16<br>2、采用尾插法<br>3、put值,根据key键,hashCode取值,在经过二次hash,二次hash是为了让数据分布更均匀,最后在根据位运算&(容量(capacity )-1)
链表意义
1、当链表长度大于8,首次会尝试扩容,在经过二次hash计算,减少链表长度,<br>如果数组容量已经>=64,才进行树化<br>2、变红黑树,是因为链表过长,O(1)变O(n),且查询对比数据过多
红黑树意义
1、红黑树用来避免dos攻击,防止链表超长超时性能下降,树化是保底策略
树化规则
树化规则<br>1、当链表长度超过树化阈值 8 时,先检查数组容量是否>=64<br>情况1:触发扩容,重新hash取模,减少链表长度<br>情况2:触发树化<br><br>2、退化规则<br>情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表,退化为链表可以节省内存空间和提高性能<br><br>情况2:remove 树节点时,树结构过于分散或不平衡,或者删除节点后的数量小于等于 6,则可能会选择将树退化为链表,果在树中某个节点的左子节点、右子节点或左子节点的左子节点为 null,也会选择将树退化为链表
流程
1、hashMap是懒惰创建数组,首次使用才创建数组<br>2、计算hash取值<br>3、节点插入之前,检查当前元素数量是否已经超过了容量乘以负载因子的阈值(0.75)。超过则触发扩容操作,将数组的容量翻倍<br>3、如果数组下标没人占用,创建Node占位返回<br>4、如果数组下标被占用<br>a、已是TreeNode走红黑树的添加和更新<br>b、是普通Node,走链表的添加和更新,如果链表长度超过树化阈值,走树化逻辑
区别
1.7和1.8区别<br>1、链表插入节点时,1.7 是头插法,1.8 是尾插法<br>2、1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容<br>3、1.8 在扩容计算 Node 索引时,会优化(红黑树)
扩容(加载)因子为何默认是 0.75f
1. 在空间占用与查询时间之间取得较好的权衡<br>2. 大于这个值,空间节省了,但链表就会比较长影响性能<br>3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多
单例模式
饿汉式
实现方式1
public class Singleton {<br> private Singleton() {<br> System.out.println("private Singleton()");<br> }<br><br> private static final Singleton INSTANCE = new Singleton();<br><br> public static Singleton getInstance() {<br> return INSTANCE;<br> }<br>}
缺点
1、会被反射、序列化破坏<br>
实现方式2
public class Singleton implements Serializable {<br> private Singleton() {<br> System.out.println("private Singleton()");<br> }<br><br> private static final Singleton INSTANCE = new Singleton();<br><br> public static Singleton getInstance() {<br> return INSTANCE;<br> }<br><br> public Object readResolve() {<br> return INSTANCE;<br> }<br>}<br>
防止被序列化破坏
实现方式3,究极结合体
public class Singleton implements Serializable {<br> private Singleton() {<br> if (INSTANCE != null) {<br> throw new RuntimeException("单例对象不能重复创建");<br> }<br> System.out.println("private Singleton()");<br> }<br><br> private static final Singleton INSTANCE = new Singleton();<br><br> public static Singleton getInstance() {<br> return INSTANCE;<br> }<br><br> public Object readResolve() {<br> return INSTANCE;<br> }<br>}
防止被构造破坏
枚举饿汉式单例
public enum Singleton2 {<br> INSTANCE;<br><br> private Singleton2() {<br> System.out.println("private Singleton2()");<br> }<br><br> @Override<br> public String toString() {<br> return getClass().getName() + "@" + Integer.toHexString(hashCode());<br> }<br><br> public static Singleton2 getInstance() {<br> return INSTANCE;<br> }<br> <br>}
枚举饿汉式能天然防止反射、反序列化破坏单例
懒汉式
public class Singleton3 implements Serializable {<br> private Singleton3() {<br> System.out.println("private Singleton3()");<br> }<br><br> private static Singleton3 INSTANCE = null;<br><br> // Singleton3.class<br> public static synchronized Singleton3 getInstance() {<br> if (INSTANCE == null) {<br> INSTANCE = new Singleton3();<br> }<br> return INSTANCE;<br> }<br><br> public static void otherMethod() {<br> System.out.println("otherMethod()");<br> }<br><br>}
同步锁结合static 锁的是当前Class
双检锁懒汉式
public class Singleton4 implements Serializable {<br> private Singleton4() {<br> System.out.println("private Singleton4()");<br> }<br><br> /*<br> * * `INSTANCE = new Singleton4()` 不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值,其中后两步可能被指令重排序优化,变成先赋值、再调用构造<br> * 如果线程1 先执行了赋值,线程2 执行到第一个 `INSTANCE == null` 时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造的对象<br> * */<br> private static volatile Singleton4 INSTANCE = null; // 可见性,有序性<br><br> public static Singleton4 getInstance() {<br> if (INSTANCE == null) {<br> synchronized (Singleton4.class) {<br> if (INSTANCE == null) {<br> INSTANCE = new Singleton4();<br> }<br> }<br> }<br> return INSTANCE;<br> }<br><br> public static void otherMethod() {<br> System.out.println("otherMethod()");<br> }<br>}
内部类懒汉式
public class Singleton5 implements Serializable {<br> private Singleton5() {<br> System.out.println("private Singleton5()");<br> }<br><br> private static class Holder {<br> static Singleton5 INSTANCE = new Singleton5();<br> }<br><br> public static Singleton5 getInstance() {<br> return Holder.INSTANCE;<br> }<br><br> public static void otherMethod() {<br> System.out.println("otherMethod()");<br> }<br>}
JDK 中单例的体现
* Runtime 体现了饿汉式单例<br>* Console 体现了双检锁懒汉式单例<br>* Collections 中的 EmptyNavigableSet 内部类懒汉式单例<br>* ReverseComparator.REVERSE_ORDER 内部类懒汉式单例<br>* Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例
线程
Java线程状态
新建(New):当线程对象被创建但还没有调用 start() 方法时,线程处于新建状态。<br><br>运行(Running):线程获取到CPU时间片后,开始执行线程的 run() 方法,此时线程处于运行状态。<br><br>阻塞(Blocked):线程由于某些原因被阻塞,无法继续执行,比如等待一个锁、等待输入输出等待等情况,线程会进入阻塞状态。<br><br>等待(Waiting):线程进入等待状态是因为调用了 Object.wait()、Thread.join() 或 LockSupport.park() 等方法,线程会一直等待直到其他线程调用notify()或notifyAll()来唤醒它。<br><br>超时等待(Timed Waiting):线程进入超时等待状态是因为调用了带有超时参数的方法,比如 Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis) 等,线程会在指定的时间之后自动唤醒。<br><br>终止(Terminated):线程执行完毕或者由于异常退出了 run() 方法,线程将进入终止状态。
操作系统
创建态(New):当进程被创建时,处于创建态。在这个状态下,操作系统正在为进程分配资源和初始化。<br><br>就绪态(Ready):表示进程已经准备好执行,正在等待分配CPU时间片。<br><br>运行态(Running):表示进程目前正在执行指令。<br><br>阻塞态(Blocked):表示进程由于某些原因(比如等待I/O完成)而暂时无法执行,处于等待状态。<br><br>结束态(Terminated):表示进程已经完成任务或被终止,不再执行。
线程池
ThreadPoolExecutor
7大核心参数
核心线程数
线程池初始化的固定核心线程数不会死亡
最大线程数
当队列任务数上限且核心线程数上限才会创建的线程,最大线程数包括核心线程数
存活时间
非核心线程的空闲时间
存活时间单位
通常单位秒,毫秒
获取设备cpu核心数
Runtime.getRuntime().availableProcessors()
队列
当核心线程上限后,则任务会存放队列,队列上限后,才创建非核心线程数执行
7种队列类型
ArrayBlockingQueue:基于数组的有界阻塞队列,先进先出,当队列上限,阻塞新任务,直到有空间可用
LinkedBlockingQueue:基于链表的可选有界或无界阻塞队列,先进先出,当队列有界上限,阻塞新任务,直到空间可用,无界则不会阻塞插入
SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的对应移除操作,反之亦然。这种队列在任务传递过程中直接传递,而不保存它们。可以被用于传递实时消息
PriorityBlockingQueue:基于优先级的无界阻塞队列,按优先级处理,任务必须实现Comparable接口或者队列定制的Comparator决定顺序
DelayQueue:一个支持延迟获取元素的无界阻塞队列,其中的元素只有在其指定的延迟时间到了才能被取出。该队列是无界的,而且不允许插入null元素
LinkedTransferQueue:一个无界队列,可以用于生产者-消费者模式。与其他队列不同,它允许生产者线程将元素直接传递给消费者线程,而不需要将元素存储在队列中。
WorkStealingPool:Java 8 引入的一种特殊类型的线程池,其工作队列是由Deque(双端队列)实现的,每个线程都有自己的工作队列。空闲的线程会从其他线程的队列中窃取任务来执行,以提高整体的任务处理能力
线程工厂
用于创建新线程的工厂。通过指定线程工厂,可以自定义线程的创建过程,例如设置线程的名称、优先级等
实现方式
实现ThreadFactory接口<br>
// 实现ThreadFactory接口的方法,用于创建新线程<br> @Override<br> public Thread newThread(Runnable r) {<br> // 创建新线程,并设置线程名称为指定前缀加上当前纳秒时间<br> Thread t = new Thread(r, namePrefix + "-" + System.nanoTime());<br> t.setPriority(priority); // 设置线程优先级为指定优先级<br> return t; // 返回创建的线程<br> }
拒绝策略
4种拒绝策略
AbortPolicy:默认,当线程池无法接受新任务时,会抛出RejectedExecutionException异常
CallerRunsPolicy:当线程池无法接受新任务时,会使用提交任务的线程来执行该任务,可以降低新任务的流量,但可能会降低整体性能
DiscardPolicy:当线程池无法接受新任务时,会直接丢弃该任务,不会抛出异常。这可能会导致部分任务丢失,慎用
DiscardOldestPolicy:当线程池无法接受新任务时,会丢弃工作队列中最早加入(未被执行)的任务,然后尝试重新提交新任务
自定义策略实现方式
实现RejectedExecutionHandler接口
// 实现rejectedExecution方法,用于定义自定义的拒绝策略<br> @Override<br> public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {<br> // 输出被拒绝的任务信息<br> System.err.println("Task Rejected: " + r.toString());<br> <br> // 这里可以根据业务需求来定义具体的拒绝策略,例如将任务记录到日志中、放入特定队列中等<br> <br> // 这里示例的简单处理方式是直接抛出异常<br> throw new RejectedExecutionException("Task Rejected: " + r.toString());<br> }
任务执行流程
1、线程池创建,初始化核心线程数,准备接受任务<br>2、新的任务进来,使用空闲的核心线程数执行<br> (1)、核心线程数上限后,将进来的任务放入阻塞队列,空闲的核心线程会自动取阻塞队列获取任务执行<br> (2)、阻塞队列上限后,就创建新线程执行,最大只能创建到最大线程数,也就是(最大线程数 - 核心线程数)<br> (3)、最大线程数都执行任务完成后,会根据指定的空闲时间自动销毁,最终保持到核心线程数大小<br> (4)、如果线程数开到了最大线程数,还有新任务进来,就会使用reject指定的拒绝策略<br>3、所有的线程创建都是由指定的Factory创建的
线程池状态
1、RUNNING:运行状态,线程池新建或调用execute()方法后,处于运行状态,能够接受新的任务<br> <br>2、SHUTDOWN:关闭状态,调用shutdown()方法后,变为关闭状态,不在接受新的任务,但会执行已提交的等待队列中的任务<br> <br>3、STOP:停止状态,调用shutdownNow()方法后,变为stop状态,不在接受新的任务,并且会中断正在处理的任务<br> <br>4、TIDYING:当线程池处于Shutdown或Stop状态时,如果等待队列中还有未执行的任务,则线程池将执行整理操作,将等待队列中的未执行任务移除,并保存到一个列表中<br> <br>5、TERMINATED:当线程池处于Shutdown状态,并且等待队列中的任务全部执行完毕,或者在Stop状态下,线程池内部的所有线程都已经终止时,线程池进入Terminated状态
JUC
Synchronized
1、可重入锁、重量级锁、同步锁、非公平锁,不可中断<br>2、Java层面的关键字,Jvm的锁<br>3、自动加锁,解锁,不可获取当前线程是否上锁<br>4、锁的是对象,锁信息在对象的对象头中<br>5、底层有锁的升级过程
锁升级过程
偏向锁:当一个线程访问同步块时,如果这个同步块未被其他线程访问过,并且没有锁竞争,会尝试获取偏向锁,并将对象头的进程ID设置为自己的ID,标记为偏向模式,在未来访问中,线程进入同步块,无需在进行任何同步操作,提高性能
轻量级锁:当偏向锁失败时,表示有其他线程尝试竞争同步块,这时JVM会尝试升级为轻量级锁,轻量级锁获取是通过CAS操作来实现,如果,某个线程获取了轻量级锁,则表示它独占同步资源,如果其他线程也想获取这个锁,也会发生自旋,而不是阻塞线程,自旋的目的是等待持有锁的线程释放锁,如果自旋失败,表示有多个线程竞争锁,则轻量级锁会升级为重量级锁
重量级锁:当自旋超过一定次数或持续时间后,轻量级锁会升为重量级锁,重量级锁会导致竞争失败的线程进入阻塞等待,直到持有锁的线程释放锁,重量级锁使用操作系统的互斥量来实现线程的阻塞和缓存,性能开销大,但确保了线程的安全
核心:轻量级锁,会去使用CAS不断自旋获取锁,目的是为了避免频繁地将现场在用户态和内核态之间切换,从而减少操作系统介入的开销
ReentrantLock
1、Java层面的类,Api层的锁,公平锁与非公平锁,可重入锁<br>2、手动加锁与释放锁<br>3、可获取当前线程是否上锁<br>4、可中断<br> 1、可设置超时时间tryLock()<br> 2、可调用lockInterruptibly()为了线程在获取锁的过程中能够响应中断请求<br>interrupt() 方法是用来中断线程的执行<br>5、通过state判断锁状态
Volatile
1、Java层面关键字<br>2、修饰变量,具有可见性、有序性、防止指令重排<br>3、缺点:不能保证原子性<br>当线程修改Volatile变量会导致其他线程的工作内存失效,从而去读取主内存,保证可见性<br>由于应用层面在操作系统层面是很慢的,在多线程情况下,它会有个优化,导致指令重排,Volatile是借助于内存屏障实现有序性,防止指令重排
原子性:每一段的代码都应该是不可插入的部分,因为CPU是交互切换执行,所以并发情况是A代码刚到读取数据缓存了,正好切换到B代码修改数据了,此时的变量是无感知的,导致A代码使用的是旧数据
public class AddAndSubtract {<br><br> static volatile int balance = 10;<br><br> public static void subtract() {<br> int b = balance;<br> b -= 5;<br> balance = b;<br> }<br><br> public static void add() {<br> int b = balance;<br> b += 5;<br> balance = b;<br> }<br><br> public static void main(String[] args) throws InterruptedException {<br> CountDownLatch latch = new CountDownLatch(2);<br> new Thread(() -> {<br> subtract();<br> latch.countDown();<br> }).start();<br> new Thread(() -> {<br> add();<br> latch.countDown();<br> }).start();<br> latch.await();<br> System.out.println(balance);<br> }<br>}
可见性:说法1,在多线程在内存中读取变量的数据,在自己的本地缓存中缓存了一遍,所以导致会存在脏数据问题<br>
// 可见性例子<br>// -Xint<br>//在JDK编译器中有个 jit组件,会根据热点代码优化,因为在0.1秒钟,可以执行很多次超过阈值,默认把stop变量直接替换成了false,所以会导致foo方法停不下来,其他线程能正常的感知到stop变量变为true<br>//或者禁用jit组件使用 -Xint,但是不可能为了一个变量去禁用一个组件<br>public class ForeverLoop {<br> static volatile boolean stop = false;<br><br> public static void main(String[] args) {<br> new Thread(() -> {<br> try {<br> Thread.sleep(100);<br> } catch (InterruptedException e) {<br> e.printStackTrace();<br> }<br> stop = true;<br> System.out.println("modify stop to true...");<br> }).start();<br><br> new Thread(() -> {<br> try {<br> Thread.sleep(200);<br> } catch (InterruptedException e) {<br> e.printStackTrace();<br> }<br> System.out.println(stop);<br> }).start();<br><br> new Thread(() -> {<br> try {<br> Thread.sleep(200);<br> } catch (InterruptedException e) {<br> e.printStackTrace();<br> }<br> System.out.println(stop);<br> }).start();<br><br> foo();<br> }<br><br> static void foo() {<br> int i = 0;<br> while (!stop) {<br> i++;<br> }<br> System.out.println("stopped... c:" + i);<br> }<br>}
悲观锁
悲观锁的代表是 synchronized 和 Lock 锁<br><br>* 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】<br>* 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能<br>* 实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
乐观锁
乐观锁的代表是 AtomicInteger,使用 cas 来保证原子性<br><br>* 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】<br>* 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换<br>* 它需要多核 cpu 支持,且线程数不应超过 cpu 核数<br>
CAS
1、比较并交换,解决并发访问问题<br>2、有三个值,内存地址(变量的引用),期望值,旧值<br>3、非阻塞的,不需要加锁,因此在一些高并发的情况下可以提供更好的性能
1、读取内存地址中的当前值。<br>2、检查当前值是否等于期望值。<br>3、如果相等,则将新值写入内存地址;否则,操作失败。<br>4、返回操作是否成功的结果。
ABA
如果一个值原来是A,后来被改成了B,再被改回A,那么CAS操作会误认为这个值没有被改变过。为了解决这个问题,可以使用版本号或者标记来区分不同的状态
循环开销:由于CAS操作是一个循环,如果某个线程长时间不成功,会导致CPU的消耗增加
concurrentHashMap
1.8之前
* 数据结构:`Segment(大数组) + HashEntry(小数组) + 链表`,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突<br>* 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了
初始化时采用的设计模式饿汉式初始化,默认创建segment[0]容量,后续创建segment的时候 都是以原型模式去借用segment[0]的属性<br>
1.8之后
* 数据结构:`Node 数组 + 链表或红黑树`,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能<br>* 并发度:Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容<br>* 扩容条件:Node 数组满 3/4 时就会扩容
AQS:AbstractQueuedSynchronizer
1、线程同步器<br>2、使用一个整型的状态(state)来表示同步资源的状态,同时使用一个等待队列来管理处于等待状态的线程<br>3、AQS通过一个FIFO双向队列(即等待队列)来管理多个线程的竞争和等待状态,并提供了一些方法供子类来管理和控制这个队列<br>4、根据信号量来控制可重入锁,加锁state +1,解锁 -1<br>5、acquire(int arg) 当一个线程尝试获取同步资源时,如果失败(即无法满足获取条件),则会将线程加入到等待队列中,并进入阻塞状态,直到资源可用。<br>6、release(int arg) 当一个线程释放同步资源时,会尝试唤醒等待队列中的其他线程,让它们有机会获取到资源
ThreadLocal
1、Java实现线程局部变量的类,允许每个线程都有自己独立的副本,在多线程环境下实现数据的隔离<br>2、核心使用ThreadLocalMap,内部维护的散列表,储存每个线程的局部变量,ThreadLocalMap键是ThreadLocal实例。值是线程特定的变量副本<br>当一个线程访问 ThreadLocal 的 get() 方法时,实际上是根据当前线程获取对应的 ThreadLocalMap,然后根据 ThreadLocal 实例获取其对应的值<br>
JVM
由线程私有的,程序计数器,虚拟机栈,本地方法栈,线程共享的,方法区,堆组成
Java类编译过程<br>
1、创建JVM,编译成class文件,JVM对其检查class是否是原生文件,不是则通过类加载子系统加载,将类的原始信息存入方法区<br>2、创建主线程使用的是虚拟机栈执行,如果遇见了未见过的类,会继续出发类加载过程,同样存入方法区<br>3、需要创建对象,会使用堆内存来存储对象<br><br>执行 java 命令<br><br>1. 创建 JVM,调用类加载子系统加载 class,将类的信息存入**方法区**<br>2. 创建 main 线程,使用的内存区域是 **JVM 虚拟机栈**,开始执行 main 方法代码<br>3. 如果遇到了未见过的类,会继续触发类加载过程,同样会存入**方法区**<br>4. 需要创建对象,会使用**堆**内存来存储对象<br>5. 不再使用的对象,会由**垃圾回收器**在内存不足时回收其内存<br>6. 调用方法时,方法内的局部变量、方法参数所使用的是 **JVM 虚拟机栈**中的栈帧内存<br>7. 调用方法时,先要到**方法区**获得到该方法的字节码指令,由**解释器**将字节码指令解释为机器码执行<br>8. 调用方法时,会将要执行的指令行号读到**程序计数器**,这样当发生了线程切换,恢复时就可以从中断的位置继续<br>9. 对于非 java 实现的方法调用,使用内存称为**本地方法栈**(见说明)<br>10. 对于热点方法调用,或者频繁的循环代码,由 **JIT 即时编译器**将这些代码编译成机器码缓存,提高执行性能<br>
会发生内存溢出的区域
不会出现内存溢出的区域 – 程序计数器<br><br>出现 OutOfMemoryError 的情况<br> 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收<br> 方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类<br> 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时<br><br> 出现 StackOverflowError 的区域<br> JVM 虚拟机栈,原因有方法递归调用未正确结束、反序列化 json 时循环引用
方法区、永久代、元空间
方法区是 JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等<br><br>永久代是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)<br><br>元空间是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间<br><br>
JVM垃圾回收算法,可达性分析法
标记清除法
第一次,从根节点(如虚拟机栈中的引用、静态变量等)开始,递归地遍历所有可达对象,并标记它们<br>第二次,清除未标记的对象<br><br>缺点,内存碎片化太严重
标记整理法
第一次,从根节点(如虚拟机栈中的引用、静态变量等)开始,递归地遍历所有可达对象,并标记它们<br>第二次,清除未标记的对象,移动所有存活的对象,使它们在堆中连续分布<br><br>缺点,效率比较低,标记整理算法需要移动存活对象,可能会导致一定的延迟,特别是在堆中存活对象较多时
标记复制法
第一次,从根节点(如虚拟机栈中的引用、静态变量等)开始,递归地遍历所有可达对象,并标记它们为活动对象<br>第二次,将所有活动对象从当前使用的内存区域(比如 Eden 区)复制到另一个内存区域(比如 Survivor 区或者老年代)。同时,可能会进行对象的年龄提升(Promotion)操作,将存活时间较长的对象移动到老年代<br>第三次,清除未被标记的对象,即垃圾对象。在复制阶段之后,原来的内存区域变成了不包含存活对象的内存块,可以直接清空或者重置为可用状态<br><br>缺点,需要额外的空间作为复制目标,通常是原来空间的一半大小,移动存活对象可能会引起一定的延迟,特别是当存活对象较多时
收藏
0 条评论
下一页