此文章仅为个人以前通过网上的知识点和资源进行的整理整合,侵删
JVM (Sun:HotSpot,BEA:JRockit,IBM JVM:J9VM)
1.常见问题
- Java8 虚拟机和之前更新变化的区别
- 什么是OOM,什么是栈溢出
- JVM常用调优参数
- 内存快照如何抓取,怎么分析Dump文件
- 谈谈你对类加载器的认识
2. JVM位置
3. JVM结构
3.1 类加载器
名字引用在JVM栈里,具体属性数据在堆里
1.虚拟机自带加载器
2.启动类(根)加载器(主要负责j加载ava.lang.*)
3.扩展类加载器(主要负责加载jre/lib/ext目录下的一些扩展的jar)
4.应用程序加载器
4. 双亲委派机制(保证安全)
沙箱安全机制的扩展
AppClassLoader—>ExtClassLoader—>BOOTLoader
-
类加载器收到类加载的请求
-
将这个请求向上委托给父类加载器,一直向上委托,直到根加载器
-
加载器检查是否能加载这个类
如果根加载器能加载就结束,然后使用当前加载器
否则抛出异常,通知子加载器进行加载
重复step3
为什么设置这种机制?
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
5. Native
native:凡是带了native关键字的,说明java作用范围到达不了,会去调用底层c语言的库
进入本地方法栈,调用本地方法库:JNI
JNI作用:扩展Java的使用,融合不同的编程语言为java所用,最初c/c++(java开发初,c++横行)
在内存中专门开辟了本地方法栈(Native Method Stack),登记native方法
// java程序驱动打印机,Robot()类
// 调用其他接口,Socket,WebService
6. 方法区
static,final,Class,运行时常量池
7. 栈(Stack)
为什么main()方法最先执行,最后结束
栈:栈内存主管程序运行,生命周期和线程同步;线程结束,栈内存释放,不存在垃圾回收机制,一个线程一个栈
栈存什么?:8大基本类型 + 对象引用(地址)+ 实例的方法
栈+堆+方法区交互:
8. 堆(Heap)
一个JVM只有一个堆,堆内存的大小可以调节
堆内存三区域:新生代,老年代,永久代(元数据区1.8之后)
新生区:类诞生和成长,甚至死亡的地方;
- 伊甸区(Eden):所有对象都在此区被new出来
- 幸存区(Survivor)(0(From),1(To)):
永久代是对方法区的一种实现
- 1.6之前:永久代,运行时常量池在方法区;
- 1.7:永久代,常量池在堆(开始去永久代)
- 1.8:元空间(Metaspace)取代永久代
另外还需要注意的是在HotSpot虚拟机中永久带和堆虽然相互隔离,但是他们的物理内存是连续的,即永久代使用JVM分配的内存。而且老年代和永久带的垃圾收集器进行了捆绑,因此无论谁满了都会触发永久带和老年的GC。
元空间在1.8中不在与堆是连续的物理内存,而是改为使用本地内存(Native memory)。元空间使用本地内存也就意味着只要本地内存足够,就不会出现OOM的错误。
9. GC(Garbage Collection)
主要作用域:新生代,永久代
OOM:堆内存溢出
https://blog.csdn.net/laomo_bible/article/details/83112622
9.1 GC的对象
需要进行回收的对象就是已经没有存活的对象,判断一个对象是否存活常用的有两种办法:引用计数和可达性分析。
(1)引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
(2)可达性分析:(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
在Java语言中,GC Roots包括:
-
虚拟机栈中引用的对象。
-
方法区中类静态属性实体引用的对象。
-
方法区中常量引用的对象。
-
本地方法栈中JNI引用的对象。
9.2 GC机制
GC又分为 minor GC 和 Full GC (也称为 Major GC )
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
a.调用System.gc时,系统建议执行Full GC,但是不必然执行
b.老年代空间不足
c.方法区空间不足
d.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
e.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
9.3 GC算法
GC常用算法有:标记-清除算法,标记-压缩算法,复制算法,分代收集算法。
目前主流的JVM(HotSpot)采用的是分代收集算法。
9.3.1 标记-清除算法
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。
优点:
最大的优点是,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。此外,更重要的是,这个算法并不移动对象的位置。
缺点:
它的缺点就是效率比较低(递归与全堆对象遍历)。每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高。没有移动对象,导致可能出现很多碎片空间无法利用的情况。
9.3.2 标记-压缩算法(标记-整理)
标记-压缩法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。
优点:
该算法不会像标记-清除算法那样产生大量的碎片空间。
缺点:
如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。
9.3.3 复制算法
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
注意:
这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。
优点
实现简单;不产生内存碎片
缺点
每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半。
9.3.4 分代收集算法
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。
具体过程:新生代(Young)分为Eden区,From区与To区
当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收。一般来说这时候不是所有的
对象都没用了,所以就会把还能用的对象复制到From区。
这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区,
再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区。
经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。
老年代经过这么几次折腾,也就扛不住了(空间被用完),好,那就来次集体大扫除(Full GC),也就是全量回收。如果Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。所以要合理设置年轻代与老年代的大小,尽量减少Full GC的操作。
9.4 GC回收器
9.4.1. Serial收集器(串行收集器)
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收
-XX:+UseSerialGC
- 新生代、老年代使用串行回收
- 新生代复制算法
- 老年代标记-压缩
9.4.2. 并行收集器
ParNew
-XX:+UseParNewGC(new代表新生代,所以适用于新生代)
- 新生代并行
- 老年代串行
Serial收集器新生代的并行版本
在新生代回收时使用复制算法
多线程,需要多核支持
-XX:ParallelGCThreads 限制线程数量
Parallel收集器
- 新生代复制算法
- 老年代标记-压缩
- 更加关注吞吐量
-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行
-XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行
9.4.3. CMS收集器
- Concurrent Mark Sweep 并发标记清除(应用程序线程和GC线程交替执行)
- 使用标记-清除算法
- 并发阶段会降低吞吐量(停顿时间减少,吞吐量降低)
- 老年代收集器(新生代使用ParNew)
- -XX:+UseConcMarkSweepGC
CMS运行过程比较复杂,着重实现了标记的过程,可分为
- 初始标记(会产生全局停顿)
根可以直接关联到的对象
速度快
2. 并发标记(和用户线程一起)
主要标记过程,标记全部对象
3. 重新标记 (会产生全局停顿)
由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
4. 并发清除(和用户线程一起)
-XX:MaxGCPauseMills
- 最大停顿时间,单位毫秒
- GC尽力保证回收时间不超过设定值
-XX:GCTimeRatio
- 0-100的取值范围
- 垃圾收集时间占总时间的比
- 默认99,即最大允许1%时间做GC
9.5 堆内存调优
- 1.尝试扩大堆内存,看结果
- 2.分析结果,用分析工具
-
-XX:+PrintGCDetails
-
-Xmx2g:JVM最大的堆大小为2g,Xmx默认是物理内存的1/4但小于1G;
-
-Xms2g:JVM启动初始化堆大小为2g,Xms的默认是物理内存的1/64但小于1G。
将-Xms和-Xmx的值配置为一样,可以避免每次垃圾回收完成后对JVM堆大小进行重新的调整。
-
-Xmn512M:堆中的新生代大小为512M
-
-Xss128K:每个线程的堆栈大小为128K
-
-XX:PermSize=128M:JVM持久代的初始化大小为128M
-
-XX:MaxPermSize=128M:JVM持久代的最大大小为128M
-
-XX:NewRatio=4:JVM堆的新生代和老年代的大小比例为1:4
-
-XX:SurvivorRatio=4:新生代Surivor区(新生代有2个Surivor区)和Eden区的比例为2:4
-
-XX:MaxTenuringThreshold=1:新生代的对象经过几次垃圾回收后(如果还存活),进入老年代。如果该参数设置为0,这表示新生代的对象在垃圾回收后,不进入survivor区,直接进入老年代
-
-XX:+UseParallelGC -XX:ParallelGCThread=4
-
-XX:+UseParallelOldGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
-
-XX:+UseParallelGC:使用并行的垃圾收集器,但仅针对新生代有效,老年代仍然使用串行收集器
-
-XX:ParallelGCThread=4:设置并行垃圾回收器的线程为4个,该设置最好与处理器的数目相同
-
-XX:+UseParalleOldGC:配置老年代使用并行垃圾收集器,JDK1.6支持老年代使用并行收集器
-
-XX:MaxGCPauseMillis=100:设置每次新生代每次收集器垃圾回收的最长时间,如果无法满足该时间,JVM会自动调整新生代区的大小,以满足该值
-
-XX:+UseAdaptiveSizePolicy:设置此值后,JVM会自动调整新生代大小以及相应的surivor区的比例,以达到设置的最低响应时间或者收集频率等
-
-XX:UseConcMarkSweepGC
-
-XX:+UseParNewGC
-
-XX:CMSFullGCsBeforeCompaction=5
-
-XX:+UseCMSCompactAtFullCollection
-
-XX:UseConcMarkSweepGC:设置JVM堆的老年代使用CMS并发收集器,设置该参数后,-XX:NewRatio参数失效,但-Xmn参数依然有效
-
-XX:UseParNewGC:设置新生代使用并发收集器,在JDK1.5以上,JVM会根据系统自动设置
-
-XX:CMSFullGCsBeforeCompaction=5:设置5才CMSGC后对堆空间进行压缩、整理
-
-XX:+UseCMSCompactAtFullCollection:打开对老年代的压缩,可能会影响性能,但可以消除堆碎片
9.7 JMM (Java Memory Model)
在Java中,不同线程拥有各自的私有工作内存,当线程需要读取或修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到线程的工作内存的变量副本中,当该线程修改其变量副本的值后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值。
9.7.1 volatile
https://blog.csdn.net/vking_wang/article/details/8574376
https://blog.csdn.net/weixin_30342639/article/details/91356608
原理
- 规定线程每次修改变量副本后立刻同步到主内存中,用于保证其它线程可以看到自己对变量的修改
- 规定线程每次使用变量前,先从主内存中刷新最新的值到工作内存,用于保证能看见其它线程对变量修改的最新值
- 为了实现可见性内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来防止指令重排序。
注意:
- volatile只能保证基本类型变量的内存可见性,对于引用类型,无法保证引用所指向的实际对象内部数据的内存可见性。关于引用变量类型详见:Java的数据类型。
- volilate只能保证共享对象的可见性,不能保证原子性:假设两个线程同时在做x++,在线程A修改共享变量从0到1的同时,线程B已经正在使用值为0的变量,所以这时候可见性已经无法发挥作用,线程B将其修改为1,所以最后结果是1而不是2。
9.7.2 volital内存语义与实现
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
是否能重排序 | 第二个操作 | ||
---|---|---|---|
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | NO | NO |
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volate读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
9.7.3 三大点
-
内存可见性(MESI缓存一致性协议)
可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。
状态 描述 M 修改(Modified) 此时缓存行中的数据与主内存中的数据不一致,数据只存在于本工作内存中。其他线程从主内存中读取共享变量值的操作会被延迟执行,直到该缓存行将数据写回到主内存后 E 独享(Exclusive) 此时缓存行中的数据与主内存中的数据一致,数据只存在于本工作内存中。此时会监听其他线程读主内存中共享变量的操作,如果发生,该缓存行需要变成共享状态 S 共享(Shared) 此时缓存行中的数据与主内存中的数据一致,数据存在于很多工作内存中。此时会监听其他线程使该缓存行无效的请求,如果发生,该缓存行需要变成无效状态 I 无效(Invalid) 此时该缓存行无效 -
禁止指令重排序(内存屏障)
屏障点 描述 每个volatile写的前面插入一个store-store屏障 禁止上面的普通写和下面的volatile写重排序 每个volatile写的后面插入一个store-load屏障 禁止上面的volatile写与下面的volatile读/写重排序 每个volatile读的后面插入一个load-load屏障 禁止上面的volatile读和下面的普通读重排序 每个volatile读的后面插入一个load-store屏障 禁止上面的volatile读和下面的普通写重排序 -
不保证原子性
尽管volatile关键字可以保证内存可见性和有序性,但不能保证原子性。也就是说,对volatile修饰的变量进行的操作,不保证多线程安全。
如对i赋值:i++
- 1.首先获取变量i的值
- 2.将该变量的值+1
- 3.将该变量的值写回到对应的主内存中
9.7.4 synchronized关键字
synchronized 可以保障原子性和可见性。因为 synchronized 无论是同步的方法还是同步的代码块,都会先把主内存的数据拷贝到工作内存中,同步代码块结束,会把工作内存中的数据更新到主内存中,这样主内存中的数据一定是最新的。更重要的是禁用了乱序重组以及保证了值对存储器的写入,这样就可以保证可见性。
解决的问题:
现在可以多个线程对同一片存储空间进行访问,这时存储空间里面的数据叫做共享数据。线程并发给我们带来效率的同时,也带了一些数据安全性的问题,数据安全性是一个很严重的问题,多个线程同时访问同一片数据区,很有可能把里面的数据弄的混乱。 所以Java语言提供了专门机制以解决这种数据安全性问题,有效避免了同一个数据对象被多个线程同时访问,从而导致数据的错乱的问题。
synchronized关键字可以作为函数的修饰符(也就是常说的同步方法)
synchronized关键字可以作为函数内的语句(也就是常说的同步代码块)
synchronized它的作用域默认是当前对象,这时锁就是对象。防止多个线程同时访问这个对象的synchronized方法,如果一个对象有多个synchronized方法,只要一个线程访问了其中的某一个synchronized方法,其它线程就不能同时访问这个对象中其他任何一个synchronized方法了。不同的对象实例的 synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法。
锁对象:synchronized (this),中的this代表着当前对象。
锁class:可以防止多个线程同时访问这个类所创建的对象中的synchronized方法。它可以对这个类创建的所有对象实例起作用。锁class只需要将上面代码中的this,换成“类名.class”就行了
9.7.5 synchronized和voiatile的比较
a)volatile不需要加锁,比synchronized更轻便,不会阻塞线程
b)从内存可见性的角度来讲,volatile的读相当于加锁,volatile的写相当于解锁
c)synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性