Java 基础笔记
变量赋值操作
++i和i++的区别?
++i是先将当前变量的值自增,再入栈做运算,而i++是先入栈再进行运算,自增和自减都是修改变量的值,不会改变入栈的值。赋值之前,临时结果也是存储在操作数栈中。
public static void main(String[] args) {
int i=1;
i=i++;
int j=i++;
System.out.println(i);
System.out.println(j);
int k=i+ ++i*i++;
System.out.println(k);
//结果2,1,11
}
Java单例模式
常见的两种形式:饿汉式、懒汉式
饿汉式
在类加载时,直接创建当前类的对象,不存在线程安全问题;包含一个私有构造方法,枚举类型也属于单例中的一种。
三种创建形式
package com.ycj.demos;
/**
* java的单例模式——饿汉式
*
* @author Administrator
*/
public class SingletonHungry {
/**
* 实例A:直接创建对象的单例模式
*/
static class SingletonA {
public static final SingletonA SINGLETON_A = new SingletonA();
private SingletonA() {
}
public String tt() {
return "aaaa";
}
}
/**
* 枚举类,比较简洁的一种饿汉式单例模式
*/
static enum SingletonEnum {
SINGLETON_ENUM_INSTANCE;
public String tt() {
return "1111";
}
}
/**
* 静态代码块初始化的饿汉式单例模式,逻辑上等同于SingletonA,都是在类加载的时候加载当前类的对象
* </li>
* 常用于初始化时读取配置文件的场景,比如读取property文件的内容给当前单例作为变量
*/
static class SingletonB {
public static final SingletonB SINGLETON_B_INSTANCE;
static {
//read property file
SINGLETON_B_INSTANCE = new SingletonB();
}
private SingletonB() {
}
public String tt() {
return "2222addd";
}
}
public static void main(String[] args) {
SingletonA singletonA = SingletonA.SINGLETON_A;
System.out.println(singletonA.tt());
SingletonEnum singletonEnumInstance = SingletonEnum.SINGLETON_ENUM_INSTANCE;
System.out.println(singletonEnumInstance.tt());
SingletonB instance = SingletonB.SINGLETON_B_INSTANCE;
System.out.println(instance.tt());
}
}
懒汉式
在需要是才创建对象,最简洁的是静态内部类的实现方式
package com.ycj.demos;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* java单例模式——懒汉式
*
* @author Administrator
*/
public class SingletonLazy {
/**
* 单线程下的线程不安全懒汉式单例模式
*/
static class SingletonA {
private static SingletonA INSTANCE;
public static SingletonA getInstance() {
if (INSTANCE == null) {
//
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("创建SingletonA对象");
INSTANCE = new SingletonA();
}
return INSTANCE;
}
private SingletonA() {
}
}
/**
* 线程安全懒汉式单例模式
*/
static class SingletonB {
private static SingletonB INSTANCE;
public static SingletonB getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (SingletonB.class) {
if (INSTANCE == null) {
//
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("类创建SingletonB对象");
INSTANCE = new SingletonB();
}
}
return INSTANCE;
}
private SingletonB() {
}
}
/**
* 利用静态内部类方式实现懒汉式单例(静态内部类的加载不会随着外部类的加载而加载),比SingletonB更简洁
*/
static class SingletonC {
private SingletonC() {
}
static class Inner {
private static final SingletonC SINGLETON_C = new SingletonC();
}
public static SingletonC getInstance() {
return Inner.SINGLETON_C;
}
}
public static void main(String[] args) throws Exception {
//多线程下测试,会实例化多个不同的实例,
testAThread();
//多线程场景下测试
testBThread();
//多线程场景下测试
testCThread();
//
// SingletonA instance = SingletonA.getInstance();
// SingletonA instance2 = SingletonA.getInstance();
// System.out.println(instance);
// System.out.println(instance2);
// System.out.println(instance.equals(instance2));
}
/**
* 多线程下测试,会实例化多个不同的实例,
*
* @throws InterruptedException
* @throws java.util.concurrent.ExecutionException
*/
private static void testAThread() throws InterruptedException, java.util.concurrent.ExecutionException {
Callable<SingletonA> callable = new Callable<SingletonA>() {
public SingletonA call() {
return SingletonA.getInstance();
}
};
ExecutorService executorService = Executors.newFixedThreadPool(3);
Future<SingletonA> submit = executorService.submit(callable);
Future<SingletonA> submit1 = executorService.submit(callable);
SingletonA singletonA = submit.get();
SingletonA singletonA1 = submit1.get();
System.out.println("-------------------------------");
System.out.println(singletonA);
System.out.println(singletonA1);
System.out.println(singletonA.equals(singletonA1));
executorService.shutdown();
/*打印出:
创建SingletonA对象
创建SingletonA对象
com.ycj.demos.SingletonLazy$SingletonA@6aceb1a5
com.ycj.demos.SingletonLazy$SingletonA@2d6d8735
false
*/
}
/**
* 多线程下测试2,
*
* @throws InterruptedException
* @throws java.util.concurrent.ExecutionException
*/
private static void testBThread() throws InterruptedException, java.util.concurrent.ExecutionException {
Callable<SingletonB> callable = new Callable<SingletonB>() {
public SingletonB call() {
return SingletonB.getInstance();
}
};
ExecutorService executorService = Executors.newFixedThreadPool(3);
Future<SingletonB> submit = executorService.submit(callable);
Future<SingletonB> submit1 = executorService.submit(callable);
SingletonB singletonB = submit.get();
SingletonB singletonB1 = submit1.get();
System.out.println("-------------------------------");
System.out.println(singletonB);
System.out.println(singletonB1);
System.out.println(singletonB.equals(singletonB1));
executorService.shutdown();
/*
打印出:
类创建SingletonB对象
com.ycj.demos.SingletonLazy$SingletonB@2471cca7
com.ycj.demos.SingletonLazy$SingletonB@2471cca7
true
*/
}
/**
* 多线程下测试2,
*
* @throws InterruptedException
* @throws java.util.concurrent.ExecutionException
*/
private static void testCThread() throws InterruptedException, java.util.concurrent.ExecutionException {
Callable<SingletonC> callable = new Callable<SingletonC>() {
public SingletonC call() {
return SingletonC.getInstance();
}
};
ExecutorService executorService = Executors.newFixedThreadPool(3);
Future<SingletonC> submit = executorService.submit(callable);
Future<SingletonC> submit1 = executorService.submit(callable);
SingletonC singletonC = submit.get();
SingletonC singletonC1 = submit1.get();
System.out.println("-------------------------------");
System.out.println(singletonC);
System.out.println(singletonC1);
System.out.println(singletonC.equals(singletonC1));
executorService.shutdown();
/*
打印出:
com.ycj.demos.SingletonLazy$SingletonC@d70c109
com.ycj.demos.SingletonLazy$SingletonC@d70c109
true
*/
}
}
谈谈你对volatile的理解
volatile是java虚拟机提供的轻量级的同步机制,实现了禁止指令重排序有优化,避免了多线程环境下程序出现乱序执行的现象;它可以保证内存可见性和禁止指令重排序,但是不保证原子性。synchronized才能保证原子性。
JMM是什么?JMM与JVM区别
JVM是java虚拟机,JMM是java内存模型;JMM(Java memory model)是一种抽象概念,并不真真实存在,它描述的是一组规则或者规范,通这组规范定义了程序中各个变量的访问规则;JMM模型有以下三大特性
1. 可见性
当多个线程访问主内存的同一个变量时,其中一个线程复制变量到工作线程且修改了变量的值,当前把值刷会主存储之后,其他的线程能马上感知到主存储的变量更新,其他线程此时主动更新自己工作内存的值,这种机制就是内存的可见性。
-
原子性
原子性就是保证数据的完整、一致性;它是不可分割的,也就是某个线程正在执行业务时,中间不可以被加塞或者被分割,需要整体的同时完成或者同时失败;比如多个线程同时修改变量值时(i++操作),满足原子性的操作应该是拷贝变量到工作内存、修改值、更新到主内存、通知其他线程这四步要完整的执行,不能被中断和挂起,volatile不满足原子性也就是因为前面四步存在被中断和挂起的可能,导致修改数据丢失问题。第一种方式——加synchronize则可以满足原子性,第二种方式——使用AtomicInteger类实现累加,因为atomic满足CAS特性。
-
有序性
有序性是指计算机在执行程序时,为了提高性能,编译器和处理器会对指令做重新排序——指令重排,一般分为编译器优化的重排、指令并行的重排和内存系统的重排;在多线程情况下,指令重排可能会改变代码执行顺序,从而导致改变执行的结果,volatile关键字可以禁止指令重排
class TestDemo{
private int a=0;
private boolean flag=false;
public void initData(){
a=1;
flag=true;
}
public void printData(){
if(flag){
a=a+10;
sout("a的结果为:" + a);
}
}
psvm(String[] args)
//单线程执行时,指令重排对当前程序没影响
//多线程情况下指令重排导致initData方法出现先给flag赋值,再给a赋值,赋值操作不一定能保证一致性;
//当A线程执行initData方法时,同时B线程执行printData方法,
//线程B在打印的结果可能出现1、11和10
}
你在哪些地方使用过volatile?
- 构建单例工具类,通过volatile+synchronize方式创建单例工具类
什么是CAS?CAS的基本原理
CAS——compareAndSwap——比较且交换,它是一条CPU并发原语,它的功能是判断内存中的某个位置的值是否为预期值,如果是则更新为新的值,如果不是预期值则继续比较,直到主内存中的值和工作内存中的值已知为止。CAS并发原语体现在JAVA语言中就是UnSafe的各个方法,调用Unsafe类中的CAS方法,JVM会自动实现CAS汇编指令。这是一种完全依赖不硬件的功能,通过它实现了原子操作;由于CAS属于系统原语范畴,是有若干条指令组成的,用于完成某一个功能的一个过程,并且原语的执行必须是连续的,在执行的过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致问题(不存在线程安全问题)。
比如AtomicInteger的compareAndSet,AtomicInteger能保证原子性是因为在AtomicInteger里面使用到了UnSafe类,在更新值时通过compareAndSwap操作,先根据对象和变量内存偏移量查询主内存中变量的值,然后与工作内存中的值进行比较,与预期值一致则更新返回true,否则一直循环比较直到一致为止。
CAS的缺点
- 内存中变量与工作内存中的值进行比较是循环比较,可能存在长时间循环导致自旋循环增加CPU开销
- 只能同时保证多线程下一个共享变量的原子操作
- 会引发ABA问题
CAS——unsafe——CAS底层原理——ABA问题——原子引用更新——如何避免ABA问题
CAS的ABA问题
CAS算法实现的一个核心就是取出内存中的值与当下时刻的值进行比较并交换,那么在这个时间差内会导致数据的变化;比如线程1从内存中取出A值,线程2也取出A值,然后线程1更新A为B且写回主存储,然后线程1再次把主存储的B更新为A;此时线程2进行CAS操作时发现内存中的值仍然是A,然后线程2更新成功,虽然线程2的CAS操作成功了,但是并不代表这个过程就是没问题的。ABA问题可用增加版本号的方式解决,即每次操作内存中的数据时记录版本号,例如在java中可使用AtomicStampedeReference<T>类,操作时增加盘本号判断,如果版本号符合预期就更新,否是不更新
请编码写一个线程不安全的ArrayList案例,并给出线程安全解决方案
List<String> list=new ArrayList<>();
for(i=0;i<50;i++){
new Thread(new Runnable(){
list.add"aaaaa"+i);
}).start();
}
//1.可能会出现ConcurrentModificationException
//2.导致原因,一个线程在写入,另外一个线程进入争夺资源,导致数据不一致出现异常,类似于入场签到时,第一个人签到正在写名字还没写完,另外一个人强行抢走纸笔签名的场景
线程安全解决方案
- 用Vactor代替ArrayList,实际上Vactor是在add方法加了同步锁,不推荐此方法
- 使用Collections.synchronizedList(new ArrayList<>())代替
- 使用CopyOnWriteArrayList<>()代替;它实现原理就是写时复制“读写分离”的思想,读的时候直接读当前对象,写操作时先复制一份在新的对象上做修改,这样不影响其他线程读操作,等写操作完成之后把对象引用指向新的对象;同样的线程安全的Set读写分离还有CopyOnWriteArraySet;线程安全的HashMap是concurrentHashMap,chm是通过分段锁实现安全线程的。
Java的公平锁和非公平锁
在可重入锁ReentrantLock中,可初始化公平锁和非公平锁,默认构造方法创建非公平锁
- 公平锁:是指在多线程环境中,按照申请锁的顺序来获取锁,如果等待队列为空或者是队列的第一个则直接获得锁,如果已有线程获得锁,则进入等待队里等待获得锁;类似排队,不允许插队。
- 非公平锁:是指在多线程环境中获取锁的顺序可能不是按照申请顺序来的,有可能新线程进入之后直接尝试占有锁,如果占有成功则获得锁,如果尝试占有失败则进入等待队列采用公平锁的方式;在高并发情况下有可能会造成优先级反转或者饥饿现象。饥饿现象就是一直存在线程加塞有的线程一直获取不到锁的场景。非公平锁的吞吐量比公平锁大,synchronized也是一种非公平锁
可重入锁(递归锁)
可重入锁指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁;也就是说,线程可以进入任何一个他已经拥有的的锁所同步的代码块。可重入锁最大的作用是避免死锁问题。
//当线程获得method1的锁之后,自动可以获得method2的锁
public sync void method1(){
method2();
}
public sync void method2(){
}
自旋锁(spinLock)
自旋锁是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去获取锁,这样的好处就是减少线程上下文切换的消耗,不过循环会增加CPU的开销。UNSafe类的CAS就是用的自旋锁
知乎问答:https://zhuanlan.zhihu.com/p/317664253
CountDownLatch、CyclicBarrier、Semaphore使用过吗?
- CountDownLatch主要应用于倒计时,wait方法是等待计数器回归到0时才执行当前线程后续代码,wait(time)表示在time时间段之后执行当前线程后续代码;
public class Test{ psvm(String[] args){ CountDownLatch latch=new CountDownLatch(6); fori(1,6){ new Thread(new Runnable(){ sout("第"+i+"国被灭"); }).start(); } } latch.wait(); sout("秦国一统天下"); } //必须是六国被灭,秦国才统一天下
- CyclicBarrier与CountDownLatch相反,主要应用于自增计数,先到的数的被阻塞,当计数器达到设置的最大值时才执行CyclicBarrier里的run方法。
public class Test{
psvm (String[] args){
CyclicBarrier cb=new CyclicBarrier(7,new Runnable(){
public void run(){
sout("召唤神龙");
}
});
fori(1,7){
new Thread(new Runnable(){
public void run(){
sout("收集到第"+i+"颗龙珠");
cb.await();
}
}).start();
}
//收集到七龙珠才能召唤神龙
}
}
- Semaphore即信号量,主要用于多个共享资源的互斥使用(20个车位有30辆车要停场景),和用于并发线程数控制
public class Test{
//五个车位
Semaphore sem =new Semaphore(5);
fori(1,10){
new Thread(new Runnable{
public void run(){
sem.aquire();//进入车位
sout("第I辆车进入车位");
Thread.sleep(Random.nextInt(10)*1000);//每个车停车随机0-10秒
sout(第i辆车离开停车场");
sem.relaease();//释放车位
}
}).start();
//十辆车,五个停车位,先进的先停车,有车离开则下一辆立即进入停车
}
}
synchronize与Lock的区别是什么?
- 在原始构成上不一样,synchronize是关键字,底层是通过Monitor对象来完成的,wait、notify方法也依赖于monitor对象,只有在同步代码块中才能调用wait、notify方法;Lock是具体的java类,是API层面的锁
- 在使用方法上不同;synchronize不需要用户手动的去释放锁,而是当同步代码块执行完毕之后系统系统让线程释放对锁的占用;ReentrantLock则需要用户手动的lock()和unLock()
- synchronize代码块的执行不可中断,只有抛出异常或者正常运行完成,ReentrantLock可以中断,它可以设置超时方法tryLock(timeout,TimeUnit)中断或者把lockInterruptibly()放在执行代码块中调用interrupt方法进行中断
- synchronize加锁是非公平锁;ReentrantLock可以选择公平或非公平锁,默认非公平
- synchronize不能绑定Condition,唤醒线程时是随机唤醒一个(notify)或者唤醒全部(notifyAll);ReentrantLock可以使用Condition来精确环境线程,可以指定唤醒一个或全部
synchronize、Lock、LockSupport
- synchronize加锁与释放锁必须依赖于synchronize关键字——必须在synchronize代码块中;wait与notify依赖底层的monitor对象。
- ReentrantLock加锁与解锁依赖于lock与unlock,等待与、通知依赖于Condition对象的await、signal——必须在在lock与unlock代码块中
- LockSupport通过park与unpark实现线程的阻塞与唤醒,如果线程先被unpark之后,再加park则park会无效,不会被阻塞——实际上就是先发放了凭证后面park之后直接拿到了凭证会直接唤醒
你知道阻塞队列吗?
什么是阻塞队列?
阻塞队列,首先是一个满足先进先出规则的队列,当在阻塞队列为空时,取数数据的操作会被阻塞,直到其他线程往队列里添加元素才退出阻塞状态;当队列已满时,添加数据的操作会被被阻塞;
为什么使用?有什么好处?
在多线程领域,阻塞也就是在某些情况下挂起线程的操作,一旦条件满足,被挂起的线程又会被唤醒;而使用阻塞队列的好处是不需要关心什么时候需要阻塞线程、什么时候需要唤醒线程,阻塞和唤醒都由BlockingQueue来完成;
常用的阻塞队列七个,比较重要的有如下三个
- ArrayBlockingQuene,是一个基于数组实现的阻塞队列,默认长度是10,队列按照先进先出规则对元素进行排序
- LinkedBlockingQuene,是一个基于链表结构的阻塞队列,先进先出,吞吐量高于ArrayBlockingQueue,阻塞队列的大小为Integer.MAX_VALUE
- SynchronousQuene,是一个不存储元素的阻塞队列(单元素队列),每个元素的插入操作必须等到另一个线程取队列里的元素之后才能正常插入,否则插入操作处于阻塞状态
为什么用线程池,有什么优势?
- 降低资源消耗,通过重复利用已创建的线程来降低线程创建和销毁造成资源消耗
- 提高响应速度,当任务到达时,任务可以不需要等线程创建就可以执行
- 提高线程可管理性,线程是稀缺资源,如果无限制创建不仅会消耗系统资源还会降低系统稳定性,使用线程池可以对线程进行统一的分配,调优和监控
创建线程池的方式,参数有哪些?
通过new ThreadPoolExecute();填入参数创建线程池。七大参数有如下:
- corePoolSize:线程池中常驻线程的数量
- maximumPoolSize:线程池中能够容量同时执行任务的最大线程数量,必须大于等于1,当线corePoolSize线程处理不过来任务时,先把任务放在 阻塞队列中,阻塞队列满了之后然后根据maximumPoolSize创建新线程处理新进入的任务,之前在阻塞队列里的任务稍后执行
- keepAliveTime:多余的空闲线程存活时间,当线程数量大于corePoolSize且线程处于空闲状态时会被回收,存活的时间有keepAliveTime控制
- unit:线程空闲存活时间单位
- workQueue:任务队列的类型,当存在已提交但是未执行的任务时,任务背会队列存放
- threadFactory:创建线程的线程工厂,用于创建线程,一般用默认的即可
- handler:拒绝策略,当任务队列已满且工作线程大于等于maxPoolSize时,新任务的拒绝策略
线程池执行任务流程
- 线程池创建之后,等待提交任务,
- 当提交任务到线程池之后,线程出会做一下判断
a. 如果正在运行的线程数量小于corePoolSize,则马上创建新线程执行任务
b. 如果正在运行的线程数大于等于corePoolSize,任务则会被放入阻塞队列
c. 如果任务进来时队列已经满了,且正在运行的任务小于maximumPoolSize,则会创建新线程执行新进入的任务,且数量不大于maximumPoolSize
d. 如果队列满了且正在运行的线程数大于等于maximumPoolSize,则线程池会启动饱和和拒绝策略来拒绝新任务加入 - 当线程执行完任务之后,它会从阻塞队列里取下一个任务来执行
- 当线程空闲超过keepAliveTime,线程池会判断,如果线程数量大于corePoolSize则这个线程会被回收掉。
线程池拒绝策略
当阻塞队列已满,且线程数量大于等于maximumPoolSize时,线程池会启动拒绝策略
- AbortPolicy:默认策略,会直接抛出RejectExecutionEx阻止系统正常运行
- CallerRunsPolicy:“调用者运行”,这是一种节制机制,该策略既不抛弃任务也不会抛出异常,而是将某些任务回退给调用者,从而降低线程池负载
- DiscardOldestPolicy:抛弃队列中等待最久的任务,把新任务加入到队列中尝试再次执行
- DiscardPolicy:直接丢弃任务,不抛出异常,如果允许任务丢失,这是最好的一种方案
线程池参数如何配置
- CPU密集型:也就是任务需要大量计算,而没有阻塞,CPU一直高占用,一般公式为:线程数=(CPU核数+1个线程)。CPU核数=Runtime.getRuntime().getAvailableProcessor();
2.IO密集型:
场景1:由于IO密集型并不是一直有任务在执行,所以应该稍微多一点,线程数=CPU核数*2;
场景2:大量的IO可能存在阻塞,参考公式为线程数=CPU核数/(1-阻塞系数) ,阻塞系数在0.8-0.9之间,例如八核配置 count=8/(1-0.9)=80个线程
线程死锁编码以及定位分析
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干扰,双方将无法继续推进
//模拟死锁代码
public class MyThreadTest impl Runnable {
private String resA;
private String resB;
public MyThreadTest(String resA,String resB){
this.resA=resA;
this.resB=resB;
}
public void run(){
synchroniz(resA){
sout(Thread.currentThread().getName()+"对资源"+resA+"加锁成功,尝试对资源"+resB+"加锁");
Thread.sleep(2000);
synchronize(resB){
sout(Thread.currentThread().getName()+"已经获得"+resA+"资源,对资源"+resB+"加锁成功");
}
}
}
public static void main(String[] args){
String strA="aaaaaaa";
String strB="bbbbbbb";
new Thread(new MyThread(strA,strB)).start();
new Thread(new MyThread(strB,strA)).start();
}
}
死锁排查:
- 查看java进程号:jps -l
- 分析死锁代码:jstack [pid] 或者 jstack -f
死锁解决办法:
- 对锁增加时间限制
- 严格按照顺序加锁
JVM参数类型
- 标配参数、了解即可,java -version 、java -help
- x参数,了解即可, -Xint(解释执行)、-Xcomp(第一次使用就编译成本地代码)、-Xmixed(混合模式)
- xx参数(重点),包含Boolean型、KV型、
查看java进程jvm参数名称
- 查看进程号:jps -l
- 查看指定参数配置:jinfo -flag [xx参数名],例如:jinfo - flag MetaspaceSize 9929
- 查看当前java进程所有jvm参数:jinfo -flags
-xms等价于最小堆内存-xx:initialHeapSize
-xmx等价于最大堆内存-xx:MaxHeadSize - 查看JVM初始默认参数;java -XX:+PrintFlagsInitial
- 查看JVM被用户改修或者jvm自己更新后的参数; java -XX:+PrintFlagsFinal -version,输出结果中带有冒号的表示用户修改或者jvm自己更新后的值;也可以指定某个参数:java -XX:+PrintFlagsFinal -XX:+MetaspaceSize
平时工作用过的JVM常用基本配置参数有哪些?
- -Xms,初始堆大小,等价于-XX:InitialHeapSize
- -Xmx,最大堆大小,等价于-XX:MaxHeapSize
- -Xss,线程栈大小,等价于-XX:ThreadStackSize默认配置为0,为0 时使用的是系统的默认值512kb-1024kb之间,配置之后则会显示配置后的值
- -Xmn,设置年轻代大小
- -XX:MetaspaceSize,元空间大小
- -XX: +PrintGCDetail,输出详细的GC手机日志信息,一般是:[回收区域名称] [GC前内存占用] [->] [GC后内存占用] [内存区域总大小]
- -XX:SurvivorRatio,设置新生代中eden和s0、s1这三个比例,默认配置为-XX:SurvivorRatio=8(表示eden:s0:s1=8:1:1,eden是s0的8倍),一般使用默认值
- -XX:NewRatio,配置老年代在堆里对比例,默认值是-XX:NewRatio=2(表示新生代1老年代2,新生代占整个堆的1/3),如果-XX:NewRatio=4(表示新生代1老年代4,新生代占堆的1/5)
- -XX:MaxTenuringThreshold,设置垃圾最大年龄,如果设置为0则新生代对象不经过survivor区直接进入老年代,对于老年年代比较多的应用可以提高效率,如果把值配置得比较大,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代里的存活时间。默认值为15,配置范围0-15之间。
强引用、软引用、弱引用、虚引用分别指的是什么?
- 强引用:就是常见的普通对象引用,只要还有强引用指向这个对象,就能证明这个对象还活着,垃圾回收器就不会回收这中对象;在java中当最常见的就是强引用,吧一个对象赋值一个引用变量,这个引用变量就是一个强引用,。当一个对象被强引用变量引用时,对象还处于可达状态,垃圾回收器不会回收该对象;因此强引用也是造成内存泄漏的主要原因之一。
class Demo{
public static void main(String[] args){
Object obj=new Object();
Object obj1=obj;
Object obj2=obj1;
System.gc();
sout(obj2);//依然能正常打印出obj的信息,因为obj2还持有new Object()的引用
}
}
-
软引用:它是一种相对于强引用弱化了一些的引用,需要用java.lang.ref.SooftReference类来实现,可以让对象避免一些垃圾手机;对于软引用对象,在内存足够的时候不会被垃圾回收器,当内存不足时被回收;软引用通常用在对内存敏感的程序中,比如高速缓存就用到软引用,内存够用的时候就保留,不够用就回收。SoftReference<Object> sr=new SoftReference< >(new Object());
-
弱引用:它需要用java.lang.ref.WeakReference来实现,它比软引用的生存时间更短,对于弱引用对象来说,只要垃圾回收器执行就会被收回对该对象占用的内存。
3.1.举个软引用和弱引用的适用场景的例子? 软引用和弱引用可用于加载大量图片的这种场景,还有就是Mybatis底层源码也用到 软引用和弱引用;
3.2. 能谈谈WeakHashMap吗?WeakHashMap也是AbstractMap的子类,当它的key的引用丢失后,执行GC后WeakHashMap存储的对象会被回收 -
虚引用:它需要依赖java.lang.ref.PhantomReference来实现,也叫幽灵引用,顾名思义也就是形同虚设,与其他几种引用不同,虚引用并不会决定对象的生命周期。如果一个对象仅有虚引用,那么它就跟没有他任何引用一样, 在任何时候都能被垃圾回收期回收;它不能单独使用也不能通过它访问对象(调用get方法会返回null),虚引用必须和引用队列ReferenceQueue联合使用。 它的主要作用是用来跟踪对象被垃圾回收的状态。
-
ReferenceQueue:引用队列是用来配合引用工作的,没有引用队列引用也可以运行(PhantomReference必须结合引用队列使用),创建引用的时候可以指定关联的引用队列,当垃圾回收器回收垃圾时,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入引用队列,那么就可以在所应用的对象的内存被回收之前采取必要的行动(相当于引用留临终遗言);当关联的引用队列里有数据时,意味着引用指向的堆内存中的对象被回收,通过这种方式,JVM允许我们在对象被销毁后做一些我们自己想做的事情。
谈谈你对OOM的认识
内存溢出一般包含以下六种情况
- java.lang.StackOverflowError
1.1. 栈内存溢出,死递归或者过多的深度递归多会出现此异常 - java.lang.OutOfMemoryError:Java heap space
2.1. JVM堆的内存溢出,对内存被占用满;堆内存中对象太大或者对象过多会出现此error - java.lang.OutOfMemoryError:GC overhead limit exceeded
3.1. GC回收时间过长时会抛出此Error,过长的定义是超过98%的时间用来做GC且回收不到2%的对内存,连续多次GC都回收不到2%的内存的极端情况下会抛出此Error。假如不抛出GC overhead limit exceeded 错误会出现GC清理出一点点内存空间之后很快被再次填满,迫使GC再次执行,造成恶性循环,导致CPU一直100%而GC没有任何成果。 - java.lang.OutOfMemoryError:Direct buffer memory
4.1. 直接内存溢出,常见于写NIO程序使用ByteBuffer.allocateDirect(capability)方法分配内存的时候,allocateDirect是直使用Native函数直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作的,虽然这样做可以提高性能(因为避免了Java堆内存和Native堆之间复制数据),但是如果不断的分配内存,堆的内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象就不会被回收这时候堆内存充足,但是堆外的内存区域可能已经被占满,再次分配内存时就会抛出OuterOfMemory:Direct buffer memory导致程序崩溃。 - java.lang.OutOfMemoryError:unable to create new native thread
5.1. 在高并发服务器中,经常会出现unable to create new native thread error,准确的说此error与对应的平台有关;导致原因有两种可能:a. 当前应用创建了太多的线程,超过了系统的承载限制;b. 当前服务器不允许当前应用创建更多的线程,linux非root用户系统默认允许单个进程可以创阿金1024个线程,超过了这个数量就会抛出此Error;解决办法要么修改程序是否能降低创建线程的数量,要么修改linux系统1024个线程数限制
5.1.1. 可通过ulimit -u命令查看当前用户可创建线程数量;通过修改系统默认线程数配置,vim /etc/security/limits/90-nproc.conf 或者20-nproc.conf - java.lang.OutOfMemoryError: Metaspace
6.1. 在元空间爱你中存储着虚拟机加载的类信息、常量、静态变量,编译后生成的代码,当持续往元空间里面存对这些信息时,如果元空间内存不足时可能就会出现此Error。
GC垃圾回收算法和垃圾回收器的关系是什么?
- GC垃圾回收算法是内存回收的方法论,算是一种思想,垃圾回收器是算法的实现;(垃圾回收算法一般有:引用计数、复制拷贝、标记清除、标记整理四种)。复制拷贝一般用在新生代区域,标记整理和标记清除一般用载老年代区域,引用计数不在JVM垃圾回收器中实现
垃圾回收的方式有哪些?谈谈你对垃圾回收器的理解
- serial:串行回收器,它为单个线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所有不适合服务器环境。比如在吃饭的过程中,突然有一个服务员来收拾桌子导致吃饭中断,收拾干净后再接着吃饭
- parallel:并行回收器,与串行垃圾回收器有一点类似,不同之处是它由多个垃圾回收线程并行进行工作,垃圾回收时用户线程也是暂停的,适用于科学计算这些弱交互场景。比如在吃饭的时候有多个服务员来收拾桌子,吃饭中断,但是比前面的串行效率高一些
- CMS:并发标记回收器——ConcurrentMarkSweep,用户线程与垃圾回收线程同时执行(可能并行也可能交替执行),不需要停顿用户线程,互联网公司用的比较多,适用于对响应时间有要求的场景,比如吃饭的时候,服务员收拾桌子先让客户去另外一桌接着吃,其他桌的用户不影响;这种情况就是中断时间比较短或者是不中断
- G1:garbage first,G1垃圾回收器将堆内存分割成不同的区域,然后并发的对其进行垃圾回收
- ZGC:Java 11之后的新垃圾回收算法(了解)
怎么查看服务器默认的垃圾回收器?
1.java -XX:+PrintCommandLineFlags -version;java1.8得到默认的垃圾回收器是-XX:UseParallelGC
一般Young区使用Serial、ParNew、Parallel Scavenge垃圾回收器,Old区使用CMS、Serial Old、Parallel Old垃圾回收器,G1垃圾回收器可使用在Young和Old区;
新生代垃圾回收器:
a. Serial:如果在Young区配置了此垃圾回收器,那么在Old区会自动配置Serial Old垃圾回收器—— -XX:+UseSerialGC
b. ParNew:如果在Young区配置了此垃圾回收器,那么在Old(Tenured)区会自动激活CMS垃圾回收器,ParNew常见的场景是配合CMSGC工作,其余的行为和Serial收集器完全一样,ParNew垃圾回收器在垃圾回收过程中同样会暂停所有其他的工作线程,它是很多java虚拟机在Server模式下新生代的默认垃圾回收器—— -XX:+UseParNewGC
b.1. 开启-XX:+UseParNewGC之后,JVM会使用ParNew和Serial Old的这个组合,新生代使用复制算法,老年代使用标记整理算法,但是ParNew+Serial这样的组合在java8已经不再推荐使用
b.2. 可以使用-XX:+ParallelGCThreads配置串行垃圾回收器的工作线程数量,默认值为CPU数据相同的线程数
c. Parallel Scavenge:这个收集器类似ParNew,也是新生代的垃圾收集器,使用复制算法,也是一个并行的多线程垃圾回收器,俗称吞吐量优先级收集器。如果在Young区配置了Parallel Scavenge收集器,那么在Turned区会自动激活Parallel Old回收器;总结就是:并行垃圾回收器就是串行垃圾回收器在新生代和老年代的并行化
优点:
c.1. 可控制吞吐量:ThoughPut=运行用户代码时间/(运行用户代码时间+垃圾回收时间),也就是比如程序运行100分钟,垃圾回收器工作1分钟,那么吞吐量就是99%;搞吞吐量意味着高效的利用CPU的时间,它多用于在后台运算二不需要太多交互的任务。
c.2. 自适应调节策略也是ParallelScavenge回收器与ParNew回收器的一个重要区别,自适应调节策略:虚拟机会根据当前系统的运行情况手机性能监控信息,动态调整这些参数以提供最适合的停顿时间(-XX:MaxGcPauseMillis)或最大吞吐量。
c.3. 常用JVM参数:可使用-XX:+UseParallelGC或者-XX:UseParallelOldGC激活Parallel Scavenges垃圾回收器,开启该参数之后:新生代使用复制算法,老年代使用标记整理算法
c.4. -XX:ParallelGCThreads=数字N,表示启动多少个线程进行垃圾回收,cpu核数>8时N=5/核个数;CPU核数小于8时N=CPU核数
老年代(turened 区)垃圾回收器
1. 串行GC(Serial Old),是Serial垃圾回收器的老年代版本,单线程回收,使用标记整理算法,主要用在client默认的JVM里面的老年代里,在JVMServer模式下java8环境中主要作为老年代CMS垃圾回收器的备用方案
2. 并行GC (Parallel Old),它是Parallel Scavenge的老年代版本
3. 并发标记清除GC(CMS——concurrent Mark sweep),并发停顿低,并发是指与用户线程一起执行,它是一种以最短回收停顿时间为目标的收集器。适用在互联网站活B/S服务器上,这类应用重视服务器的响应时间,希望系统停顿时间最短。CMS非常适合堆内存大、CPU核数对的服务器上,也是G1出现之前大型应用的首选垃圾回收器。开启参数:-XX:+UseConcMarkSweepGC ,开启该参数之后,会自动激活Young区的-XX:+UseParNewGC,使用ParNewGC(Young区)+CMD(Old区)+Serial Old(Old区),Serial Old是作为CMS出现错误时的备用垃圾回收器。当老年代堆内存使用完之前CMS还没完成垃圾回收时CMS回收会失败,此时会启用Serial Old垃圾回收器串行地执行垃圾回收,导致较大的停顿时间。
3.1 执行垃圾回收的四步过程
3.1.1. 初始标记(CMS initial mark):只是标记GCRoots能直接关联的对象,时间很短,但是仍然需要暂停所有用户线程
3.1.2. 并发标记(CMS concurrent mark):与用于线程一起工作,不会暂停其他用户线程,这一步进行GC Roots跟踪,主要是标记过程,标记全部对象
3.1.3. 重新标记(CMS remark):为了修正在并发标记期间,因用户线程继续工作二导致标记产生变动的哪一部分对象的标记记录,所以在正式清理之前需要暂停所有工作线程再标记一次
3.1.4. 并发清除(CMS concurrent sweep):基于前一步的标记结果,用户线程一起工作来清除GC Roots不可达对象,不需要暂停工作线程。由于耗时较长的并发标记和并发清除过程是与用户线程一起执行的,所以总体上来说CMS垃圾回收器是并发的执行
3.2. 优缺点
3.2.1 优点:并发执行垃圾回收,停顿低,提高访问速度
3.2.2. 缺点:并发执行增加会对CPU占用,还有就是采用标记清楚算法会导致大量的内存碎片,可以配置-XX:CMSFullGCsBeForeCompaction=N来指定多少次CMS垃圾回收之后进行一次压缩的FullGC,N默认为0。
生产环境如何配置垃圾回收器
- 单CPU或者小内存,单机程序:-XX:+UseSerialGC
- 多CPU且需要最大吞吐量(后台计算型):-XX:+UseParallelGC或者-XX:+UseParallelOldGC(任选一个另外一个会被自动激活)
- 多CPU且追求停顿低,需要快速响应:-XX:+UseConcMarkSweepGC或者-XX:+UseParNewGC
参数 | 新生代垃圾回收器 | 新生代算法 | 老年代垃圾回收器 | 老年代算法 |
---|---|---|---|---|
-XX:UseSerialGC | SerialGC | 复制 | SerialOldGC | 标记整理 |
-XX:UseParNewGC | ParNew | 复制 | SerialOldGC | 标记整理 |
-XX:UseParallelGC/-XX:UseParallelOldGC | Parallel | 复制 | Parallel Old | 标记整理 |
-XX:UseConcMarkSweepGC | ParNew | 复制 | CMS+ParNew+SerialOld | 标记整理 |
-XX:UseG1GC | G1(整体上采用标记整理算法) | 局部是通过复制算法,不会产生内存碎片 |
对G1垃圾回收器有了解吗?
G1——garbage first,它是一款面向服务端应用的垃圾回收器,可以像CMS一样能与工作线程一起并发执行垃圾回收,从jdk1.7-4开始支持,是java9中默认的垃圾回收器。主要改变是Eden、Survivor和Tenured等内存区域不在是连续的,而是变成一个个大小一样的region(区块),每个region从1-32M不等,一个region有可能属于Eden、survivor或者tenured内存区域。
特点
1. G1充分利用多CPU、多核环境的硬件有阿虎,尽量缩短STW(stop the word)
2. G1整体上采用标记整理算法,局部是通过复制算法,不会产生内存碎片
3. 宏观上G1之中不再区分年轻代和老年代,它把内存区分成多个独立的子区域(region),类似围棋棋盘
4. G1垃圾回收器里虽然内存区都混在一起了,但是微观上还是区分了新生代和老年代,单他们不是物理隔离的,而是一分部region的集合,且不需要region是连续的,也就是说依然会采用不同的GC方式来处理不同的region
5. 虽然G1也是分代垃圾回收器,但整个内存分区不存在物理上的年轻代与老年代之分,也不需要完全独立的survivor堆来做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随着G1的运行在不同代之间前后切换。
G1与之前的垃圾回收器不同有以下四点
1. 之前的Young区和Old区是各自独立的内存区域
2. 之前的年轻代使用额扥+s0+s1进行复制算法
3. 之前的老年代垃圾回收器必须扫描真个老年代区域
4. 之前都是尽可能少儿快速的执行GC为基本设计原则
G1垃圾回收器的底层原理
G1核心思想是将内存划分为大小相同的子区域(region),避免了全内存区的GC操作,在JVM启动的时候会自动设置这些region的大小,在堆的使用上,G1并不要求对象的存储一定是物理上的连续,只要逻辑上连续即可,每个分区可以按需在年轻代和老年代之前切换。可以根据-XX:G1HeapRegionSize设置分区的大小(1-32M且是2的幂次方),默认将整个堆划分为2048个分区(也是最大的分区数),也就是说最大支持32M*2048=64G内存。G1算法将堆内存划分为eden、survivor、old、Humongous这四种region
这些region的一部分包含新生代,新生代的垃圾回收一般采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者survivor region。
这些region的一部分包含老年代,G1回收器通过将对象从一个region复制到另一个region,完成清理工作。这就意味着在正常的处理过程中G1完成了堆的部分region压缩避免了内存碎片问题的存在
这些region还包含一些Humongous区域,这些region主要用来存储大对象,当一个对象占用region的50%的空间G1就认为这是一个巨型对象,如果一个region存不下这个对象就会用多region来存储(类似吃饭的时候人多拼小桌子成大桌子)。为了能够找到连续的H region,有时候不得不启动FullGC来获得Humongous region
回收步骤:
- 初始标记:只标记GC Roots能直接关联到的对象,需要暂停用户线程
- 并发标记:进行GC Roots tracking过程
- 最终标记: 修正并发标记期间,因程序运行导致标记发生变化的那部分对象
- 筛选回收:根据时间来进行价值最大化的回收
常用的G1配置
-XX:+UseG1GC 启用G1垃圾回收器
-XX:G1HeapRegionSize=n 设置G1单个region的大小,1-32,必须是2.幂次方,最大2048个region
-XX:MaxGCPauseMillis=n 最大GC停顿时间,这是个软目标,JVM将尽可能控制停顿失效小于这个数,单不保证都小于
-XX:InitiatingHeadOccupancyPercent=n 堆占用多少的时候触发GC,默认45%
-XX:ConcGCThread=n 并发GC使用的线程数
-XX:G1ReservePercent=n 设置作为空闲空间的预留内存百分比,以降低目标空间移除的风险,默认值10%
G1与CMS相比有什么优势?
- 理论上G1不会产生内存碎片
- G1可以精确的控制停顿
配置JVM参数
java -server [jvm 参数] -jar xxxxx.jar
java -server -Xmx1024m -Xms1024m -XX:+UseConcMarkSweepGC -jar /home/test/testServer.jar
CSDN优秀笔记:https://blog.csdn.net/u011863024/article/details/114684428