线程私有:栈,本地方法栈,程序计数器
线程共享:堆,方法区
程序计数器 线程私有 无内存溢出
JVM的多线程是通过线程之间轮流切换,分配处理器执行时间的方式来实现的
。
为了保证线程切换后能回到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,这个内存区域称为线程私有
的内存,执行Native方法时,程序计数器为空。
JVM栈 线程私有
描述的是Java执行方法的线程内存模型
:每个方法被执行的时候,JVM都会同步创建一个 栈桢
用于存储 局部变量表 操作数栈 动态连接 方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈桢在虚拟机栈中的从入栈到出栈的过程。
栈异常
如果线程请求的 栈深度 大于 虚拟机所允许的深度,将抛出StackOverFlowError
异常。
如果JVM栈容量可以动态扩展,当栈扩展时无法申请到足够内存空间将会抛出OutOfMemoryError
异常。
本地方法栈 线程私有
他与虚拟机栈所发挥的作用是极其相似的,区别在于虚拟机栈为虚拟机执行java方法也就是class字节码服务。而本地方法栈则是为虚拟机使用到 本地(Native)方法 服务。
堆 线程共享
存放对象的实例。是 垃圾收集器
管理的内存区域。可扩展(通过 -Xms和-Xmx参数设定),如果在java堆中没有内存完成实例分配,并且堆无法再扩展时,会抛出 OutOfMemoryError
异常。
方法区 线程共享
通过 永久代
实现 方法区,到了JDK8
,完全废弃了 永久代 的概念,改用 元空间(Meta-space)
来代替。把JDK7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。这区域的内存回收目标主要是针对 常量池的回收
和对类型 的卸载
。
如果方法区无法满足新的内存分配需求时,会抛出 OutOfMemoryError
运行时常量池
是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,
还有一项信息是 常量池表(Constant Pool Table)
,用于存放 编译器生成的各种字面量和符号引用
,这部分内容将在 类加载后
存放到方法区的运行时常量池中
。除了保存Class文件中描述的符号引用,还会把由符号引用``翻译
出来的直接引用
也存储在运行时常量池中。
直接内存
JDK1.4引入了 NIO(New Input/OutPut)类,引入一种基于 通道(Channel)与缓冲区(Buffer)的I/O方式
,它可以使用 Native函数库
直接分配堆外内存
,然后通过一个存储在java堆里面的 DirectByteBuffer对象
作为这块内存的引用
进行操作。这样能在一些场景中显著提高性能
因为避免了在 java堆 和 Native堆 中来回 复制数据
。
对象的创建
- 当 java 虚拟机遇到一条字节码 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被
加载、解析和初始化过
。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小
在类加载完成后
便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从java堆中划分出来。假设java堆中内存是绝对规整的,所有被使用过的内存都放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。
这种分配方式称为指针碰撞
(Bump The Pointer)。但如果 java堆中的内存 并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行 指针碰撞 了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到了一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)
。选择哪种分配方式由 java堆 是否规整
决定,而 java堆 是否规整又由所采用的 垃圾收集器 是否带有 空间压缩整理(Compact)的能力
决定。
因此,当使用Serial、ParNew等带压缩整理过程的收集器
时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用 CMS这种 基于清除(Sweep)算法
的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。
- 除如何划分可用空间外,还有一个需要考虑的问题:对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在
并发情况下也并不是线程安全的
,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
①对分配内存空间的动作进行同步处理—实际上虚拟机是采用 CAS(CAS,compare and swap,比较并交换,在JDK 5之前Java 是靠synchronized 保证同步的,synchronized是独占锁,独占锁是一种悲观锁,会导致其他线程挂起。乐观锁用到的机制就是 CAS
配上失败重试的方式保证更新操作的原子性
。
在CAS中,有这样三个值:
V:要更新的变量(var)
E:预期值(expected)
N:新值(new)
⽐较并交换的过程如下:
判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。所以这⾥的预期值E本质上指的是"旧值"。
②另外一种是把内存分配的动作
按照线程
划分在不同的空间之中进行,即每个线程java堆中预先分配一小块内存,称为
`本地线程分配缓冲`,哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,`只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定`。
取消堆的自动扩展
将 堆
的 最小值 -Xms
参数与 最大值 -Xmx
参数设置为一样
即可避免堆自动扩展
内存溢出 与 内存泄漏
溢出
:没有足够的空间
泄漏
:申请到内存空间后,无法释放已申请的空间,积累多了,会提高溢出发生的概率。
堆溢出
一般的异常信息:java.lang.OutOfMemoryError:Java heap spaces。
java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免
垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。
解决
:先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(MemoryLeak)还是内存溢出(Memory Overflow)。
①如果是 内存泄漏,可通过查看泄漏对象到GC Roots的引用链
,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收他们。
②如果是 内存溢出,就是内存中的对象确实是必须存活的,那就应当检查JVM的堆参数(-Xms和-Xmx)设置,与机器的内存对比,看看是否还有向上调整的空间
。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗
。
虚拟机栈和本地方法栈溢出
栈容量
只能通过 -Xss
参数来设定。
① 如果线程请求的栈深度大于虚拟机所允许的最大深度
,将抛出StackOverFlowError
。
② 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时
,抛出OutOfMemoryError
。
结论
:无论是由于栈桢太大
还是虚拟机栈容量太小
,当新的栈桢内存无法分配的时候,HotSport虚拟机 抛出的都是 StackOverFlowError
。如果是在允许动态扩展栈容量大小的虚拟机上,相同代码会导致不一样的情况。
方法区和运行时常量池溢出
异常信息:java.lang.OutOfMemoryError:PermGenspace
String:intern()是一个本地方法
,他的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回该对象在常量池中的引用。否则,将会此String对象包含的字符串添加到常量池中,并且返回此String对象的引用
。 在JDK6
或者更早之前的HotSport中,常量池
都是分配在 永久代
中。
可以通过 -XX:PermSize
和 -XX:MaxPermSize
限制 永久代
的大小。
SOF(堆栈溢出StackOverflow):
StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。
因为栈一般默认为1-2m
,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。
栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、map数据过大。
自JDK7起,原本存放在 永久代 的字符串常量池被移至 java堆 之中。
JDK6和JDK7的String:intern()区别
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
这段代码在JDK6中运行,结果是两个false,在JDK7中,会得到一个true和一个false。
因为在JDK6中,intern()方法会把首次遇到的字符串实例 复制
到永久代的字符串常量池
中存储,返回的也是永久代中的这个字符串实例的引用
,而由StringBuilder创建的字符串对象实例实在java堆
上。所以不是同一个引用。
在JDK7中的 intern() 方法实现不需要拷贝字符串的实例到永久代中
了,既然字符串常量池已经移到了java堆
中,那只需要在常量池里记录一下首次出现的实例引用
即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个
。而对 str2中的java
这个字符串在执行StrigBuilder.toString()之前
就出现出现过了。字符串常量池中已经有它的引用,不符合intern()要求首次遇到
的原则,“计算机软件”这个字符串是首次出现的,所以返回true。
方法区和元空间是什么关系?
- 首先,方法区是JVM规范的一个概念定义,并不是一个具体的实现,每一个JVM的实现都可以有各自的实现;
- 然后,在Java官方的HotSpot 虚拟机中,Java8版本以后,是用元空间来实现的方法区;在Java8之前的版本,则是用永久代实现的方法区;
- 也就是说,“元空间” 和 “方法区”,一个是HotSpot 的具体实现技术,一个是JVM规范的抽象定义;
元空间是使用本地内存(Native Memory)实现的,也就是说它的内存是不在虚拟机内的
为什么用元空间代替永久代?
类的元数据信息(metadata)转移到Metaspace的原因是PermGen很难调整。PermGen中类的元数据信息在每次FullGC的时候可能会被收集。而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class的总数,常量池的大小,方法的大小等。
由于**类的元数据可以在本地内存(native memory)**之外分配,所以其最大可利用空间是整个系统内存的可用空间。这样,你将不再会遇到OOM错误,溢出的内存会涌入到交换空间。最终用户可以为类元数据指定最大可利用的本地内存空间,JVM也可以增加本地内存空间来满足类元数据信息的存储。
设置元空间的参数:
-XX:MaxMetaspaceSize
:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。-XX:MetaspaceSize
:指定元空间的初始空间大小 字节为单位,达到该值就会触发垃圾收集
进行类型卸载
,同时收集器会对该值进行调整,如果释放了大量空间就适当降低该值
,如果释放了少量空间,那么在不超过-XX:MetaspaceSize(如果设置了话)的情况下,适当提高该值
。-XX:MinMetaspaceFreeRatio
:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:MaxMetaspaceFreeRatio
:用于控制最大的元空间剩余容量的百分比。
引用计数算法 <不是java使用的判断对象是否已经死亡的算法>
在对象中添加一个引用计数器,每当有一个地方引用他就加1,当引用失效时,就减1,任何时刻计数器为0的时候就说明对象不再是被使用的。但是当两个对象互相引用着彼此,导致计数器都不为0,所以无法被引用计数算法回收
。
JAVA使用的是 可达性分析算法
思路是:通过一系列称为"GC Roots"
的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链
,如果某个对象到GC Roots之间没有任何引用链相连,或者是GC Roots到这个对象不可达
的时候则证明此对象不可能再被引用了。
固定可作为GC Roots的对象包括:
- 在
虚拟机栈(栈桢中的本地变量表)中引用的对象
,譬如当前正在运行的方法所使用到的参数、局部变量、临时变量等 - 在方法区中
类静态属性引用的对象
,如java类的引用类型静态变量 - 在方法区中
常量引用的对象
,譬如字符串常量池(String Table)里的引用 - 在
本地方法栈中JNI(即通常所说的Native方法)引用的对象
- JVM内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象等,还有系统类加载器。
- 所有被
同步锁(synchronized)持有的对象
- 反映JVM内部情况的JMXBean、JVMTI中注册的回调,本地代码缓存等。
引用
引用分为 强引用、软引用、弱引用和虚引用
。
-
强引用:只要强引用关系还在,垃圾收集器就永远不会回收被引用的对象。
使用场景
:String str = new String(“str”); -
软引用:描述一些
还用有,但非必须的对象
。只被软引用关联的对象,在系统将要发生内存溢出异常之前
,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出的异常。提供了SoftReference类实现了软引用。源码`:假设垃圾收集器在某个时间点确定一个对象是软可达的。 那时,它可以选择以原子方式清除对该对象的所有软引用以及对任何其他软可访问对象的所有软引用,通过强引用链可以从这些对象中访问该对象。同时或稍后,它会将那些注册到引用队列(ReferenceQueue)的新清除的软引用加入队列。
使用场景`:创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。
-
弱引用:最常用于实现
规范化映射
。被弱引用关联的对象只能生存到下一次垃圾收集发生为止,当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。WeakReference类实现了软引用。源码`:通过强引用和软引用链可以从中访问该对象。 同时或稍后它会将那些注册到引用队列的新清除的弱引用加入队列。
使用场景`: Java源码中的java.util.WeakHashMap中的key就是使用弱引用,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
-
虚引用:也称为“幽灵引用”或者“幻影引用”。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是
为了能在这个对象被收集回收的时候收到一个系统通知
。PhantomReference实现了虚引用。它被回收之前,会被放入ReferenceQueue
中。注意,其它引用是被JVM回收后才被传入ReferenceQueue中
的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作
。还有就是,虚引用创建的时候,必须带有ReferenceQueue
。使用场景`:对象销毁前的一些操作,比如说资源释放等。Object.finalize()虽然也可以做这类动作,但是这个方式既不安全又低效。
生存还是死亡
经过可达性分析算法中判定不可达的对象,也不是立刻死亡的,要真正宣告一个对象的死亡,最多会经历两次标记过程:
如果对象在进行可达性分析算法后发现`没有与GC Roots相连接的引用链`,那他将会被`第一次标记`。`随后进行一次筛选`,`筛选条件是此对象是否有必要执行 finalize(),假如对象没有覆盖finalize(),或者finalize()已经被虚拟机调用过,那么虚拟机将这两种情况都视为 没有必要执行`。
如果这个对象被视为`有必要执行finalize()`,那么`该对象会被放在一个叫做F-Queue队列中`,并在之后`由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()`。这里所说的“执行”是指虚拟机会触发这个方法`不代表会等待这个方法的结束`。原因是`如果某个对象的finalize()执行缓慢或者更极端的发生了死循环,将有可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统崩溃`。finalize()是对象逃脱死亡命运的最后机会,`稍后收集器会对F-Queue中的对象进行第二次标记,如果对象在finalize()中拯救了自己[只要重新与引用链上的任何一个对象建立关联即可]则可以逃脱。
JAVA堆 区域
新生代(Young Generation)
和老年代(Old Generation)
,在新生代中,每次垃圾收集都会发现大量对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
部分收集(Partial GC)
:指目标不是完整收集整个java堆得垃圾收集,其中又分为:
-
新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
-
老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集
目前
只有CMS收集器
会单独收集老年代
的行为。【注意】Major GC这个词会有混淆,需要区分上下文区分到底是 老年代的收集还是整堆收集。
-
混合收集(Mixed GC)
:指目标是收集
整个新生代
以及部分老年代
的垃圾收集,目前只有G1收集器
会有这种行为。 -
整堆收集(Full GC):
收集整个java堆
和方法区
的垃圾收集
Minor GC 触发条件
当Eden区满时,触发Minor GC。
Full GC 触发条件
- System.gc()方法的调用
- 老年代空间不足
- 永生区空间不足(JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据)
- GC时出现promotion failed和concurrent mode failure
- 统计得到的Minor GC晋升到旧老年代平均大小大于老年代剩余空间
- 堆中分配很大的对象
标记-清除 算法(Mark-Sweep)
先标记需要回收的对象,再进行回收。或者先标记存活的对象,再回收未被标记的对象。
有两个缺点:
执行效率不稳定
,如果java堆中包含大量对象,其中大部分都会被回收,那么进行全局标记和清除的效率就会随着对象的数量增多而降低。内存碎片化的问题
,标记,清除之后会产生大量的内存碎片,导致以后在程序运行的时候需要分配较大对象时无法找到足够大的连续的内存空间而不得不提前触发另一次垃圾收集的动作。
标记-复制 算法(Mark-Copying)
"半区复制"
:将可用内存一分为二,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用的内存空间一次清理掉。
缺点:
如果内存中是大量存活的对象,那么这种算法会产生大量的内存间复制的开销,并且可用内存缩小了一半,空间资源浪费严重。
"Appel式回收"
:把新生代分为一块较大的Eden(伊甸园)空间
和两块
较小的Survivor空间
,每次分配内存只使用Eden和其中一块Survivor空间上
。
发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已使用过的Survivor空间。
HotSport虚拟机默认Eden和Survivor的大小比例为8:1
,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%+一块Survivor的10%
),只有一个Survivor空间即10%的新生代是不会被浪费的。除此之外,还有一个“逃生门”
的安全设计,当一个Survivor空间不足以放下一次Minor GC之后存活的对象,就需要依赖其他区域(大多就是老年代)进行分配担保。
也就是如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保
机制直接进入老年代空间
。
标记-整理 算法(Mark-Compact)
标记-复制算法 在对象存活率较高的时候就要进行较多的复制操作,效率就会降低。更关键的是,如果不想浪费50%的空间,就要有额外的空间进行分配担保
,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选择 标记-复制。
标记过程和标记-清除一样
,但是后续的步骤不是直接清除可回收对象,而是让所有存活下来的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
和标记-清除
算法区别的本质
就在于 是否为移动式
。
缺点
:如果移动存活对象,尤其是老年代这种每次回收都有大量对象存活区域,更新所有引用
这些对象就成为一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才可以进行。
[通常标记-清除算法 也是需要停顿用户线程来标记、清理可回收的对象,只是停顿时间相对而言要短
]
HotSport虚拟机里关注吞吐量的Parallel Old收集器
是基于标记-整理算法
的,
而关注延迟
的CMS
则是基于标记-清除算法
的,并且在内存空间碎片过多的情况下
,CMS收集器
则会进行一次标记-整理算法
收集一次。
Serial收集器
串行 单线程
新生代收集器
标记-复制
不仅是他只会使用一个处理器或一条收集线程去完成垃圾收集工作
,更重要的是强调在他进行垃圾收集时,必须暂停其他所有工作线程,直到他收集结束。也就是STW。
有着优于其他收集器的地方
那就是 简单而高效(与其他收集器的单线程相比)
,对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的
,对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销
,专心做垃圾收集自然可以获得最高的单线程收集效率
。所以,Serial收集器对于运行在客户端模式下
的虚拟机来说是一个很好的选择。
ParNew 收集器
多线程并行
新生代收集器
标记-复制
ParNew是Serial收集器的多线程并行版本。
-XX:SurvivorRatio:
设置新生代
中eden
和S0/S1
空间的比例
默认-XX:SurvivorRatio=8
,Eden:S0:S1=8:1:1和Serial收集器完全一致。除了Serial收集器外,只有ParNew收集器可以和CMS收集器配合使用工作。
JDK5 出现了CMS收集器
,首次实现了让垃圾收集线程和用户线程(基本上)同时工作
但是,作为老年代收集器的CMS
无法和在JDK1.4.0
中的新生代收集器Parallel Scavenge配合工作
,所以在JDK5中使用CMS收集老年代的时候,新生代只能选择ParNew或者Serial之一。ParNew收集器是激活CMS后的默认新生代收集器
也可以使用-XX:+/-UseParNewGC
参数选项来强制指定或者禁用它。JDK9开始,取消了-XX:+/-UseParNewGC参数选项,这意味着ParNew合并入CMS,成为专门处理新生代的组成部分。
为什么只有 ParNew能与CMS 收集器配合?
CMS作为老年代收集器
,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge
配合工作;
因为Parallel Scavenge(以及G1
都没有使用传统的GC收集器代码框架
,而另外独立实现;而其余几种收集器则共用了部分的框架代码;
Parallel Scavenge收集器 多线程并行 新生代收集器 标记复制
也是一款新生代收集器
,同样是基于标记-复制
算法实现的收集器,也是能够并行
收集的收集器。和ParNew非常类似。
特点:``CMS等收集器
的关注点是尽可能缩短垃圾收集时用户线程的停顿时间
,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量
[可就是上面写着Parallel Scavenge收集器更专注于吞吐量,CMS收集器更专注于停顿时间]
吞吐量就是 运行程序所消耗的时间 / (运行程序所消耗的时间 + 垃圾收集时间)
高吞吐量的话用哪种gc算法,用那种垃圾收集器
复制清除 parallel Scavenge
parallel Scavenge收集器提供了两个参数用于精准控制吞吐量:
-XX:MaxGCPauseMillis
: 控制最大垃圾收集停顿时间
控制垃圾收集停顿时间缩短的话,其实是以牺牲吞吐量
和新生代空间
为代价的,这也直接导致了垃圾收集更频繁,吞吐量也下降
。
-XX:GCTimeRatio
:设置吞吐量大小
-XX:GCTimeRatio
:应设置为正整数,表示用户期望虚拟机消耗在GC上的时间不超过程序运行时间的1/(1+N),默认值为99,尽可能的保证应用程序执行的时间为收集器执行时间的99倍,即收集器的时间消耗不超过总运行时间的1%。
由于与吞吐量密切相关
,所以Parallel Scavenge收集器也被称为吞吐量优先收集器
还有一个参数 -XX:+UserAdapativeSizePolicy
:自适应调节策略
。
当这个参数被激活后
,就不需要人工指定新生代对象大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等参数,
虚拟机会自动根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或者最大的吞吐量
。只需要把基本的内存数据设置好(如-Xmx最大堆)
,然后使用-XX:MaxGCPauseMillis
或者
-XX:GCTimeRatio
给虚拟机一个优化目标
。这个自适应调节策略也是Parallel Scavenge收集器和ParNew收集器的一个重要特性
。
Serial Old收集器 是Serial的 老年代版本 单线程 标记-整理
Parallel Old收集器 是Parallel Scavenge收集器的 老年代版本 多线程并发 标记-整理算法
CMS收集器(ConCurrent Mark Sweep)并发标记清除 并发低停顿
是以获取最短回收时间为目标
的收集器
-
初始标记(CMS initial mark) 需要STW
-
并发标记(CMS concurrent mark)
-
重新标记(CMS remark) 需要STW
-
并发清除(CMS concurrent sweep)
初始标记
和重新标记
需要STW
,初始标记仅仅是标记一下GC Roots能直接关联到的对象
,速度很快;并发标记
则是根据GC Roots的直接关联对象开始遍历整个对象图的过程
,耗时较长但是不需要停顿用户线程;重新标记
是为了修正在并行标记期间
,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
,这个阶段停顿时间比初始标记长一些,但比并发标记短的多;``并发清除
,由于不需要移动对象
,所以也可以和用户线程并发
耗时最长
的是并发标记
和并发清除
,但是垃圾收集器线程都可以和用户线程一起工作,所以CMS收集器的内存回收过程是与用户线程并发执行的。
缺点
:CMS收集器
对处理器资源非常敏感
- 由于
CMS无法处理浮动垃圾
,有可能出现Concurrent Mode Failure
失败而导致另一次完全STW的Full GC产生
。在CMS的并发标记
和并发清除
阶段,用户线程还是在继续执行的,会产生新的垃圾对象,但是这些垃圾对象是在这次标记过程之后,CMS无法在本次垃圾收集中处理掉他们,只能等到下一次,这一部分就叫做“浮动垃圾”
。 - 由于使用的是
标记-清理算法
,会产生内存碎片
,会出现往往老年代还有很多空间,但是找不到一块足够大的内存空间来分配当前对象,而不得不提前触发一次Full GC的情况
Garbage First G1
面向服务端的 全功能的 用户可指定停顿时间 默认200ms
可以面向堆内存任何部分来组成回收集
(Collection Set 简称CSet)
哪块内存中存放的垃圾数量最多,回收利益最大
,这就是G1的Mixed GC
模式
G1把连续的java堆划分为多个大小相等的独立区域(Region)
,每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor和老年代空间
。这样无论是新创建的对象还是已经存活一段时间的对象、熬过多次收集的旧对象都能获取很好的收集效果
。
Region中
还有一类特殊的Humongous区域
,专门用来存储大对象
,G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数
-XX:G1HeapRegionSize
设定,取值范围为1MB~32MB
,且应为2的N次幂
。而对于超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region中
。
G1大多数行为都把Humongous Region作为老年代的一部分来进行看待。
G1之所以能建立可预测停顿的时间模型,
是因为G1将Region作为最小回收单位
,这样可以有计划的避免在整个java堆中进行全区域的垃圾收集。
具体的思路就是让G1去跟踪各个Region里面的垃圾堆积的“价值”大小。``价值即回收所获得的的空间大小及回收所需要时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间
(-XX:MaxGCPauseMillis)
默认200毫秒,优先处理回收价值收益最大的那些Region
,这就是名字的由来。这种使用Region划分内存空间
,以及具有优先级的区域回收方式
,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
-
初始标记 STW
仅仅只是标记一下GC Roots能直接关联到的对象,这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以没有额外的停顿
-
并发标记
从GC Roots开始对堆中的对象进行可达性分析,递归扫描整个堆里的对象,找出要回收的对象,这个阶段耗时较长,但可以与用户程序并发执行。
-
最终标记 STW
对用户线程做另一个短暂的停顿,用于处理并发阶段结束后仍遗留下来的最后那些少量的SATB记录(原始快照)
-
筛选回收 STW
负责
更新Region的统计数据
,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间,
这里涉及到存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
G1和CMS的区别
- G1同时回收老年代和年轻代,而CMS只能回收老年代,需要配合一个年轻代收集器。另外G1的分代更多是逻辑上的概念,G1将内存分成多个等大小的region,Eden/ Survivor/Old分别是一部分region的逻辑集合,物理上内存地址并不连续。
- CMS在old gc的时候会回收整个Old区,对G1来说没有old gc的概念,而是区分Fully young gc和Mixed gc,前者对应年轻代的垃圾回收,后者混合了年轻代和部分老年代的收集,因此每次收集肯定会回收年轻代,老年代根据内存情况可以不回收或者回收部分或者全部(这种情况应该是可能出现)。
- G1在压缩空间方面有优势
- G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
- Eden,Survivor,Old区不再固定、在内存使用效率上来说更灵活
- G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象,可驾驭度,G1 是可以设定GC 暂停的 target 时间的,根据预测模型选取性价比收益更高,且一定数目的 Region 作为CSet,能回收多少便是多少。
- G1在回收内存后会马上同时做,合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
- G1会在Young GC中使用,而CMS只能在Old区使用
- SATB 算法在 remark 阶段延迟极低以及借助 RSet 的实现可以不做全堆扫描(G1 对大堆更友好)以外,最重要的是可驾驭度
衡量垃圾收集器的三个指标就是:
内存占用,吞吐量,延迟
在CMS和G1之前的全部收集器,其工作的所有步骤都会产生STW式的停顿
;CMS和G1分别使用增量更新
和原始快照
技术实现了标记阶段的并发
,不会因管理的堆内存变大,要标记的对象变多而导致停顿的时间随之增长。但是对于标记阶段后的处理,仍未得到妥善解决。CMS使用标记-清除
,虽然避免了整理阶段收集器带来的停顿
,但是仍避免不了产生空间碎片
。随着空间碎片不断淤积最终依然逃不过STW
。G1虽然可以按更小的粒度进行回收,从而抑制整理阶段出现时间过长的停顿,但毕竟也还是要暂停的
。
JDK9的默认收集器 是G1
查看GC基本信息,JDK9之前使用-XX:+PrintGC
之后使用 -Xlog:gc
查看GC详细信息,JDK9之前使用-XX:+PrintGCDetails
之后用 -Xlog:gc*
垃圾收集相关参数
-
-XX:UseSerialGC:打开此开关后,使用
Serial+Serial Old
组合进行内存回收 -
-XX:UseParNewGC:使用
ParNew
和Serial Old
组合进行垃圾回收,JDK9后弃用
-
-XX:UseConcMarkSweepGC:使用ParNew + CMS + Serial Old。Serial Old作为CMS出现“Concurrent Mode Failure” 失败后的后背收集器使用。
-
-Xloggc:log/gc.log 指定GC log的位置,以文件输出
-
-XX:NewRatio:新生代(Eden+2S)和老年代(不包含永久区)的比值
例如 -XX:NewRatio=4,表示新生代:老年代=1:4,新生代占堆内存的 1/5
-
UseParallelOldGC:使用Parallel Scavenge + Parallel Old
-
SurvivorRatio:Eden和Survivor的比值 默认为8
-
对象优先在Eden分配
当Eden区没有足够的空间时,会进行一次Minor GC JDK9之前通过-XX:+PrintGCDetails来打印详信息,之后用-Xlog:gc*
-
大对象直接进入老年代
通过
-XX:PretenureSizeThreshold
来设置大于该值即可晋升老年代
,目的是避免在Eden区和两个Survivor区之间来回复制。这里要写字节 不能直接写MB单位。还有该参数只对Serial
和ParNew
两款新生代收集器生效。 -
长期存活的对象进入老年代
对象通常在Eden中诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移入Survivor区域中,并将其年龄设为1岁,每熬过一次Minor GC就加一岁,当年龄达到一定程度时候(默认15岁)就会晋升到老年代中。对象晋升老年代的阈值,可以通过`-XX:MaxTenuringThreshold`设置
GC分代年龄为什么最大为15?
因为Object Header采用4个bit位来保存年龄,4个bit位能表示的最大数就是15
什么情况对象直接在老年代分配
-
分配的对象大小 大于eden space。适合所有收集器。
-
eden剩余空间不足分配,且需要分配对象内存大小不小于eden space总空间的一半,直接分配到老年代,不触发Minor GC。适合-XX:+UseParallelGC、-XX:+UseParallelOldGC,即适合Parallel Scavenge。
-
大对象直接进入老年代,使用-XX:PretenureSizeThreshold参数控制,适合-XX:+UseSerialGC、-XX:+UseParNewGC、-XX:+UseConcMarkSweepGC,即适合Serial和ParNew收集器。
-
动态对象年龄判断
并不是永远要求对象的年龄必须达到
-XX:MaxTenuringThreshold
才能晋升老年代
如果
在Survivor空间中低于或等于某年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。
-
空间分配担保
JPS
:虚拟机进程状况工具 JPS(JVM Process Status Tool)
列出正在运行的虚拟机进程,并显示虚拟机执行的主类(Main Class,main()所在的类)名称以及这些进程的本地虚拟机唯一ID(LVMID)
命令格式:jps [options] [hostid]
options为-l表示 输出主类的全名,如果主类是jar包,则输出jar路径
jstat
:虚拟机统计信息监视工具(JVM Statistics Monitoring Tool)
可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据
命令格式:jstat [option vmid interval [s|ms] [count] ] ]
参数interval和count表示查询间隔时间和次数。如果省略,说明只查询一次
例如:每250毫秒查询一次进程2764垃圾收集的情况,一共查询20次
jstat -gc 2764 250 20
[-gc表示监视java堆,-class表示监视类加载,卸载数量,总空间以及类装载耗时]
[-gcutil表示和-gc基本相同,但输出主要关注已使用空间占总空间的百分比]
jsatck
:java堆栈跟踪工具 JDK9集成到了JHSDB中
用于生成虚拟机当前时刻的线程快照(threaddump或者javacore文件)
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的是定位线程出现长时间停顿的原因,如线程间死锁,死循环,请求外部资源导致的长时间挂起等。线程出现停顿时通过jstack来查看各个线程的调用堆栈,来查看没有相应的线程在后台做什么或者等待什么资源。
命令格式:jsatck [option] vimd
-F:当正常输出的请求不被响应时,强制输出线程堆栈
-l:除堆栈外,显示关于锁的附加信息
-m:如果调用本地方法的话,可以显示C/C++的堆栈
JDK5起,Thread类有一个getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象,可以完成jstack大部分功能。
javap:字节码分析工具
JHSDB:基于服务性代理的调试工具
JConsole:Java监视与管理控制台
内存页签的作用相当于可视化的jstat命令
线程页签的作用相当于可视化的jstack命令
VisualVM:多合一故障处理工具
不需要被监视的程序基于特殊Agent去运行,因此他的通用性很强,对于程序实际性能的影响也很小,使得它可以直接应用在生产环境中。
- 显示虚拟机进程以及进程的配置,环境信息(jps,jinfo)
- 监视应用程序的处理器,垃圾收集,堆,方法区以及线程的信息(jstat,jstack)
- dump以及分析堆转储快照(jmap、jhat)
- 方法级的程序运行性能分析,找出被调用最多,运行时间最长的方法
- 离线程序快照:收集程序的运行时配置,线程dump,内存dump等信息建立一个快照,可以将快照发送开发者处进行Bug反馈
- 其他插件带来的无限可能
调优
-
升级JDK版本的性能变化及兼容问题
类加载时间,编译时间和垃圾收集时间可能占据时间的很大比例
编译时间是指虚拟机的即时编译器编译热点代码的耗时,会被编译成本地代码,提高运行速度。
-Xint禁止编译器运作,强制虚拟机对字节码采用纯解释方式执行。
客户端 C1轻量级编译器 服务端C2重量级编译器
-
编译时间和类加载时间的优化
通过参数-Xverify:none 禁用掉字节码验证过程
-
调整内存设置控制垃圾收集频率
通过
jstat -gccause
查看最近一次垃圾回收
的原因。 -
选择收集器降低延迟
JVM性能调优的步骤
-
分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点;
-
确定JVM调优量化目标;
-
确定JVM调优参数(根据历史JVM参数来调整);
-
依次调优内存、延迟、吞吐量等指标;
-
对比观察调优前后的差异;
-
不断的分析和调整,直到找到合适的JVM参数配置;
-
找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。
常量池
可以比作为Class文件的资源仓库。
常量池主要存放两大类常量:字面量和符号引用。
javap
命令可以查看常量池表
类加载
生命周期:加载,验证,准备,解析,初始化,使用,卸载 七个阶段,其中验证,准备和解析统称为连接
一、加载
根据查找路径找到相应的 class 文件然后导入
二、验证
检查加载的 class 文件的正确性
三、准备
给类中的静态变量分配内存空间
四、解析
是将常量池内的符号引用替换为直接引用的过程
五、初始化
对静态变量和静态代码块执行初始化工作。
初始化就是执行类构造器<·clinit>()的过程
双亲委派模型
站在虚拟机角度,只存在两种不同的类加载器。
1.启动类加载器(BootStrap ClassLoader)
负责加载存放在<·JAVA_HOME·>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的而且是虚拟机能够识别的类库加载到虚拟机的内存中。
2.其他所有的类加载器 独立存在于虚拟机之外,全部继承自抽象类 ClassLoader
- 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
双亲委派模型要求:
除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这些父子类之间不是继承,而是组合
来复用父加载器的代码。
工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求的时候,才会由子加载器去尝试自己完成。
例如Object类,存在于rt.jar中,无论哪一个类加载器要加载这个类,最终都要委派给处于顶端的启动类加载器进行加载。因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
好处:java中的类随着他的类加载器一起具备了一种带有优先级的层次关系
双亲委派的实现
用于实现双亲委派模型的代码十分简单,全部集中在java.lang.ClassLoader的loadClass()中
- 首先 检查请求的类是否已经加载过了
- 如果父类抛出ClassNotFoundException说明父类无法完成加载请求,再调用自身的findClass()来加载
双亲委派作用:
- 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
常用的方法:
- findLoadedClass(),
- loadClass()
- findBootstrapClassOrNull()
- findClass()
- defineClass():把二进制数据转换成字节码。
- resolveClass()
如何自定义类加载器?
自定义类加载器的方法:继承 ClassLoader 类,重写 findClass()方法
在什么情况下需要自定义类加载器呢?
- 隔离加载类。 在某些框架内进行中间件与应用的模块隔离 , 把类加载到不同的环境。比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的 jar包不会影响到中间件运行时使用的 jar 包。
- 修改类加载方式。 类的加载模型并非强制 ,除Bootstrap 外 , 其他的加载并非定要引入,或者根据实际情况在某个时间点进行按需进行动态加载。
- 扩展加载源。 比如从数据库、网络,甚至是电视机机顶盒进行加载。
- 防止源码泄露。 Java代码容易被编译和篡改,可以进行编译加密。 那么类加载器也需要自定义,还原加密的字节码。
破坏双亲委派模型
- 继承ClassLoader覆盖loadClass方法
- 使用线程上下文类加载器
JMM(java 内存模型 Java Memory Model)
JMM并不像JVM内存结构一样是真实存在的。他只是一个抽象的概念,JMM是和多线程相关的,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。
在JMM中,我们把多个线程间通信的共享内存称之为主内存,而在并发编程中多个线程都维护了一个自己的本地内存(这是个抽象概念),其中保存的数据是主内存中的数据拷贝。而JMM主要是控制本地内存和主内存之间的数据交互的。
JAVA对象模型
Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。
三者区别
- JVM内存结构,和Java虚拟机的运行时区域有关。
- Java内存模型,和Java的并发编程有关。
- Java对象模型,和Java对象在虚拟机中的表现形式有关。
volatile<易变的>
当一个变量被定义为volatile之后,他将具备两项特性:
-
保证此变量对所有线程的可见性,即当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。
-
禁止指令重排序优化
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,仍要通过加锁(synchronized、java.util.concurrent中的锁或原子类)来保证原子性:
-
运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
-
变量不需要与其他的状态变量共同参与不变约束。
<普通变量和volatile最大的区别在于>:volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,因此可以说volatile保证了多线程操作时变量的可见性。
-
原子性 可见性 有序性
-
原子性:要么全部成功,要么全部失败
-
可见性:当一个线程修改了共享变量的值的时候,其他线程可以立即得知这个修改。
java内存模型是通过 在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。
除了volatile,还有两个关键字能实现可见性,
synchronize
和final
synchronize
的可见性是由“一个变量执行unlock操作之前,必须先把此变量同步回主内存中”这条规则获得的。而final
的可见性是指: 被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸很危险,其他线程可能通过这个引用访问到初始化一半的对象)那么在其他线程中就能看到final字段的值 -
有序性:java提供了volatile和synchronize来保证线程之间操作的有序性,volition本身就包含了禁止指令重排序的语义,而synchronize则是由“一个变量在同一时刻只能允许一条线程对其进行lock操作”
线程调度是指系统为线程分配处理器使用权的过程,调度方式有两种:
-
协同式
线程的执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另一个线程上去
-
抢占式
每个线程将由系统来分配执行时间、线程的切换不由线程本身来决定。
线程状态:
-
新建(new)
-
运行(Runnable) 包括Runnable和Ready 也就是处于此阶段的线程可能正在执行,也可能等待操作系统为他分配执行时间。
-
无期限等待(Waiting) 不会被分配处理器执行时间,要等待其他线程显示唤醒。
以下方法会让线程陷入无期限的等待:
没有设置Timeout参数的Object::wait()
没有设置Timeout参数的Thread::join()
LockSupport::park()
-
期限等待(Timed Waiting)
以下方法会让线程进入等待状态:
Thread:sleep()
设置了Timeout参数的Object::wait()
设置了Timeout参数的Thread::join()
LockSupport::parkNanos()
LockSupport::parkUntil()
-
阻塞(Blocked)
阻塞和等待的区别是:
阻塞在等待着获取到一个
排它锁
<也就是写锁>,这个时间将在另外一个线程放弃这个锁的时候发生。等待则是在等待一段时间,或者唤醒动作的发生
-
结束(Terminated)
线程安全
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调动作,调用这个对象都可以得到正确的结果,那么就称这个对象是线程安全的。
可以将java中各种操作共享的数据分为:
-
不可变
不可变的对象一定是线程安全的
-
绝对线程安全
-
相对线程安全
-
线程兼容
-
线程对立
实现线程安全
1.互斥同步<也称为阻塞同步>
synchronize 可重入:同一个线程反复进入同步块也不会出现自己把自己锁死的现象
JUC包下的lock:可重入,有一个重入锁(ReentrantLock)是Lock接口最常见的一种实现。
在用法上也和synchronize类似,但是多了一些高级功能:
-
等待可中断
指当持有锁的线程长期没有释放的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
-
公平锁
指多个线程等待同一个线程的时候,必须按照申请锁的时间顺序依次获得锁,而非公平锁则不保证这一点。非公平锁是在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronize中的锁是非公平的。ReetrantLock默认也是非公平锁,但是可以通过带布尔值的构造函数要求使用公平锁。但是用了公平锁会导致ReetrantLock性能急剧下降,明显影响吞吐量。
-
锁绑定多个条件
是指一个ReetrantLock对象可以同时绑定多个Condition对象,在synchronize中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件,但是要和多个条件进行关联的时候,就不得不额外添加一个锁,而ReetrantLock不需要,多次调用newCondition()即可
2.非阻塞同步
互斥同步面临的主要问题就是进行线程阻塞和唤醒带来的性能开销。因此互斥同步也被称为阻塞同步。
非阻塞同步也称为无锁编程
CAS:
-
内存地址 V
-
旧的预值 A
-
新值 B
当且仅当符合A的时候才用B更新V,否则不更新。但是不管更不更新V,都会返回V的旧值,原子操作。
3.无同步方案
泛型和类型擦除的关系
Java泛型的实现方法:类型擦除
Java的泛型是伪泛型:因为,在编译期间,所有的泛型信息都会被擦除掉。
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
Java不能实现真正的泛型,只能使用类型擦除来实现伪泛型。
类型擦除引起的问题:
- 先检查,在编译,以及检查编译的对象和引用传递的问题,java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,在进行编译的
- 类型擦除与多态的冲突和解决方法:桥方法
- 泛型类型变量不能是基本数据类型
- 泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数
编译过程的五个阶段
- 第一阶段:词法分析
- 第二阶段:语法分析
- 第三阶段:词义分析与中间代码产生
- 第四阶段:优化
- 第五阶段:目标代码生成
JVM、Java编译器和Java解释器
-
Java编译器:将Java源文件(.java文件)编译成字节码文件(.class文件),这种字节码就是JVM的“机器语言”。javac.exe可以简单看成是Java编译器。注意,它不会执行代码
-
Java解释器:是JVM的一部分。Java解释器用来解释执行Java编译器编译后的程序。java.exe可以简单看成是Java解释器。注意,它会执行代码
-
JVM是Java平台无关的基础。JVM负责运行字节码:JVM把每一条要执行的字节码交给解释器,翻译成对应的机器码,然后由解释器执行。JVM解释执行字节码文件就是JVM操作Java解释器进行解释执行字节码文件的过程。
-
每次读一代码就将字节码起转换(翻译)为JVM可执行的指令,叫翻译
-
一次性全部将字节码转换为JVM可执行的指令,叫编译
-
自旋锁
如果物理机器上有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行进行,就可以让后面请求锁的线程稍等一会,但是不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,只需要让线程执行一个忙循环(自旋),这就是自旋锁。默认是10次
偏向锁,自旋锁,轻量级锁,重量级锁
通过 synchronized 加锁,第一个线程获取的锁为偏向锁,这时有其他线程参与锁竞争,升级为轻量级锁,其他线程通过循环的方式尝试获得锁,称自旋锁。若果自旋的次数达到一定的阈值,则升级为重量级锁。
需要注意的是,在第二个线程获取锁时,会先判断第一个线程是否仍然存活,如果不存活,不会升级为轻量级锁。