Java基础知识
文章目录
- Java基础知识
- 一、容器&队列&栈
- 1、List 容器
- ImmutableList:不可变列表,任何的 remove,add 方法都会抛出异常。
- 2、Map 容器
- 3、Queue 队列
- 4、Set 容器
- 5、Stack 栈
- 二、多线程
- 1、线程管理
- 2、同步器
- 3、信号量
- 4、并发集合
- 5、阻塞队列
- 6、线程池
- 7、Future
- 8、分治&并行编程
- 9、Atomic并发包
- 10、其他
- 11、并发包背后依赖
- 三、synchronized原理
- 四、内存锁与总线锁
- 五、堆栈异常
- 六、垃圾回收器设计思想
- 1、垃圾移动方式
- 2、垃圾回收思想
- 3、JVM对象数据结构是图
- 4、JVM对象标记算法
- 七、JVM垃圾回收器回收流程
- 1、Serial
- 2、Parallel
- 3、Cms
- 4、G1
- 4、ZGC
- 八、JVM对象回器参数
- 1、Serial参数
- 2、Parallel参数
- 3、CMS参数
- 4、G1参数
- 5、ZGC参数
- 九、JVM调优
- 1、调整堆内存大小
- 2、垃圾收集器种类选择
- 3、新生代老年代大小比例
- 4、新生代Eden、Survivor空间大小比例
- 5、晋升到老年代的对象年龄
- 6、启用自适应堆大小策略
- 7、垃圾回收器线程暂停时间
- 8、非堆内存的调整
- 9、线程调整
- 十、类加载
- 1、Java 的类加载流程?
- 2、Java 的类加载机制是什么?
- 3、类的加载过程中,发生了什么?
- 4、Java 类加载器有哪些?
- 5、类加载器的双亲委派机制是什么?
- 6、如何打破类加载器的双亲委派机制?
- 1、继承ClassLoader 类并覆盖 findClass方法打破
- 2、定义线程ClassLoader方式打破
- 7、类加载器的作用是什么?
- 8、类加载器都有哪些常见的使用场景?
- 9、类加载器的双亲委派模型有哪些优点?
- 10、如何实现自定义类加载器?
- 11、类加载机制和反射的关系是什么?
- 10、如何实现自定义类加载器?
- 11、类加载机制和反射的关系是什么?
一、容器&队列&栈
1、List 容器
ArrayList:实现了可变大小的数组。在元素数量方面,不需要调整大小。
LinkedList:基于链式结构的集合,顺序地存储元素。每个元素都被赋予了一个指向前后元素的地址。
Vector:类似于 ArrayList。Vector默认所有方法加上了 synchronized 修饰,是线程安全的,但 concurrent 并不高。
Stack:Stack继承自Vector,它的特定在于只能进行栈的操作(push、pop、peek)。
CopyOnWriteArrayList:它是线程安全的,采用的是一种对于写入操作,采取“先复制原有数组,然后往新数组中添加数据,最后将新数组赋给array”这种机制,可以保证读取的同时任何并发的写入操作都不会影响到正在进行的读取操作。
RoleList:基于角色的列表,它允许我们在每个元素上关联若干个角色。一个 RoleList 实例的行为非常类似一个 ArrayList 实例,不同之处在于,我们可以在每个元素上分配若干个角色,角色可以在列表中的最小单元和最大范畴间随意设置。
Vector:类似于 ArrayList,是线程安全的。它默认所有方法加上了 synchronized 修饰,避免了并发修改导致的数据不一致。
ImmutableList:不可变列表,任何的 remove,add 方法都会抛出异常。
2、Map 容器
HashMap:使用数组和链表储存键值对,并且按照哈希算法进行分配储存位置。它允许空键和空值,但不保证顺序。
TreeMap:使用红黑树结构实现,按照键对象的自然顺序进行排序储存。因此,它的键必须实现 Comparable 接口才能使用自然顺序排序。如果没有指定 Comparator,它将使用键按照自然顺序排序,如果指定了 Comparator,它将使用 Comparator指定的比较器对键进行排序。
LinkedHashMap:HashMap的一个子类,它通过额外维护一个双向链表来记录插入顺序或者最近的访问顺序,因此保留了键值对的顺序。
ConcurrentHashMap:线程安全的HashMap,在多线程情况下,它可以提供更高的性能和吞吐量。
3、Queue 队列
LinkedList:LinkedList类实现了List、Deque、Queue接口,由于它支持双向链表,因此它可以被当作队列、列表或者双端队列使用。LinkedList类是一种基于链表结构实现的队列,在添加或删除元素时,链表会重新连接,数据量越大时性能越差;但LinkedList支持快速在队列两端操作元素,将是双向队列的一种好选择。
ArrayDeque:ArrayDeque类实现了Deque接口,它是一个使用数组实现的双端队列。
PriorityQueue:PriorityQueue类实现了Queue接口,它是一个基于优先级的队列。元素按照它们的自然顺序排序,或者根据提供的 Comparator 进行排序。
ConcurrentLinkedQueue:ConcurrentLinkedQueue类实现了Queue接口,它是一个线程安全的队列,使用链表实现。
ArrayBlockingQueue:ArrayBlockingQueue类实现了BlockingQueue接口,它是一个有界的阻塞队列,内部使用一个定长的数组作为队列的缓存,如果队列已满,那么插入操作就会被阻塞。
LinkedBlockingQueue:LinkedBlockingQueue类实现了BlockingQueue接口,它是一个可选有界队列,内部使用单向链表实现,如果队列已满,那么插入操作也会被阻塞。
PriorityBlockingQueue:PriorityBlockingQueue类实现了BlockingQueue接口,它是一个可以根据元素的优先级来进行排序的阻塞队列,使用堆进行排序。
SynchronousQueue:SynchronousQueue类实现了BlockingQueue接口,它是一个没有缓存的阻塞队列,生产者线程必须等到消费者线程消费完这个元素后,才能继续往下生产。
ConcurrentLinkedDeque类:ConcurrentLinkedDeque类是Java中线程安全的双向阻塞队列,可以在两端并发地插入、移除元素,实现了Deque接口。
ArrayDeque:ArrayDeque类是一种基于可重置数组的队列,支持在队列两端移动且不必调整元素的位置。它通常比LinkedList更快,尤其是强制类型的操作,当队列大小已知时,建议使用这种类型的队列。
PriorityQueue:PriorityQueue类是一种基于堆数据结构实现的队列,支持初始大小限制的动态新增、迭代、删除等多种操作。适合需要按照优先级处理元素的场合使用。
ConcurrentLinkedQueue:ConcurrentLinkedQueue类是一种高效的、线程安全的队列类型,它以无锁方式实现并发。如果多个线程需要同时读写和修改队列,则此类是最佳选择。
SynchronousQueue:SynchronousQueue类是一种线程安全的队列,两个线程借助此队列交换数据。SynchronousQueue类的特殊之处在于它不会保存任何元素。如果有读线程想要读,写线程就必须馬上进行写操作,并返回元素。
4、Set 容器
HashSet:基于哈希表实现,散列表中不允许有重复键值,适用于不要求元素顺序的场景。
LinkedHashSet:基于哈希表和双重链表实现,保证了元素插入的顺序和去重功能。
TreeSet:基于红黑树实现,保证了元素的自然顺序或指定的顺序,并保证元素的唯一性。
CopyOnWriteArraySet:线程安全的,它通过在写操作时创建一个新的数组来保证线程安全性。
5、Stack 栈
Stack:Java自带的栈类,基于Vector实现,线程安全,但弊端是性能相对较低。
ArrayDeque:Java自带的双端队列类,也可以当作栈使用,基于数组实现,性能比Stack更好,但不是线程安全的。
LinkedList:Java自带的链表类,也可以当作栈使用,基于双向链表实现,性能比Stack更好,但不是线程安全的。
二、多线程
1、线程管理
Thread:Thread 是 Java 中用于实现多线程的重要类,可以通过继承 Thread 类或者实现 Runnable 接口来实现多线程。线程是程序执行中的基本单位,通过并发运行多个线程,可以提高程序的执行效率和吞吐量。Thread 类提供了一系列的方法来管理线程的生命周期和状态,例如 start()、join()、sleep()、yield() 等。通过这些方法,可以创建、启动、阻塞、等待和终止线程等操作,同时 Thread 类还提供了访问线程状态和环境信息的方法,例如 getState()、getName()、getPriority() 等。
ThreadGroup: Java 中一种线程组管理机制,可以把多个线程组合成一个线程组,并统一管理这些线程的优先级、中断状态和异常信息等。线程组可以减少线程和线程之间的耦合度,提高了应用程序的可维护性和可扩展性。ThreadGroup 类提供了一系列的方法来管理线程组,例如 activeCount()、interrupt()、suspend()、resume()、setMaxPriority() 等。通过这些方法,可以获取当前线程组中的活动线程数、中断线程组中的所有线程、挂起/恢复线程组中的所有线程、设置线程组中所有线程的最大优先级等操作。
ThreadLocal:ThreadLocal 是 Java 中一种线程本地变量的实现方式,可以为每个线程提供独立的变量副本,避免了线程安全问题和可见性问题。线程本地变量是针对每个线程本身不会发生冲突的数据,线程之间的数据互相独立,可以提高多线程应用程序的性能和可靠性。ThreadLocal 类提供了一系列的方法来管理线程本地变量,例如 get()、set()、remove() 等。通过这些方法,可以获取当前线程的本地变量、为当前线程设置本地变量、移除当前线程的本地变量等操作。
2、同步器
ReadWriteLock:ReadWriteLock 是 Java 中一种读写锁机制,用于管理共享资源的读写操作,可以提高读取多于写入的共享资源的性能。它允许多个线程同时读取共享数据,但只允许一个线程写入共享数据。ReadWriteLock 接口提供了一系列的方法来管理读写锁的获取、释放和降级等操作,例如 readLock()、writeLock()、readLock().lockInterruptibly()、writeLock().tryLock() 等。通过这些方法,可以获取/释放读取锁、写入锁、中断读锁等操作。
ReentrantReadWriteLock:ReentrantReadWriteLock是一个读写锁,是Java中concurrent包提供的工具类,它支持多个线程同时读取共享资源,但只能有一个线程写入共享资源。它内部维护了两个锁:读锁和写锁。当读锁被占用时,其他线程仍可以继续获得读锁,但当写锁被占用时,则其他线程无法获取到读锁和写锁。
ReentrantReadWriteLock具有以下特点:
支持公平锁和非公平锁,默认是非公平锁。
支持可重入性,同一个线程可以重复获取读锁或写锁。
支持锁降级,即把持了写锁,再去获取读锁,最后释放写锁,这个过程就是锁降级。
支持Condition条件变量,可以通过Condition等待和唤醒线程。
Condition: Condition 是 Java 中一种与 Lock 配合使用的条件变量,可以协助线程之间的通讯和协作。通过 Condition,线程可以在某个条件满足时等待,当条件改变时被唤醒。Condition 接口提供了一系列的方法来管理条件变量的等待、唤醒和通知等操作,例如 await()、signal()、signalAll() 等。通过这些方法,可以阻塞线程等待某个条件、唤醒当前等待的某个线程等操作。
CountDownLatch:CountDownLatch是Java并发包中用于线程同步的工具,它可以使一个或多个线程等待其他线程完成某些操作后再执行。CountDownLatch使用一个计数器来实现同步,计数器初始值为线程数量,每个线程完成操作后减少计数器的值,直到计数器的值为0时,所有等待的主线程才会被释放。
public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException {int threads = 3;CountDownLatch latch = new CountDownLatch(threads);for (int i = 0; i < threads; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 开始执行...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 执行完成...");latch.countDown(); // 计数器减1}).start();}latch.await(); // 等待计数器归零System.out.println("所有线程执行完毕,继续执行主线程...");}}
3、信号量
Phaser:Phaser可以看作是CyclicBarrier和CountDownLatch的加强版,通过维护一组线程的状态,使得这些线程可以相互协调,并协同完成某个任务,同时可重用。Phaser提供了三种不同的同步方式,分别为:到达同步点后继续执行、到达同步点后进行阻塞等待、和到达同步点后进行优先级排序。此外,Phaser还允许动态地增加或删除参与者(线程)。
public class PhaserDemo {public static void main(String[] args) {int threads = 3;Phaser phaser = new Phaser(threads);for (int i = 0; i < threads; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 开始执行...");phaser.arriveAndAwaitAdvance(); // 到达同步点,等待其他线程System.out.println(Thread.currentThread().getName() + " 继续执行...");phaser.arriveAndDeregister(); // 到达另一个同步点,注销自己}).start();}} }
CyclicBarrier:用于在线程到达某个栅栏时进行等待和阻塞,直到所有线程都到达后才继续执行后续任务。
public class CyclicBarrierDemo {public static void main(String[] args) {int threads = 3;CyclicBarrier barrier = new CyclicBarrier(threads, () -> {System.out.println("所有线程到达栅栏,开始执行后续任务...");});for (int i = 0; i < threads; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 到达栅栏...");try {barrier.await();} catch (InterruptedException | BrokenBarrierException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 继续执行后续任务...");}).start();}}}
Semaphore:用于限制并发访问数量,控制同一时刻并发线程数量的上限。比如:设置3个线程,6个并发过来,只会执行3个,其他线程阻塞。
public class SemaphoreDemo {public static void main(String[] args) {int threads = 5;Semaphore semaphore = new Semaphore(2); // 设置最大并发访问数量为2for (int i = 0; i < threads; i++) {new Thread(() -> {try {semaphore.acquire(); // 获取许可System.out.println(Thread.currentThread().getName() + " 开始执行...");Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + " 执行完成...");} catch (InterruptedException e) {e.printStackTrace();} finally {semaphore.release(); // 释放许可}}).start();}} }
4、并发集合
ConcurrentHashMap:ConcurrentHashMap 是具有高性能、线程安全的 Map 类型,允许多个线程同时对 Map 进行读写操作,同时也支持高并发条件下的迭代操作。它是通过将 Map 分成多个 Segment 来实现高并发操作的,然后在每个 Segment 中使用 ReentrantLock 作为锁来保证线程安全。ConcurrentHashMap 不支持 null 键和 null 值。
CopyOnWriteArrayList:CopyOnWriteArrayList 是一个线程安全的 List 实现,它在读取操作时不需要进行同步阻塞,而是通过进行一次复制操作来保证线程安全,从而避免了由于多个线程同时修改导致的数据不一致问题。当有写入操作时,会通过重新创建一个新的数组来更新数据,因此写入操作的性能较低,但读取操作的性能非常高。CopyOnWriteArrayList 不支持 null 元素。
CopyOnWriteArraySet:CopyOnWriteArraySet 是 CopyOnWriteArrayList 的 Set 实现,与其类似,可以实现在高并发场景下对 Set 数据结构进行安全的读写操作。它也是通过在读取操作时不需要进行同步阻塞,在写入操作时进行一次复制操作来保证线程安全的。
5、阻塞队列
LinkedBlockingQueue:阻塞队列,底层是基于链表实现的。它的容量可以非常大,可以自动扩容,它的读写操作是线程安全的。
ArrayBlockingQueue:阻塞队列,底层是基于数组实现的。它的容量是固定的,一旦初始化就不能再改变。它的读写操作也是线程安全的。
DelayQueue:是一个延时队列,可以用来实现定时任务。它实现了Delayed接口,并且底层使用PriorityQueue来存储元素。
LinkedTransferQueue:非阻塞队列,底层使用链表实现,可以用来实现生产者消费者模式。它比BlockingQueue更加灵活,可以实现一些高级操作。
SynchronousQueue:特殊的队列,它只有一个元素的容量。它在读写操作时需要满足另一方同时也在操作,否则会被阻塞。常用于线程间传递数据。
6、线程池
Executor:是一个接口,用于执行Runnable任务的简单框架。它提供了一种统一的方式来管理线程的生命周期,比直接使用Thread更加灵活。Executor框架可以实现线程池、定时器、调度器等功能。
ExecutorService:是Executor的一个扩展接口,它提供了更多的方法,例如submit()和invokeAll(),可以返回Future对象。ExecutorService可以用于管理生命周期,提供线程池的管理和控制,以及线程任务的执行。
Executors:是一个工厂类,它包含了一些静态方法,用于创建不同类型的线程池、定时任务等。Executors提供了许多工具方法来创建不同的ExecutorService,例如newCachedThreadPool()、newFixedThreadPool()和newSingleThreadExecutor()等。
ThreadPoolExecutor:ThreadPoolExecutor是Java中ExecutorService的默认实现类,它实现了线程池的管理和控制,可以复用线程资源,避免创建和销毁线程的开销。
ThreadPoolExecutor具有以下属性:
corePoolSize:线程池核心线程数,线程池中的线程数不会小于该数量。
maximumPoolSize:线程池最大线程数,线程池中的线程数不会超过该数量。
keepAliveTime:非核心线程的空闲时间超过该时间就会被回收。
TimeUnit:时间单位,用于keepAliveTime参数。
workQueue:等待队列,将等待执行的任务存储在队列中。
threadFactory:用于创建工作线程的工厂类实例。
handler:拒绝策略,用于处理当队列和线程池都已满时如何处理新任务。
ThreadPoolExecutor提供了四种拒绝策略,分别为:
AbortPolicy(默认):当线程池不断处理的任务数已经达到了最大线程数和阻塞队列的容量之和时,再次添加追加任务时就会抛出一个RejectedExecutionException异常。
CallerRunsPolicy:当线程池不断处理的任务数已经达到了最大线程数和阻塞队列的容量之和时,将任务交给调用线程处理。
DiscardPolicy:当线程池不断处理的任务数已经达到了最大线程数和阻塞队列的容量之和时,直接丢弃此任务。
DiscardOldestPolicy:当线程池不断处理的任务数已经达到了最大线程数和阻塞队列的容量之和时,将阻塞队列队头的任务丢弃,再尝试添加新的任务。
7、Future
Future:是Java中使用比较广泛的异步编程接口,用于获取异步任务的结果。Future的特点是可以在线程执行异步操作的同时,立即返回一个Future对象,线程可以继续执行其他操作,之后在需要结果的时候调用Future的get()方法来获取异步操作的结果。
CompletableFuture:是Future的扩展实现,提供了更加强大和灵活的异步编程功能。CompletableFuture支持链式回调和组合多个异步任务的结果,可以通过thenApply()、thenAccept()、thenRun()等方法实现链式回调,在异步任务完成之后继续执行下一个异步任务。并且,CompletableFuture更加高效,在执行异步任务的过程中,它可以更好地利用线程资源,提高程序的并发能力。
//Future 获取异步结果 public class FutureDemo {public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {// 创建一个线程池ExecutorService executor = Executors.newFixedThreadPool(2);// 提交一个Callable异步任务Future<Integer> future = executor.submit(() -> {Thread.sleep(2000);return 123;});// 主线程继续执行其他操作System.out.println("continue do something...");try {// 等待异步操作的结果并获取Integer result = future.get(3000, TimeUnit.MILLISECONDS);System.out.println("result = " + result);} catch (TimeoutException e) {System.out.println("timeout");}// 线程池关闭executor.shutdown();} }
//和Future 获取异步结果功能差不多,不过CompletableFuture提供其他链式回调功能 public class CompletableFutureDemo {public static void main(String[] args) throws Exception {// 创建一个CompletableFuture对象CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return 123;});// 主线程继续执行其他操作System.out.println("continue do something...");// 等待异步操作的结果并获取Integer result = future.get();System.out.println("result = " + result);}}
8、分治&并行编程
ForkJoinTask: 一个抽象类,代表了一个可以被分割为多个子任务并行执行的任务。使用ForkJoinTask可以方便地将一个大型任务拆分成多个小任务并行执行,提高程序的性能。
ForkJoinPool:一个执行ForkJoinTask的线程池,使用ForkJoinPool可以方便地管理ForkJoinTask的执行,包括线程的创建和销毁、任务的分割和合并等操作。
//合并数组 public class ForkJoinDemo {public static void main(String[] args) {// 创建一个整数数组int[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};// 创建一个ForkJoinPool对象ForkJoinPool pool = ForkJoinPool.commonPool();// 创建一个计算整数数组和的任务SumTask task = new SumTask(data, 0, data.length);// 提交任务到线程池执行Integer result = pool.invoke(task);// 输出结果System.out.println(result);// 关闭线程池pool.shutdown();}// 计算整数数组的和的任务static class SumTask extends RecursiveTask<Integer> {private static final int THRESHOLD = 3; // 任务分割的阈值private final int[] data; // 待计算的整数数组private final int start; // 当前任务计算的起始位置(包含)private final int end; // 当前任务计算的结束位置(不包含)public SumTask(int[] data, int start, int end) {this.data = data;this.start = start;this.end = end;}@Overrideprotected Integer compute() {if (end - start <= THRESHOLD) {// 如果当前任务计算的数据量小于阈值,直接计算结果int sum = 0;for (int i = start; i < end; i++) {sum += data[i];}return sum;} else {// 如果当前任务计算的数据量大于阈值,分割任务并合并结果int mid = start + (end - start) / 2;SumTask leftTask = new SumTask(data, start, mid);SumTask rightTask = new SumTask(data, mid, end);leftTask.fork(); // 提交左子任务到线程池执行(异步)Integer rightResult = rightTask.compute(); // 计算右子任务的结果(同步)Integer leftResult = leftTask.join(); // 获取左子任务的结果(同步)return leftResult + rightResult; // 合并结果}}} }
9、Atomic并发包
atomic并发包主要使用cas比较并交换思想编写。
AtomicBoolean:原子性的boolean值
AtomicInteger:原子性的int值
AtomicLong:原子性的long值
AtomicReference:原子性的对象引用,支持get、set、compareAndSet等操作
10、其他
ThreadLocalRandom:ThreadLocalRandom是Java SE 7中新增的一个用于生成随机数的类,它优于旧的Random类的地方在于它不是线程安全的,而是为每个线程独立生成随机数,避免线程间竞争导致的性能问题。
ThreadLocalRandom提供了一些常规的随机数生成方法,如nextInt, nextLong, nextFloat等。下面是一个简单的Java代码示例,演示了如何使用ThreadLocalRandom生成随机数:
public class ThreadLocalRandomDemo {public static void main(String[] args) {int max = 10;ThreadLocalRandom random = ThreadLocalRandom.current();// 生成随机整数int randomInt = random.nextInt(max);System.out.println("随机整数: " + randomInt);// 生成随机长整数long randomLong = random.nextLong(max);System.out.println("随机长整数: " + randomLong);// 生成随机双精度浮点数double randomDouble = random.nextDouble();System.out.println("随机双精度浮点数: " + randomDouble);}}
11、并发包背后依赖
并发编程JMM三要素:原子性、可见性、顺序性
1、cas + volatile
我们都知道并发包背后依赖 cas【原子性】 + volatile【可见性、顺序性】 ,cas通过线程自旋的方式,提供原子性。 volatile 通过刷新cpu寄存器与主存的数据方式提供可见性。
2、cup与cas关系
CAS 是一种硬件指令,由 CPU 的处理器提供支持。当进行 CAS 操作时,操作系统不需要提供额外的特殊支持,CPU 会直接调用自己的硬件指令实现 CAS 操作。
在多线程环境下,多个线程可以同时进行 CAS 操作。当多个线程同时对同一内存地址进行 CAS 操作时,只有其中一个线程可以成功地执行 CAS 操作,其他线程需要重新尝试。
操作系统的作用是提供对 CPU 和内存的管理和调度。当多个线程在处理器上抢占资源时,操作系统会根据不同的调度算法,为线程分配 CPU 时间片,实现多线程在处理器上的并发执行。同时,操作系统还提供了一些进程间通信的机制(如管道、消息队列、共享内存等),方便多线程间进行协作并实现同步和互斥。
因此,操作系统与 CAS 操作的关联在于,操作系统提供的线程管理机制可以提供并发执行的环境,而多线程之间的同步和互斥问题需要由程序员使用同步工具(如锁、信号量等)或基于 CAS 操作实现。
3、cup与volatile关系
CPU层面,volatile关键字通过内存屏障的机制来确保变量的可见性。通过禁止指令重排的方式提供顺序性。
三、synchronized原理
1、锁范围
synchronized关键字是Java中实现同步的主要方法之一,其主要原理是通过对对象(类、对象、方法、静态块、常量)的锁进行获取和释放来实现线程之间的同步,其锁粒度取决锁的对象。
2、锁实现原理
synchronized 关键字进行加锁时,实际上是利用了对象头中的一部分状态位,通过加锁和解锁的方式来改变对象头中的信息,从而达到同步的目的。
3、锁升级过程
无锁:当没有资源竞争就是无锁场景
偏向锁:同一个线程多次获得同一个锁时,可以通过偏向锁来减少加锁和解锁的消耗。
轻量级锁:当多个线程争夺同一个锁时,偏向锁失效,锁会通过 CAS 操作来尝试获取,如果获取成功,就将对象头中的信息转为轻量级锁状态。
重量级锁:如果多个线程一直无法获得锁,就会使用重量级锁,将线程进入阻塞状态,通过操作系统的支持来实现锁的机制。
4、锁背后依赖操作系统
例如在 Linux 下,JVM 会利用操作系统提供的 futex(Fast Userspace muTEX)来实现锁的机制。futex 是一种低级别的系统原语,允许用户空间的线程在内核支持下等待一个特定的内存地址发生变化(futex system call)。这样可以避免进入内核态的开销,提高锁竞争的效率。
Futex 是一种用户空间和内核空间协作的机制,可以在用户态中实现一个简单的锁机制,当锁没有被占用时可以直接在用户态下完成操作,避免了不必要的内核态参与,从而提高了性能。
Futex 的实现依赖于一个内存地址(通常是一个整数地址),称为 futex 地址。Futex 运行时,只是在用户空间简单的检查 futex 值,当 futex 值满足条件时,直接返回;否则通过系统调用进入内核,使用了一些锁操作,等待在等待队列中,直到条件满足或者时间耗尽。当 Mutex 释放时,本来尝试醒来的进程将会被激活,并争用锁。因此,在 Mutex 处于自旋状态时,CPU 不会浪费时间在系统调用和上下文的切换上,而是在用户空间自旋,极大地提高了锁的效率.
Futex 具有以下两种类型:
普通 futex:是最常见的一种类型,通过等待互斥量来实现同步。在这种情况下,内核会将 futex 值存储在互斥体中,如果互斥体当前没有被占用,线程可以在 nsec 的时间范围内从 sys_futex() 系统调用中返回,否则它会悬挂在进程的 futex 队列上,在互斥量上睡眠,直到互斥量被释放。
内部 futex:内部 futex 是进程内部使用的,它可以轻松地在不同的线程之间传递状态信息。
**5、cpu 单核 多核 futex实现机制 **
Futex 是一种适用于多线程编程的轻量级锁机制,其中包括用户态(用户空间)和内核态(内核空间)协同工作的机制。在单核处理器上,Futex 可以使用下面这样的伪代码来实现:
while (atomic_compare_exchange(futex_value, expected_value, new_value) != expected_value) {sched_yield(); // 这里使用 sched_yield() 函数是为了让出当前线程的 CPU 时间片,以便其他线程有机会执行。 }
具体而言,线程在尝试获取锁时,会检查 futex 值。如果它等于期望值,则将 futex 值设置为新值,并返回 True。否则,该线程会重复上述步骤,直到成功获取锁位置。
在多核处理器上,由于并发性,使用自旋锁可能导致线程在内核调度程序上浪费大量 CPU 时间,这些时间可能会用于更有用的任务。为了避免浪费 CPU 时间,Futex 在多核处理器中采用 wait-wake 机制,即线程在获取 futex 值时会被阻塞,直到成为唤醒的另一个线程或者 futex 计时器超时。在 wait-wake 机制中,Futex 的核心代码是基于系统级函数 futex() 来实现的,可以利用这个函数来阻塞和唤醒线程,从而实现锁机制。
总之,Futex 机制是一种适用于多线程编程的轻量级锁机制,它可以在单核处理器和多核处理器上使用,通过 wait-wake 机制来避免浪费 CPU 时间,从而提高程序的性能和并发处理能力。
6、总结
synchronized也是一种内存锁
四、内存锁与总线锁
我们在java及操作系统见到的锁都是内存锁,总线锁锁粒度太大为了性能考虑很少见到其实现。
- 内存锁
内存锁是指对内存中特定地址进行锁定,以保障数据的原子性访问。当一个核心访问内存时,如果使用了内存锁指令,则其他核心会被阻塞,直到该核心访问完成,才会解除锁定。内存锁操作速度较快,因为它只涉及到锁住的内存地址,不会造成多处理器之间总线的竞争。但是,如果频繁地使用内存锁,会导致性能下降。常用的内存锁操作有 XCHG、ADD 和 XOR 等。
- 总线锁
总线锁是指对总线进行锁定,以保障某些操作的原子性。当一个核心访问总线上锁定的内存地址时,总线会被阻塞,直到该核心访问完成。这种锁定方式适用于需要锁住大块内存时,但会造成多个处理器之间总线的竞争,因此速度较慢。
在使用多核处理器时,内存锁和总线锁都可以用于保障数据的原子性访问。在实际应用中,当需要保障小块内存访问原子性时,应使用内存锁,而当需要保障大块内存访问原子性时,应使用总线锁。
五、堆栈异常
堆异常报错信息为: java.lang.OutOfMemoryError + xxx
- Java heap space
Java heap space溢出错误是最常见的一种OutOfMemoryError异常。这通常发生在Java程序试图分配的对象太多或对象太大时。
设置堆空间 -Xmx和-Xms
- Metaspace
Metaspace溢出错误发生在JVM内存区域中的方法区(Metaspace)中,通常是因为动态创建的类太多或采用了不合适的动态类加载器。
- PermGen space
PermGen space溢出错误发生在JVM内存区域中的永久代(PermGen space)中,通常是由于过多的类或类加载器未释放导致的。下面是解决办法。
1. 增加永久代空间 可以通过设置-XX:MaxPermSize和-XX:PermSize参数来增加永久代的大小,例如:-XX:PermSize=256m -XX:MaxPermSize=512m2. 清理无用的类和对象 在程序运行过程中,可能会同时存在大量的无用类和对象,需要及时清理它们。可以通过Java虚拟机提供的工具如jmap等来查看当前Java heap中的对象占用,并尝试清理无用的对象。3. 避免重复加载类 如果Java虚拟机重复加载同一个类,会占用大量的永久代内存空间。可以考虑使用类加载器缓存机制来避免重复加载同一个类。4. 升级JVM版本 在一些老的JVM版本中,PermGen空间存在一些内存泄漏的问题,可能会导致PermGen space溢出错误。升级JVM到最新版本,可以避免这些问题。
- Requested array size exceeds VM limit
当Java程序试图分配超出JVM限制的数组时,将抛出此类型的OutOfMemoryError异常。
- GC overhead limit exceeded
当Java虚拟机花费大量时间来执行垃圾收集,而最终可以回收的内存非常小且无法满足程序需求时,将抛出此类型的OutOfMemoryError异常。
- unable to create new native thread
当Java虚拟机无法为新线程分配足够的内存时,将抛出此异常。
- Direct buffer memory
当Java程序尝试分配NIO Direct Buffer却无法分配足够的内存时,将抛出此异常。
- unable to create new native thread
当Java虚拟机无法为新线程分配足够的内存时,将抛出此异常。
//解决办法 1. 增加最大线程数 可以使用命令ulimit -u来查看系统允许的最大进程数,默认情况下是1024。如果需要增加线程数,可以修改/etc/security/limits.conf文件. 例如,增加最大线程数到20000: * soft nproc 20000 * hard nproc 200002. 优化代码 过多创建线程会导致操作系统和Java虚拟机的负载增加,可以考虑优化代码逻辑。避免使用过于复杂的算法,避免使用无限循环等。3. 使用线程池 可以使用线程池来管理线程,避免无限制创建线程。4. 升级硬件 如果硬件资源过于紧张,可以考虑升级硬件,提升系统性能。5. 调整JVM参数 可以通过设置JVM参数,例如-Xss,来调整线程栈大小,从而减少每个线程占用的资源。另外,可以通过-Xmx和-Xms参数来增加堆内存空间,从而减少系统IO频繁发生。
- StackOverflowError
当方法调用层级过多导致调用栈溢出时,将抛出此异常。
1、降低代码调用深度 2、修改 -Xss 值,这个值是设置过大会降低线程并发。
六、垃圾回收器设计思想
常见的垃圾回收器有Serial、Parallel、CMS、G1、ZGC、Shenandoah等,他们背后都有不同的设计思想。下面从垃圾回收器区域划分、按照代回收、是否是并发回收、对象标记算法、垃圾移动方式等多角度分析垃圾回收器思想。
1、垃圾移动方式
标记-清除:首先会对内存空间进行扫描,标记出所有被引用的对象,然后会对所有未标记的对象进行清除,清理掉垃圾对象。但是这种算法会产生内存碎片,可能导致无法分配大对象。 老年代垃圾回收器常用方案,会导致程序暂停
标记-整理:首先进行标记,然后将所有存活的对象压缩到内存的一端,将压缩后的空间归还给空闲区域,从而解决了内存碎片的问题。 老年代垃圾回收器常用方案,会导致程序暂停
标记-复制:将内存空间划分成两个区域,每次只使用其中一个区域存放对象,在垃圾回收时,扫描其中一个区域,将存活对象复制到另一个区域,然后将原来的区域清空。这样可以避免内存碎片的问题,但是需要更多的内存空间。青年代垃圾回收器常用方案,复制算法通常区域不一样不会导致程序暂停
2、垃圾回收思想
分区垃圾收集:将堆内存划分为一些相互独立的区域,每个区域内部采用复制算法或标记-压缩算法对垃圾进行回收。这种算法减少了内存碎片,同时也减少了内存间的交互,提高了垃圾回收的效率。
分代垃圾收集:根据对象的使用模式和存活周期,将堆内存划分为不同的代(Young Generation,Old(Tenured)generation),对不同代采用不同的垃圾回收算法,如新生代采用复制算法,老年代采用标记-压缩算法等。
并发垃圾收集:在程序运行过程中,垃圾回收程序和应用程序同时运行,尽可能地减少程序暂停时间。这种算法可以通过多线程、增量标记等方式实现。
虚拟机统计学垃圾收集(简称G1):G1是一种分代垃圾回收的算法,由于它可以避免全局停顿,因此它被认为是解决大内存应用程序垃圾回收问题的关键技术。它将堆内存划分成多个区域,通过优先回收最多垃圾的区域,以达到最大化的垃圾回收效率。
3、JVM对象数据结构是图
在JVM中,对象之间的数据结构通过指针相互连接形成一个对象图。一个对象包括对象头和实例数据两部分,对象头中存储了一些元数据信息,比如对象的类型、GC信息等。而实例数据则根据不同的对象类型存储着不同的数据。
4、JVM对象标记算法
根搜索算法(Root算法):从程序中初始的一些根对象开始,遍历它们的引用关系,逐步标记出所有被引用的对象,最后不在根集中的所有对象都被标记为垃圾。根对象包括线程栈中的对象、静态变量等,是程序中所有可直接或间接访问到对象的起点。
采用Root算法的垃圾回收器: 1. CMS(Concurrent Mark Sweep)垃圾回收器:CMS垃圾回收器使用了多种Root算法,包括根扫描、Card Table和Remembered Set。2. G1(Garbage First)垃圾回收器:G1垃圾回收器将堆分成多个region,在初始化标记阶段使用了一种特殊的Root算法,称为SATB(Snapshot-At-The-Beginning)Root算法。3. Shenandoah垃圾回收器:Shenandoah垃圾回收器使用了一种Root算法,称为Global Heap Scan,其扫描所有存活对象的引用,将其标记为可达对象。4. ZGC(Z Garbage Collector)垃圾回收器:ZGC垃圾回收器在标记阶段使用了Root算法,称为Concurrent Refinement Roots。
染色算法(Tri-Color Marking):将对象的标记划分为白色、黑色和灰色三种状态。初始时所有对象都是白色,标记开始时将根对象标记为灰色,灰色对象被处理后会将其引用到的所有对象标记为灰色或黑色。一直重复这个过程,直到所有引用链上的对象都被标记为黑色,然后将全部白色对象回收,剩下的黑色对象则留给程序继续使用。
G1垃圾回收器最终标记算法。
七、JVM垃圾回收器回收流程
Serial、Parallel、CMS、G1、ZGC
类别 | 新生代 | 老年代 | 默认JDK版本 |
---|---|---|---|
Serial | Young Serial | Serial Old | JDK1.3及以前 |
Parallel | Parallel Scavenge | Parallel Old | JDK1.4及以前 |
CMS | 未实现 | CMS Old | JDK 1.5-JDK1.8 |
G1 | JDK9及以后版本 | ||
ZGC | JDK11及以后版本 |
1、Serial
Serial是一种串行垃圾回收器,它主要用于单线程环境下的Java虚拟机,并且适用于小型的应用程序。它的内存回收机制分为两种:新生代和老年代的垃圾回收。Serial采用“复制”算法回收新生代内存,并采用标记-清除算法回收老年代内存。
新生代: 在Serial垃圾回收器中,新生代采用复制算法(Copying)实现内存回收。当新生代区域存活对象过多时,会触发Minor GC(小型GC)操作,这个操作会将新生代中的存活对象复制到新生代的另外一块未被使用的内存区域,新生代中的对象会根据年龄分配到不同的区域中,因为每个区域的大小是一致的,所以存活的对象在复制后,排列在区域的首部,使得区域的分配变得简单。
老年代: 回收空间则使用标记-清除(Mark-Sweep)算法回收。老年代中的对象的生命周期更加的长,所以它们往往会自然地排列在一起,同时标记过程会产生一个巨大的停顿时间,垃圾回收期间无法同时运行应用程序线程。
2、Parallel
新生代: Parallel Scavenge是一种并行垃圾回收器,主要用于新生代内存回收。Parallel Scavenge GC使用多个线程并行地处理 Java 堆中的对象,在回收新生代时,它会将整个新生代进行分割,并且每一部分都维护一个 Eden 区和两个 Survivor 区,在回收时采用了复制算法(Copying)。
老年代: Parallel Old是一种并行垃圾回收器,主要用于老年代内存回收。Parallel Old GC 的回收速度快,对于大型程序具有良好的适应性,能够有效地并行处理大的内存区域。与 Parallel Scavenge GC 一样,Parallel Old GC 也是采用多个线程并行进行垃圾回收的,它采用标记-整理算法(Mark-Compact Algorithm)来回收老年代内存。
需要特别注意的是,Parallel Scavenge GC 和 Parallel Old GC有不同的设计目标和优先级,Parallel Scavenge GC比较注重吞吐量,Parallel Old GC则比较注重减少停顿时间,因此,在使用这两种垃圾回收器时,需要根据应用程序的实际情况选择合适的回收器来提高应用程序的性能和吞吐量。
3、Cms
CMS(Concurrent Mark and Sweep)是 JVM 中的一种并行垃圾回收器,主要用于回收老年代内存。它采用标记-清除算法(Mark-Sweep Algorithm)来进行回收,同时还支持多线程并发的清扫(Sweep),以提高回收效率。
CMS 回收老年代内存的流程如下:
1.初始标记:暂停所有应用线程,直接扫描 GC roots(如静态变量),标记与之直接关联的对象,这个过程通常只需几十毫秒,属于 STW 策略。
2.并发标记:GC 并不直接干预应用程序的执行,而是和应用程序一起并发执行。并发标记是标记的主要阶段,其中启用了多个线程用于同时扫描对象图,并标记未被标记的对象。
3.重新标记:在并发标记的期间,可能会有新的对象被标记,这个时候就需要进行重新标记。随着并发标记的进行,与 GC roots 相连的对象的向外的引用一定会经历四种状态:未标记、标记、变更和重新标记。在重新标记时会统计发生变更,修改指向信息,保证统计的准确性。
4.并发清理:由于并发标记的存在,清理阶段无法做到像 STW 一样将所有线程停顿下来,只能在程序并发执行的同时清理不再使用的对象。
需要注意的是,CMS 回收机制通常需要一定的 CPU 运算能力和内存碎片保持良好的垃圾回收效果。它在减少 GC 时间方面做得好,但并发执行对资源的占用较高,在内存比较小的环境中可能导致频繁的 Full GC。
4、G1
????
4、ZGC
八、JVM对象回器参数
1、Serial参数
-XX:+UseSerialGC:启用串行垃圾回收器。
-XX:-UseParNewGC: 不使用年轻代并行垃圾收集器,年轻代会使用串行垃圾回收器
-XX:MaxTenuringThreshold:晋升老年代的年龄阈值,超过这个值就晋升。
-XX:TargetSurvivorRatio:设定Eden区与Survivor区的比例(实际上是2个Survivor去的占用比例),默认值为50。
-XX:SurvivorRatio:按Size 单位(字节)设定eden空间与survivor空间的比值;默认值为8。
-XX:NewSize:设置年轻代初始大小。
-XX:MaxNewSize:设置年轻代最大值。
2、Parallel参数
-XX:+UseParallelGC:启用并行垃圾回收器。
-XX:+UseParallelOldGC:使用并行垃圾收集器来收集年老代。
-XX:ParallelGCThreads:设置并行收集器的垃圾收集线程数目。
-XX:+UseAdaptiveSizePolicy:启用自适应调节策略。
-XX:AdaptiveSizeThroughputPolicy:垃圾收集和应用程序时间比例,默认为99%的时间用于垃圾收集。
-XX:MaxGCPauseMillis:最大垃圾收集时间,默认值为200毫秒。
3、CMS参数
-XX:+UseConcMarkSweepGC:启用CMS垃圾回收器。
-XX:+UseParNewGC:与CMS一起使用,启用并行的年轻代垃圾收集器。
-XX:ParallelCMSThreads:并行处理垃圾回收和压缩时的线程数目。
-XX:CMSInitiatingOccupancyFraction:开始CMS收集的占用率阈值,默认为68%。
-XX:+UseCMSInitiatingOccupancyOnly:只使用CMS的占用率阈值,关闭Young GC时的使用ParNew的机制。
-XX:+CMSClassUnloadingEnabled:启用CMS时,允许对永久代(class等)的回收。
4、G1参数
-XX:+UseG1GC: 使用G1收集器
-XX:ParallelGCThreads: 指定GC工作的线程数量
-XX:G1HeapRegionSize: 指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis: 目标暂停时间(默认200ms)
-XX:G1NewSizePercent: 新生代内存初始空间(默认整堆5%,值配置整数,默认就是百分比)
-XX:G1MaxNewSizePercent: 新生代内存最大空间
-XX:TargetSurvivorRatio: Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold: 最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent: 老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了
-XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。
5、ZGC参数
- 启用 ZGC:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
- 堆大小设置:
-XX:ZHeapSize=n,设置堆大小为n,单位为M,G或T。
- 并发垃圾回收器的并发线程数:
-XX:ConcGCThreads=n,其中n为并发垃圾回收时使用的线程数。默认值是cpu核心数的1/4,或2,取其中更小的值。
- 设置不可回收内存阈值:
-XX:ConcUncommit=n,其中n是指定的阈值。由于 ZGC使用内存映射,因此堆内存上的大部分页都是不可回收的。默认值为1GB,即1000M。
- 操作系统的内存大小:
-XX:ZPhysicalMemorySize=n,其中n是指定的物理内存大小。堆大小不能超过该值。可以使用K、M和G做后缀。
- 关闭透明大页:
-XX:-UseTransparentHugePages,禁用透明大页。默认情况下,ZGC启用透明大页。
- 关闭内存映射文件:
-XX:-UseMappedFile,关闭内存映射文件。默认情况下,ZGC启用内存映射文件。
- 策略参数:
-ZPauseTarget=,,指定暂停时间的目标和容差。默认情况下,目标暂停时间为10ms;容差取5%。
-XX:ZUncommitDelay=m,用于控制ZGC何时释放不再使用的虚拟内存页,其中m是时间单位(毫秒)。默认值为5秒。
-XX:SoftMaxHeapSize=,指定此JVM运行的ZGC的软最大堆大小,该值会极大的限制垃圾回收暂停的时间。默认值是硬最大堆大小的一半,软确保堆大小是一定不会超过这个值。默认情况下,这个值会和–Xmx配对使用。
九、JVM调优
1、调整堆内存大小
堆内存是JVM运行所必需的内存池。如果堆内存太小,可能会导致OutOfMemoryError异常或频繁的垃圾回收。一般建议将堆内存分配为系统可用内存的1/4到1/2之间。
-Xms:设置JVM堆的初始大小。
-Xmx:设置JVM堆的最大大小。
-XX:NewSize=:设置JVM新生代的大小。
-XX:MaxNewSize=:设置JVM新生代的最大大小。
2、垃圾收集器种类选择
JVM中有多种不同类型的垃圾回收器可供选择。Java 8中,可用的垃圾回收器包括Serial、Parallel、CMS、G1和ZGC,各有不同的优缺点。
-XX:+UseSerialGC:使用串行垃圾回收器。
-XX:+UseParallelGC:使用并行垃圾回收器。
-XX:+UseConcMarkSweepGC:使用CMS垃圾回收器。
-XX:+UseG1GC:使用G1垃圾回收器。
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC :使用实验性的ZGC垃圾回收器(Java 11或更高版本)。
3、新生代老年代大小比例
-XX:NewRatio=:设置JVM年轻代和老年代的大小比例,默认值为2.
4、新生代Eden、Survivor空间大小比例
-XX:SurvivorRatio=:设置JVM新生代中Eden空间和Survivor空间的大小比例,默认值为8.
5、晋升到老年代的对象年龄
-XX:MaxTenuringThreshold=:设置晋升到老年代的对象年龄,默认值为15
6、启用自适应堆大小策略
-XX:+UseAdaptiveSizePolicy:启用自适应堆大小策略。可以最大的利用系统资源及降低垃圾回收时间,风险可能导致堆不稳定,带来未知问题。
7、垃圾回收器线程暂停时间
-XX:GCTimeRatio:时间比率,垃圾回收时间与应用程序执行时间的比值,默认值为99.
-XX:MaxGCPauseMillis=:设置最大的垃圾回收暂停时间,单位为毫秒,默认值为200毫秒。
-XX:ParallelGCThreads=:设置并行垃圾回收器线程数,默认值为CPU核心数。
8、非堆内存的调整
除了堆内存之外,JVM还需要一些叫做非堆内存的内存池来存储不同的数据,如线程栈、Class对象和元数据。也可以调整这些内存池的大小和其他设置。
以下是一些常用的非堆内存设置参数:
-XX:PermSize=:设置永久区大小(Java 7或更早版本)。
-XX:MaxPermSize=:设置永久区最大大小(Java 7或更早版本)。
-XX:MetaspaceSize=:设置元空间大小(Java 8及以上版本)。
-XX:MaxMetaspaceSize=:设置元空间最大大小(Java 8及以上版本)。
-XX:ThreadStackSize=:设置线程堆栈大小。
9、线程调整
线程是JVM中的基本执行单元。可以调整线程池大小等线程相关的设置来优化JVM的性能。
-XX:ThreadPriorityPolicy=:线程优先级策略。将policy设置为0表示使用操作系统默认策略,将其设置为1表示JVM优先级,将其设置为2表示使用混合策略。
-XX:ParallelGCThreads=:设置并行垃圾回收器线程数,默认值为CPU核心数。
-XX:ConcGCThreads=:设置并发垃圾回收器线程数,默认值为CPU核心数的1/4。
-XX:+UseSpinning:使用线程自旋提高锁定的性能,适用于较低的并发性应用程序。
十、类加载
1、虚拟机视角什么是对象 2、类如何加载 3、双亲委派模式? 4、类冲突的问题? 5、java反射原理? 6、动态代理原理
1、Java 的类加载流程?
加载
类加载的第一步是将 class 文件从磁盘或网络等外部存储器加载到 JVM 内存中。在加载时,会将 class 文件分成常量池、类变量、实例变量、构造方法等多个不同的组成部分,并在内存中开辟相应的内存空间用于存储。
链接
当类加载器将 class 文件加载到虚拟机内存中后,虚拟机必须对这些内容进行链接,以便能够执行程序。包括如下三个部分:
验证
验证是类加载的第一个阶段,主要是对被加载的 class 文件进行格式验证、语义验证和字节码验证等一系列的验证操作。主要是为了检测被加载的 class 文件是否符合 JVM 规范,并保证代码的正确性和安全性。
准备
准备是类加载的第二个阶段,它的目的是为类的静态变量分配内存,并设置默认初始值。在准备阶段中,虚拟机为类中定义的所有静态变量分配内存,并将其初始化为默认初始值(零值)。例如,对于 int 类型的静态变量,初始值为 0,对于引用类型的静态变量,则初始值为 null。
解析
解析是类加载的第三个阶段,主要是将符号引用解析为直接引用。在 Java 中,如果一个类中使用了另外一个类的变量或方法时,编译器会生成符号引用(symbolic reference),用于在运行期间定位该变量或方法。JVM 在解析阶段中,将这些符号引用转换成直接引用(direct reference),即指向具体内存位置的引用。
初始化
在链接阶段中,当所有的静态成员变量都被分配了内存和初始值后,虚拟机才会进入初始化阶段。在初始化阶段,虚拟机会执行类的初始化方法,也就是执行类的 clinit() 方法。clinit() 方法中的代码是由编译器自动收集的静态变量赋值语句和静态代码块中的语句合并产生的,顺序由语句在源文件中出现的顺序决定。
2、Java 的类加载机制是什么?
Java 的类加载机制是指将 class 文件加载到 JVM 中,并在运行时动态地生成 Java 类的过程。Java 类加载机制是 JVM(Java Virtual Machine)运行时的基础,是实现动态语言特性和运行期类型识别等重要特性的核心。
3、类的加载过程中,发生了什么?
类的加载过程主要包括加载、链接和初始化三个步骤。加载指从磁盘或网络中读取类的二进制数据,并将其转换为 JVM 内部的数据结构存储在内存中;链接指的是验证、准备和解析三个过程,其中验证是检查类的二进制文件是否符合 JVM 规范,准备是为静态变量分配存储空间并设置默认初始值,解析是将符号引用替换为直接引用,以便 JVM 在后面的运行中找到正确的内存地址;最后初始化阶段是执行该类中的初始化方法,包括执行静态变量初始化和静态代码块。
4、Java 类加载器有哪些?
Java 类加载器主要分为以下三类:Bootstrap ClassLoader(启动类加载器)、Extension ClassLoader(扩展类加载器)和 System ClassLoader(应用类加载器)。其中,启动类加载器是 JVM 的一部分,它负责加载核心类库,扩展类加载器和应用类加载器则负责加载用户自定义类。
5、类加载器的双亲委派机制是什么?
双亲委派机制是指当一个类加载器加载类时,会先将这个类的加载请求委托给父类加载器进行处理,如果父类加载器还存在父类加载器,那么请求会依次向上委托,直到委托到最顶层的启动类加载器或者父类加载器能够找到所请求的类为止。双亲委派机制保证了每个类只会被加载一次,并且由上层加载器加载的类能够被下层加载器访问。
6、如何打破类加载器的双亲委派机制?
1、继承ClassLoader 类并覆盖 findClass方法打破
可以通过自定义类加载器来打破类加载器的双亲委派机制。可以继承 ClassLoader 类并覆盖 findClass 方法,并在该方法中自己实现类的加载逻辑。在处理类加载请求时,可以先委派给父类加载器进行处理,如果父类加载器无法加载,则在当前类加载器中查找是否有该类,并将其加载到 JVM 中。
//承ClassLoader 类并覆盖 findClass方法打破双亲委托 public class CustomClassLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 从特定的位置读取 class 文件,并将对应的字节码转换为 Class 对象// TODOreturn null;}public static void main(String[] args) throws Exception {CustomClassLoader loader = new CustomClassLoader();Class<?> clazz = loader.loadClass("com.example.Test");Object obj = clazz.newInstance();Method method = clazz.getMethod("execute");Object result = method.invoke(obj);System.out.println(result);} }
2、定义线程ClassLoader方式打破
在 Java 中,每个线程都有一个上下文类加载器(Context ClassLoader),可以通过 Thread.getContextClassLoader() 方法获取。上下文类加载器是一种与双亲委托模型无关的类加载器,它可以用来打破双亲委托模型,从而使得某些类可以使用特定的类加载器进行加载,而不受默认的双亲委托规则的限制。 通过 Thread.currentThread().setContextClassLoader() 方法可以将上下文类加载器设置为指定的类加载器。这样,在使用当前线程加载类时,JVM会优先使用上下文类加载器进行加载,而不是使用默认的双亲委托模型。这种方式可以被用来打破默认的双亲委托模型。
//定义线程ClassLoader方式打破双清委托 public class CustomClassLoader extends ClassLoader {@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {// 判断是否使用自定义加载器加载指定类if ("com.example.Test".equals(name)) {return findClass(name);}// 使用默认的双亲委托机制return super.loadClass(name);}@Overridepublic Class<?> findClass(String name) throws ClassNotFoundException {// 在自定义的范围内查找指定类if ("com.example.Test".equals(name)) {// TODO: 从特定的位置加载字节码并转换为Class对象return null;}// 如果找不到就返回null,JVM会使用默认的类加载机制加载return null;}public static void main(String[] args) {CustomClassLoader customClassLoader = new CustomClassLoader();// 设置自定义类加载器为上下文类加载器Thread.currentThread().setContextClassLoader(customClassLoader);// 使用自定义类加载器加载指定类try {Class<?> clazz = Class.forName("com.example.Test");Object obj = clazz.newInstance();Method method = clazz.getMethod("execute");Object result = method.invoke(obj);System.out.println(result);} catch (Exception e) {e.printStackTrace();}} }
7、类加载器的作用是什么?
类加载器的主要作用是根据类的名称查找并加载类的字节码到 JVM 中,并将其转换为可以被 JVM 所识别的类对象。在某些情况下,也可以使用类加载器加载其他资源文件,如配置文件等。
8、类加载器都有哪些常见的使用场景?
类加载器常见的使用场景包括:热部署(Hot Deployment)、动态代理、OSGi 等。其中,热部署指的是在系统运行过程中动态地更新类文件,并且让 JVM 实时加载新的类文件;动态代理则通常是使用 Javassist、asm 等工具动态生成字节码,然后创建代理类;而 OSGi 是一种用来管理 Java 应用程序结构的平台。
9、类加载器的双亲委派模型有哪些优点?
双亲委派模型的主要优点有:避免类的重复加载、保证了类的安全性、提高了代码的稳定性和可靠性、保证了类库的相互兼容性。双亲委派模型避免了因为重复加载导致的类冲突;能够保证系统的安全性,防止恶意攻击;提高了代码的稳定性和可靠性,保证应用程序的运行效率;同时,保证了类库的相互兼容性,使得不同版本的类库可以在同一个 JVM 中共存。
10、如何实现自定义类加载器?
实现自定义类加载器,需要继承 java.lang.ClassLoader 并重写 findClass() 方法。具体的步骤为:首先判断该类是否已经被加载;如果没有被加载,则委派父加载器加载,如果父加载器无法加载,则使用自定义加载器加载;最后将该类字节码转化为 Class 对象并返回。下面是一个简单的样例代码:
public class CustomClassLoader extends ClassLoader {private String classPath;public CustomClassLoader(String classPath) {this.classPath = classPath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] data = loadClassData(name);return defineClass(name, data, 0, data.length);}private byte[] loadClassData(String className) {// 读取指定的 class 文件,并返回字节数组// TODOreturn null;} }
11、类加载机制和反射的关系是什么?
类加载机制和反射是 Java 语言中两个重要的基础特性。类加载机制是指在 JVM 中将 class 文件加载到内存中,并生成 Java 类的过程,反射则是指将类的方法与属性等信息在程序运行阶段动态地获取和使用的能力。反射可以很好地支持类的动态加载和运行时扩展,是实现动态代理、依赖注入等其他技术的基础。而类加载则是反射的前提条件,只有先将 class 文件加载到 JVM 中,才能对其进行反射操作。
码的稳定性和可靠性,保证应用程序的运行效率;同时,保证了类库的相互兼容性,使得不同版本的类库可以在同一个 JVM 中共存。
10、如何实现自定义类加载器?
实现自定义类加载器,需要继承 java.lang.ClassLoader 并重写 findClass() 方法。具体的步骤为:首先判断该类是否已经被加载;如果没有被加载,则委派父加载器加载,如果父加载器无法加载,则使用自定义加载器加载;最后将该类字节码转化为 Class 对象并返回。下面是一个简单的样例代码:
public class CustomClassLoader extends ClassLoader {private String classPath;public CustomClassLoader(String classPath) {this.classPath = classPath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] data = loadClassData(name);return defineClass(name, data, 0, data.length);}private byte[] loadClassData(String className) {// 读取指定的 class 文件,并返回字节数组// TODOreturn null;} }
11、类加载机制和反射的关系是什么?
类加载机制和反射是 Java 语言中两个重要的基础特性。类加载机制是指在 JVM 中将 class 文件加载到内存中,并生成 Java 类的过程,反射则是指将类的方法与属性等信息在程序运行阶段动态地获取和使用的能力。反射可以很好地支持类的动态加载和运行时扩展,是实现动态代理、依赖注入等其他技术的基础。而类加载则是反射的前提条件,只有先将 class 文件加载到 JVM 中,才能对其进行反射操作。