JVM只识别字节码,所以JVM其实是跟语言是解耦的,没有直接关联,像Scala、Groovy等语言都可以在JVM上运行
JVM内存区域分为线程共享区和线程私有区,其中线程私有区可以分为:虚拟机栈、本地方法栈和程序计数器,线程共享区分化为方法区和堆
年轻代(Young Gen)
年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Survivor Space(命名为A和B)。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Survivor Space,如果对象仍然存活,则复制到B Survivor Space,如果B Survivor Space已经满,则复制到Old Gen。同时,在扫描Survivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Survivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Survivor Space。这么做主要是为了减少内存碎片的产生。(采用赋值算法)
我们可以看到:Young Gen垃圾回收时,采用将存活对象复制到到空的Suvivor Space的方式来确保尽量不存在内存碎片,采用空间换时间的方式来加速内存中不再被持有的对象尽快能够得到回收。
年老代(Tenured Gen)
年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。
持久代(Perm Gen):持久代主要存放类定义、字节码和常量等很少会变更的信息。
JVM内存回收过程
对象在Eden Space创建,当Eden Space满了的时候,gc就把所有在Eden Space中的对象扫描一次,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象所占用的空间释放。当Eden Space再次变满了的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中的有效对象复制到第二个Survivor Space。如果填充到第二个Survivor Space中的有效对象被第一个Survivor Space或Eden Space中的对象引用,那么这些对象就是长期存在的,此时这些对象将被复制到Permanent Generation。若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC,此时JVM GC停止所有在堆中运行的线程并执行清除动作。
年轻代中的GC
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
也就是分代年龄达到15之后会进入老年代。这个具体是多少岁进入老年代,可以通过JVM参数 “-XX:MaxTenuringThreshold”来设置,默认情况是15岁
2. 动态对象年龄判断
假如说当前放对象的Survivor区域里一批对象的 总大小大于了这块Survivor区域的内存大小的50% ,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了
另外我们要理清楚一个概念,这个实际这个规则运行的时候是如下的逻辑:年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代
在没有回收的情况下 所有对象存活着
比如说一块s区块 100M 如果第一次有20M 不到老年代 第二次来了51M存活 如果之前的20M全部存活,那么这51M和20M将全部到老年代
另外一种情况 在这3次的对象都持续引用,不能回收的情况下,比如说一块s区块 100M 如果第一次有10M 不到老年代 第二次20M 第三次 31M 那么就会由于20+31>50了 那么第一次的10M就会到老年代了。
有一个JvM参数,就是 -XX:PretenureSizeThreshold“,可以把他的值设置为字节数,比如“1048576”,就是1M
如果你创建一个大于这个大小的对象,比如一个超大的数组,或者是别的啥东西,此时就直接把这个大对象放在老年代中,压根不会经过新生代,这样可以避免新生代出现那种大对象,然后在2个Survivor区域里回来复制多次之后才能进入老年代
4.MinorGC后的对象太多无法放入Survivor区怎么办?
如果在Minor GC之后发现剩余的存活对象太多了,没办法放入另外一块Survivor,那么这个时候就必须得把这些对象直接转移到老年代中去
5.老年代空间分配担保规则
在执行任何一次Minor GC之前,JVM会检查一下老年代可用的可用内存空间,是否大于新生代所有对象的总大小
为啥会检查这个呢?因为最极端的情况下,可能新生代的Minor GC过后,所有对象都存活下来了,那岂不是新生代所有对象全部都要进入老年代?
如果说发现老年代的内存大小是大于新生代所有对象的,此时就可以放心大胆的对新生代发起一次Minor GC了,也可以转移到老年代去。
但是假如执行Minor GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小了,那么这个时候是不是有可能在Minor GC之后新生代的对象全部存活下来,然后全部需要转移到老年代去,但是老年代空间又不够?
所以假如Minor Gc之前,发现老年代的可用内存已经小于看新生代的全部对象大小了,就会看一个-XX:-HandlePromotionFailure的参数是否设置了,如果有这个参数,那么就会继续进行下一步判断,
下一步判断,就是看老年代的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小。
举个例子,之前每次Minor GC后,平均都有10MB左右的对象会进入老年代,那么此时老年代可用内存大于10MB
这就说明很可能这次Minor GC过后也是差不多10MB左右的对象会进入老年代,此时老年代空间是够的
如果上面那个步骤判断失败了,或者是 -XX:-HandlePromotionFailure“参数没设置,此时就会直接触发一次Full GC,就是对老年代进行垃圾回收,尽量腾出来一些内存空间,然后再执行Minor GC
如果上面2个步骤都判断成功了,那么就是说可以冒点风险尝试一下Minor GC 此时进行Minor GC,此时进行Minor GC有几种可能:
(1)Minor GC过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survicor区域即可
(2)Minor GC过后,剩余的存活对象的大小是大于Survivor区域的大小,但是是小于老年代可用内存大小的,此时就直接进入老年代即可
(3)Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内存的大小,此时老年代都放不下这些存活对象了,就会发生Handle Promotion Failure的情况,这个时候就会触发一次Full GC
Full GC就是对老年代进行垃圾回收,同时也一般会对新生代进行垃圾回收。
因为这个时候必须把老年代理的没人引用的对象给回收掉,然后才可能让Minor GC过后剩余的存活对象进入老年代里面
如果要Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致所谓的OOM内存溢出了
Major GC和Full GC区别
Full GC:收集young gen、old gen、perm gen
Major GC:有时又叫old gc,只收集old gen
栈溢出、堆溢出、方法区溢出、本机直接内存溢出
JVM创建对象的过程:类加载、检查加载、分配内存、内存空间初始化、设置对象头、对象初始化
划分内存的方式:指针碰撞、空闲列表
解决并发安全:CAS+失败重试(使用乐观锁的机制,有一定的内存开销)、本地线程分配缓冲
“指针碰撞”(Bump the Pointer)(默认用指针碰撞)
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点 的指示器,当创建新的对象时,所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
“空闲列表”(Free List)
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
CAS
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
本地线程分配缓冲
每个线程在java堆中预先分配一小块私有内存,也就是本地线程分配缓冲,这样没有线程都独立拥有一个buffer,如果需要分配内存,就在自己的buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
java对象的内存布局分为三部分,对象头、实例数据、对齐补白
对象头:
一个对象要被回收,需要经过两次过程,一次是没有找到GCRoots的引用链,它将被第一次标记,随后进行一次筛选(如果对象覆盖了finalize),我们可以在finalize方法中去拯救(变为存活对象)
public class Demo {
public static Demo instance=null;
public void iaAlive()
{
System.out.println("I am still Alive ");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed");
Demo.instance=this;
}
public static void main(String[] args) throws InterruptedException {
instance=new Demo();
//对象第一次进行gc
instance=null;
System.gc();
Thread.sleep(1000);//方法优先级低,需要等待
if(instance!=null)
{
instance.iaAlive();
}
else
{
System.out.println("I am dead");
}
//对象第二次GC
instance=null;
System.gc();
Thread.sleep(1000);
if(instance!=null)
{
instance.iaAlive();
}
else
{
System.out.println("I am dead");
}
}
}
算法就是将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完后,将活的对象标记出来,然后把这些活对象复制到另外一块空闲区域上,最后再把已使用过的内存空间完全清理掉。这样每次都是对整个半区进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现方法简单,运行高效。但是这种算法的代价是将内存缩小了一半。
垃圾回收一般分为两个部分:1.扫描新生代判断对象是否存活,2. 将存活对象复制到S区,复制对象地方时间远远大于扫描的时间
假设对象A存活时间为7秒
新生代空间200M,每隔5秒GC一次,每次GC耗时100ms
此时gc一次时间为T1+T2
对新生代进行扩容:空间为400M,没10秒GC一次,每次GC耗时200ms
此时gc耗时2T1(对象A的存活时间已经到了,其自动消亡,不要复制)
Concurrent Mark Sweep(并发标记清除算法的垃圾回收器,适用于老年代)
取最短回收停顿时间为目标的老年代收集器,适合基于B/S的服务器上,系统停顿时间短,用户体验较好。CMS也是一款真正意义上的并发收集器,能够与用户线程同时进行。虽然,并发回收过程中也有几个阶段需要Stop the world,但是由于任务简单,所以停顿时间非常短。
步骤
CMS中的问题
java中的线程分为两种守护线程(Daemon)和用户线程(user)
守护线程通过调用Thread.setDaemon(true)设置
一般程序使用的都是用户线程
守护线程我们一般用不上,比如垃圾回收线程就是守护线程(Daemon)
使用守护线程的注意点
CPU时间片:CPU时间片是CPU分配给每个线程执行的时间段,一般为几十毫秒
上下文切换:当一个线程的时间片用完了,或者因自身原因被迫暂停运行了,这个时候另外一个线程(可以是同一个线程或者其他进程中的线程)就会被操作系统选中。来占用处理器。这个一个线程被暂停剥夺使用权,另外一个线程被选中或者继续进行的过程叫作上下文切换。
上下文:在这种切入切出的过程中,操作系统需要保存和恢复响应的进度信息,这个进度信息就是上下文
死锁:是指两个或两个以上的进程(或线程)在执行的过程中,因争夺资源而造成的一种相互等待的现象,若无外力作用,他们都将无法进行下去
危害
Executor 接口对象能执行我们的线程任务
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
CAS(Compare And Swap),即比较并交换 CAS(V,E,N)。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——要更新的变量(V)、预期原值(E)和新值(N)。核心算法是如果V 值等于E 值,则将V 的值设为N 。若V 值和E 值不同,则说明已经有其他线程做了更新,则当前线程不做更新,直到V、E两个值相等,才更新V的值。
缺点:
Lock接口比同步方法同步代码块提供了更具扩展性的锁操作
Lock是synchronized的扩展版,Lock提供了无条件的,可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的锁等操作,另外Lock的实现类基本都支持非公平锁(默认锁)和公平锁,synchronize只支持非公平锁,当然在大部分情况下,非公平锁是最高效的选择
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列
这两个附加的操作是:当队列为空时,获取元素的线程会等待队列变为非空;当队列满时,存储元素的线程会等待队列可用
阻塞 队列在实现上,主要利用了Condition和Lock的等待通知模式
Callable接口类似于Runnable接口,但是Runnable不会返回结果,无法抛出返回结果的异常,而Callable的功能更强大一些,被线程执行后可以返回值 ,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。
Callable可以认为带有回调的Runnable
Future接口表示异步任务,是还没有完成任务给出的 未知结果,所有Callable用于产生结果,Future用于获取结果
FutureTask表示一个可以取消的异步运算,他有启动和取消运算、查询运算是否完成和取回运算结果等方法,只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞
一个FutureTask对象可以调用Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所有他可以交给Executor执行
并发容器可以简单的理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,他们将会串行执行。比如vector、HashTable以及Collections.synchronizedSet,synchronizedList等方法返回容器
当你调用start()方法是你将创建新的线程,并且执行run()方法里面的代码
如果你直接调用run()方法,他不会创建新的线程也不会调用线程的代码,只是将run()方法作为普通的方法区执行
不可变对象(Immntable Objects)即对象一旦被创建他的状态(对象的数据即对象的属性值)就不能改变,反之即为可变对象(Mutable Objects)
Java平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。
不可变对象天生是线程安全的。他们的常量(域)是在构造函数中创建的。既然他们的状态无法修改,这些常量永远不会变
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所有每次在拿数据的时候都会上锁,这样别人想拿数据的时候就会阻塞直到他拿到锁。java里面synchronize关键字实现的是悲观锁
乐观锁:顾名思义就是很乐观,每次去拿数据的时候都认为别人不会修改,所有不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制,java中原子变量就是使用了乐观锁
乐观锁的实现方式:使用版本标识来确定读到的数据和提交时的数据是否一致。提交后修改版本标识,不一致时可以采用丢弃和再次尝试的策略
java中的Compare and Swap即CAS,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程可以更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并且可以再次尝试
java提供的锁是对象级的而不是线程级的。
每个对象都有锁,通过线程获得,如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。
如果wait方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。
简单的锁,由于wait、notify、notifyAll都是锁级别的操作,所有把他们定义为Object类中因为锁属于对象。
当多个线程访问某个类时,不管运行环境采用了何种调度方式或者这些线程将如何交替运行,并且在调用代码中不需要额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的
实现线程安全的方式
另外重要的一点是:如果这个线程持有某个对象的监视器,那么这个对象监视器会被立即释放
关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。
一个变量被定义为 volatile 的特性:
如果不符合 运算结果并不依赖变量当前值,或者能够确保只有单一的线程修改变量的值 和 变量不需要与其他的状态
变量共同参与不变约束 就要通过加锁(使用 synchronize 或 java.util.concurrent 中的原子类)来保证原子性。
通过插入内存屏障保证一致性。
ThreadLocal解决多线程的并发问题,是Thread的局部变量,使用它维护变量,会使该变量的线程提供一个独立的副本,可以独立修改,不会影响其他线程的副本
抢占式:一个线程执行完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行
线程组和线程池是两个不同的概念,他们的作用完全不同
线程组是为了方便线程的管理
线程池是为了管理线程的生命周期,复用线程,减少常见销毁线程的开销
SynchronizedMap一次锁住整张表来保证线程安全,所以每次只有一个线程来访问map(强一致性)
ConcurrentHashMap使用分段锁来保证多线程下的性能(弱一致性)
强一致性:任何时刻线程读到的缓冲数据都是一样的
弱一致性:不能保证任何一次都能读到最近一次写入的数据,但能保证最终可以读到写入的数据
JDK1.7
ConcurrentHashMap把实际的map分成若干部分来实现他的可扩展性和线程安全。这种划分是使用并发度获得的,他是ConcurrentHashMap类构造函数的一个可选参数,默认值为16,这样在多线程的情况下可以避免使用
在JDK1.8后。他摒弃了Segment(段锁)的概念,而是启用了一种全新的方式实现利用CAS算法。同时加入更多的辅助变量来提高并发度
由于大部分操作系统采用抢占式的线程调度算法,因此可能出现出现某条线程常常获取到CPU控制权的情况,为了让某些某些优先级比较低的也能获取到CPU的使用权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片段的操作,这也是平衡CPU控制权的一种操作
大循环里面可以写一句Thread.sleep(0),因为这样给了其他线程获取CPU控制权的权力,这样线程就不会处于假死状态
线程调度器是一个操作系统的服务,他负责为Runnable状态的线程分配CPU时间,一旦我们创建线程并启动他,他的执行便依赖于线程调度器得实现
CPU时间片是CPU分配给每个线程执行的时间片,一般为几十毫秒
时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程,分配CPU的时间可以基于线程优先级或线程的等待时间
可以使用Thread类的join方法来确保所有程序创建的线程在main()方法退出去结束
java.util.Timer是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer类可以用安排一次性任务或者周期性任务
Semaphore就是一个信号量,他的作用是现在某段代码块的并发数
Semaphore有一个构造函数,可以传入一个int型的整数n,表示某个代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这个代码块,下一个线程在进入
由此可以看出如果Semaphore构造函数中传入的int类型的整数n=1,相当于变成了一个synchronize了
在不影响单线程程序执行结果的前提下,计算机为了最大限度的发挥机器性能,会对指令重排序优化
ReadWriteLock接口的实现是ReentrantReadWriteLock
ReentrantLock等大部分锁是排它锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞,读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相对于一般的排它锁有了很大的提升
一般情况下,读写锁的性能都会比排它锁好,因为大多数常见读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量
Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前,但他并不能保证原子性,例如volatile修饰count变量那么count++操作就不是原子性。
而AtomicInterger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性进行增量操作把当前值加一,其他数据类型和引用变量也可以进行相似的操作
Thread.holdsLock(Object obj)方法,当且仅当对象的obj的监视器被某条线程持有的时候才会返回true
注意这里是一个static方法,这意味着**“某条线程”指的是当前线程**
用来构建锁或其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch就是基于AQS实现的,他使用了一个int成员变量表示同步状态,通过内置FIFO队列俩完成资源获取线程的队列工作。他是CLH队列锁的一种变体实现,他有两种实现方式:独占式、共享式
AQS主要使用方式是继承,子类通过继承;AQS并实现他的抽象方法来管理他的同步状态,同步器的设计基于模板方法模式,所有要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法,如:tryAcquire,tryReleaseShared等等
线程类的构造方法、静态方法是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的
假设Thread2中new了Thread1,main函数中new了Thread2 那么
(1)Thread2的构造方法、静态方法是main线程调用,Thread2的run方法是Thread1自己调用的
(2) Thread1的构造方法、静态方法是Thread2调用,Thread1的run方法是Thread1自己调用的
多线程中线程不确定的执行时序导致不正确的结果,这就是竞争(竞态)条件
可以采用加锁的方式使线程串行访问临界区
wait/notify是线程之间的通信,他们存在竞态,我们必须保证在满足条件的情况下才进行wait。换句话说,如果不加锁的话,那么wait被调用的时候可能wait的条件已经不满足了
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后返回
安全点
用户线程暂停,GC 线程要开始工作,但是要确保用户线程暂停的这行字节码指令是不会导致引用关系的变化。所以 JVM 会在字节码指令中,选一些指令,作为“安全点”,比如方法调用、循环跳转、异常跳转等,一般是这些指令才会产生安全点。
为什么它叫安全点,是这样的,GC 时要暂停业务线程,并不是抢占式中断(立马把业务线程中断)而是主动是中断。
主动式中断是设置一个标志,这个标志是中断标志,各业务线程在运行过程中会不停的主动去轮询这个标志,一旦发现中断标志为 True,就会在自己最近的“安全点”上主动中断挂起。
安全区域
为什么需要安全区域?
要是业务线程都不执行(业务线程处于 Sleep 或者是 Blocked 状态),那么程序就没办法进入安全点,对于这种情况,就必须引入安全区域。
安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区城看作被扩展拉伸了的安全点。
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,这段时间里 JVM 要发起 GC 就不必去管这个线程了。
当线程要离开安全区域时,它要 JVM 是否已经完成了(根节点枚举,或者其他 GC 中需要暂停用户线程的阶段)
1、如果完成了,那线程就当作没事发生过,继续执行。
2、否则它就必须一直等待, 直到收到可以离开安全区域的信号为止。
一般只要开启gc日志打印,都会默认开启简单日志模式,生成环境强烈建议开启详细gc日志模式,两种模式互斥,同时开启详细的gc日志模式