Java常见问题总结二

本文深入探讨Java中的阻塞队列、ReentrantLock、GCroots、JVM参数、引用类型及OOM常见问题,详解各类锁机制、垃圾回收机制与内存管理策略,助您掌握Java并发与内存优化的核心技巧。
摘要由CSDN通过智能技术生成

1、阻塞队列

是一个队列,当阻塞队列是空的时候从队列中取元素的操作会被阻塞,当队列是满的时候从队列中放元素就会被阻塞,共有7种阻塞队列。

ArrayBlockingQueue:由数组组成的有界阻塞队列
LinkedBlockingQueue:由链表组成的有界阻塞队列,默认大小是Integer.max_value。
PriorityBlockingQueue:按照优先级的无界阻塞队列
DelayQueue:按照优先级实现的延迟的无界阻塞队列
SynchronousQueue:不存储元素的阻塞队列,即单个元素的阻塞队列
LinkedTransferQueue:由链表组成的无界阻塞队列
LinkedBlockingDeque:由链表组成的双向阻塞队列

阻塞队列的使用

public static void main(String[] args) throws InterruptedException {
    BlockingQueue<String> queue = new ArrayBlockingQueue<>(3); // 创建一个阻塞队列

    /*第一种方式**/
    queue.add("a");  // 添加元素,溢出报错 
    System.out.println(queue.element());  // 获取队首元素
    queue.remove("a");// 删除元素

    /*第二种方式**/
    queue.offer("a");  // 添加元素,溢出返回false
    queue.offer("a",2L,TimeUnit.SECONDS); // 可以添加时间
    System.out.println(queue.peek());  // 获取队首元素
    queue.poll();  // 删除元素

    /*第三种方式**/
    queue.put("a");  //添加元素,当队列满的时候就阻塞
    queue.take();    //移除元素,当队列为空就阻塞
}

2、ReentrantLock

使用ReentrantLock 可以做到更加细粒度的加锁,同时也是一个可重入的锁。

ReentrantLock lock = new ReentrantLock();
lock.lock();      // 加锁
lock.unlock();    // 解锁

Condition:可以做到精准唤醒某个线程。

package exc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

class ShareSource{
    private int number = 1;
    private ReentrantLock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();  // 创建一个condition
    private Condition c2 = lock.newCondition();

    public void print5() throws InterruptedException {
        lock.lock();
        while (number != 1){
            c1.await();  // c1进行阻塞
        }
        System.out.println("111");
        number = 5;
        // 精准唤醒c2,c2.singnalAll()就是唤醒全部
        c2.signal();
        lock.unlock();
    }
    public void print15() throws InterruptedException {
        lock.lock();
        while (number != 5){
            c2.await();
        }
        System.out.println("222");
        number = 1;
        // 唤醒c1
        c1.signal();
        lock.unlock();
    }
}
public class Sync {
    public static void main(String[] args) {
        ShareSource s = new ShareSource();
        new Thread(() -> {
            for(int i=0;i<=4;i++){
                try {
                    s.print5();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"aa").start();
        new Thread(() -> {
            for(int i=0;i<=4;i++){
                try {
                    s.print15();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"bb").start();
    }
}

阻塞、唤醒 wait和notify也可以做到,在没有 synchronized 关键字修饰下会报错,wait在阻塞的时候会释放锁对象。

※:LockSupport,其优势在于不用使用加锁就可以阻塞线程,并且可以先唤醒后阻塞,对先后顺序没有太多的要求。

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
        System.out.println(Thread.currentThread()+ "come in");
        LockSupport.park(); // 阻塞
        System.out.println(Thread.currentThread()+ "wake");
    }, "a");
    thread.start();

    Thread b = new Thread(() -> {
        LockSupport.unpark(thread); // 唤醒,需要传入具体线程
        System.out.println(Thread.currentThread()+ "waking");
    }, "b");
    b.start();
}

LockSupport的底层调用的是unsafe类的native代码,LockSupport不能多次park,否则会阻塞。

LockSupport会对每个线程发放一个凭证,连续两次unpark和一次unpark的效果是一样的,只会增加一个凭证,当连续调用两次park时,就会阻塞,因为要消费两个证书,但是只有一个证书。

※ ReadWriteLock(读写锁)

多个读操作可以共享一把锁,写线程独占锁,读锁和写锁是互斥的。多个读线程之间不会相互阻塞;读线程和写线程之间会阻塞。

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();     // 读锁
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();  // 写锁

※ 锁降级策略:就是将锁的严苛程度降低。当同一个线程持有了写锁,在没有释放写锁的情况下还获得了读锁,这时写锁就降级为了读锁。如果先获取读锁在获取写锁,这样会卡住,是不能够重入的。锁降级提高了并发性。锁降级的好处就是保证在原有锁不被其他线程抢占的情况下还能让其他线程读到数据。

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
HashMap<String, String> map = new HashMap<>();

new Thread(() -> {
    writeLock.lock();
    map.put("1", "xxxx");
    readLock.lock();
    System.out.println("线程a读到的值----》" + map.get("1"));
    writeLock.unlock();   // 当写锁释放以后才会让其他线程读取到数据  相当于重入锁
    readLock.unlock();
}, "a").start();

new Thread(() -> {
    readLock.lock();
    System.out.println("线程b读到的值----》" + map.get("1"));
    readLock.unlock();
}, "b").start();

※ StampedLock(邮戳锁)

邮戳锁比读写锁的速度更快,但是不保证结果的正确性需要对结果进行校验。是一个乐观锁,当获取乐观读锁以后不会阻塞其他线程获取写锁。

StampedLock stampedLock = new StampedLock();

long readRecord = stampedLock.readLock();  // 创建读锁
stampedLock.unlockRead(readRecord);

long writeRecord = stampedLock.writeLock();  // 创建写锁
stampedLock.unlockRead(writeRecord);

long optRecord = stampedLock.tryOptimisticRead();   // 创建乐观读锁
Boolean result = stampedLock.validate(optimisticRead);  // 验证是否被改动,true代表被修改过

邮戳锁的读锁和写锁和ReentrantLock的读写锁一样。乐观读锁使用需要对结果进行校验,如果检验失败则需要升级为悲观读锁。

HashMap<String, String> map = new HashMap<>();

new Thread(() -> {
    long optimisticRead = stampedLock.tryOptimisticRead();
    String s = map.get("1");
    for (int i = 0; i < 3; i++) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("value--->" + stampedLock.validate(optimisticRead));
    }
    if(!stampedLock.validate(optimisticRead)){
        long readRecord = stampedLock.readLock();
        s = map.get("1");
        stampedLock.unlockRead(readRecord);
    }
    System.out.println("线程b读到的值----》" + s);
}, "b").start();

new Thread(() -> {
    long writeRecord = stampedLock.writeLock();
    map.put("1", "xxxx");
    stampedLock.unlockWrite(writeRecord);
}, "a").start();

3、GC roots

是一组必须活跃的引用,基本思路就是以GC roots引用对象作为起点进行通过引用关系遍历对象图,可以被遍历到的就为存活,否则就会判定为死亡。

哪些是GC对象?

虚拟机栈中引用的对象。
方法区中的类静态属性引用的对象。
方法区中的常量引用的对象。
本地方法栈(Native方法)中引用的对象。

4、JVM的参数

标配参数:

-version  查看版本号
-help  查看帮助

X参数:

-Xint  解释执行模式
-Xcomp 第一次使用就编译成本地代码
-Xmixed 混合模式

XX参数之布尔类型参数:

总结为一个公式模板:-XX:+/- 属性,+/-代表开启或关闭。

例:-XX:+PrintGCDetails

要想查看是否开启属性值就需要先用到Java相关的工具。

首先通过jps工具,在输入jps -l命令以后就会看到正在执行Java文件的线程编号。
然后再通过jstack工具,输入jstack 线程号 就可以进行解析查看是否是死锁死循环等问题。
最后还可以通过jinfo工具进行参数的查看,输入jinfo -flag 属性名称 线程号 来查看是否开启。

排查是否有死锁的位置

1. top -c  # 去查看CPU占用较高的进程ID
2. ps H -eo pid,tid,%cpu | grep 进程ID   # 查看当前线程下 CPU占用率高的线程
3. 将线程ID转换成16进制  
4. jstack 16进制线程ID                   # 方式一定位问题
5. jstack 进程ID|grep 16进制线程ID -A 20 # 方式二定位问题

XX参数之KV型参数:

总结为一个公式模板:-XX: 属性=值。

例:-XX:MaxTenuringThreshold=12

常用JVM参数

查看JVM配置的初始默认值:java -XX:+PrintFlagsInitial  [-version (具体参数)]

查看JVM配置的更新修改后的值:Java -XX:+PrintFlagsFinal [-version (具体参数)]
        --uintx CodeCacheExpansionSize = 65536(属于kv型)
        --bool ClassUnloading = true(属于布尔型)
        --ClassUnloading := true 代表修改以后的值

-Xms参数:初始大小内存,默认是物理内存的1/64 -Xms 10m等价于-XX:InitialHeapSize=10m

-Xmx参数:最大分配内存,默认为物理内存的1/4 -Xmx 10m等价于 -XX:MaxHeapSize=10m

-Xss参数:设置单个线程栈的大小,默认是512k--1024k -Xss 10m等价于-XX:ThreadStackSize=10m
        --当用jinfo查询ThreadStackSize时会出现值为0的情况,这是代表用的系统默认值

-Xmn参数:设置年轻代的大小

-XX:MetaspaceSize参数:设置元空间的大小。
        -- 元空间和永久代的区别:元空间并不在虚拟机中,而是使用本地内存,也就是说只受本地内存限制
        -- 然而还会出现OOM的异常是因为,元空间只是用了本地内存的一部分而不是全部

-XX:SurvivorRatio参数:设置新生代中Eden的空间比例。
        -- -XX:SurvivorRatio=8代表 Eden:s0:s1 = 8:1:1

-XX:NewRatio参数:配置年轻代和老年代在堆结构的占比。
        -- -XX:NewRatio=3代表 新生代:老年代=1:3

-XX:MaxTenuringThreshold参数:设置垃圾最大年龄。

GC垃圾回收日志

当添加jvm参数 -XX:PrintGCDetails 在触发GC的情况下会出现以下日志

[GC [DefNew: 2242K->0K(2368K), 0.0018814 secs] 2242K->2241K(7680K), 0.0019172 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

GC代表是哪一种类型的GC
DefNew: 2242K->0K(2368K)。2242k代表yongGC前新生代内存占用 0k代表yongGC后新生代的占用 (2368k)代表新生代总共大小
2242K->2241K(7680K) 2242k代表yongGC前JVM堆内存的占用,2241k代表yongGC后JVM堆的内存使用情况,(7680k)JVM堆的总大小

总结为一个公式:名称:GC前占用内存->GC后占用内存                                                  

5、Java中的引用(强软弱虚)

强引用:当内存不足时,JVM开始垃圾回收,对于强引用的对象,就算出现OOM的问题也不会进行对象回收,该对象以后永远不会使用JVM也不会回收,因此强引用是造成Java内存泄漏的主要原因之一。

HelloGc helloGc = new HelloGc();  这就是一个强引用

软引用:对于软引用对象来说当系统内存充足时不会进行回收,当内存不足时就会被回收。软引用通常在对内存敏感的程序中,内存够用就保留,不够用就回收。

Object o1 = new Object();
SoftReference<Object> softReference = new SoftReference<>(o1); // 创建一个软引用
System.out.println(softReference.get()); //获取引用

弱引用:只要一进行垃圾回收,不管JVM内存是否充足都会进行回收

Object o1 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(o1); // 弱引用
System.out.println(weakReference.get());  // 获取引用

System.gc();  // 由于此时还有o1这个强引用,所以并不会回收弱引用,没有任何强引用指向的时才会被回收

o1 = null;    // 将强引用置空

System.gc();  // 此时就能成功回收
System.out.println(weakReference.get());  // null

weakhashMap:并不是一个可并发操作的hashmap

WeakHashMap<Integer, String> hashMap = new WeakHashMap<>(); // 创建一个key为弱引用的hashmap
String value = "WeakHashMao";
Integer key = new Integer(1);
hashMap.put(key, value);
key = null; //将key置位null
System.gc(); //这里模拟一次GC过程,这时会进行回收
System.out.println(hashMap + "\t " + hashMap.size()); // 这里的hashmap中的元素为空,但是结果有可能不为0,这个是由于指令重排导致的

虚引用:顾名思义,形同虚设,如果一个对象仅持有虚引用,那么它将和没有任何引用一样,任何时候都有可能被回收,唯一的目的就是能够在这个对象被回收时收到一个系统通知需要配合引用队列使用。

Object o1 = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> reference = new PhantomReference<>(o1,queue); // 创建一个虚引用,并和引用队列进行绑定
System.out.println(reference.get()); // 虚引用始终为null

引用队列:是用来配合引用工作的,没有这个队列引用也可以正常工作,创建引用时可以指定引用队列,当GC释放对象时会将引用加入到引用队列。

Object o1 = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> weakReference = new WeakReference<>(o1,queue); // 将引用和队列进行绑定,当被GC以后就会放到定义好的队列中
System.out.println(queue.poll()); // 在GC之前就是null
o1 = null;
System.gc();
Thread.sleep(3000);
System.out.println(queue.poll());  // 获取引用队列里的元素

6、OOM常见问题

java.lang.StackOverflowError(栈溢出错误)
    -- 函数调用栈太深了,代码中是否有循环调用方法而无法退出的情况,例如递归操作。

java.lang.OutOfMemoryError:java heap space(堆溢出)
    -- 内存中加载数据量过大,或者强引用太多导致JVM无法进行回收

java.lang.OutOfMemoryError:GC overhead limit exceeded
    --当GC回收的时间太长的时候就会抛出这个异常,过长的定义就是当超过98%的时间全部来做GC,并且回收了
    --不到2%的内存极端条件下就会抛出

java.lang.OutOfMemoryError:Direct buffer memory(直接缓存溢出)
    --通过allocate进行内存分配是分配JVM内存堆,属于GC的管辖范围,拷贝的速度慢
    --通过allocateDirect,直接分配到本地的内存,不属于GC的范围所以拷贝速度快

java.lang.OutOfMemoryError:unable to create new native thread(创建线程到达一定数目)
    -- 当一个进程创建多个线程,超过了系统的承载量的极限
    -- 解决方法,降低应用程序的创建线程数量
    -- 对 vim /etc/security/limits.d/90-nproc.conf配置文件进行修改

java.lang.OutOfMemoryError:MetaSpace(元空间的溢出)
    --MetaSpace并不在虚拟机中存在,主要存放了常量池,静态变量,虚拟机加载的类信息,即时编译后的代码

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值