admin管理员组

文章数量:1565352

一. 什么是JVM

1.内容:

Java程序的运行环境(Java二进制字节码的运行环境)
好处:
a. 一次编写,到处运行
b. 自动内存管理,垃圾回收功能
c. 数组下标越界检查
d. 多态
比较:
Jvm隔绝与操作系统的联系

2. JVM的作用:

    A. 面试
    B.  理解底层的实现原理
    C. 中高级程序员的必备技能

3.常见的JVM的组成:

4.程序计数器的作用:

(1 程序技术器:

先将java源代码编译成二进制字节码
二进制字节码对应着jvm指令
然后将jvm指令交给解释器
解释器将jvm指令转换成机器码 然后交给CPU执行
程序计数器:将下一条程序的地址记住 供解释器执行完当前的程序取下一条指令

5. 程序计数器的特点:

------------------------------线程私有
----------------------------- 不会存在内存溢出

6. 栈

先进后出

6.1 定义:

Java虚拟机栈
每个线程运行时所需的内存称为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占 的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法(栈顶方法)
垃圾回收是否涉及栈内存???不会因为栈只是调用方法
栈内存太大会使线程数变小
方法内的局部变量是否线程安全:是 (如果局部变量是static类型的就不是了)
线程是不是安全:
如果方法内局部变量没有逃离方法的作用范围,他是线程安全的
如果是局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全

6.2栈内存溢出

 1. 栈帧过多会导致栈内存溢出
 2. 一个栈帧直接导致栈内存溢出

本地方法栈: jvm在调用本地方法时需要一个本地方法栈来调用

7.堆:

7.1 堆(Heap)

通过new关键字穿件对象都会使用堆内存
特点:
他是线程共享的,堆中对象都需要考虑线程安全的 问题
有垃圾回收机制

7.2 堆内存溢出

堆内存诊断的工具:
jps工具
查看当前系统中有哪些Java进程
Jmap工具
查看堆内存占用情况
Jconsole 工具
图形界面的,多功能的监测工具,可以联系监测

8.方法区

方法区是所有Java虚拟机线程中的共享区 file 成员变量 成员方法 构造方法 类的一些数据 运行时常量池
是在虚拟机启动时创建 逻辑上是堆得组成部分
方法区溢出会抛异常

8.1 方法区溢出


场景:
Spring
MyBatis

9. 运行时常量池

二进制字节码(类基本信息 常量池 类方法定义 包含虚拟机指令)
常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名和方法名 参数类型 字面量等信息

运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址

常量池中的信息都会被加载到运行时常量池中
串池是一个hashtable结构的不能扩容
每个字符串对象在运行时才会创建并放入到串池中
String s1=”a”;
String s2=”b”;
String s3=”ab”;//在运行期才确定的用的stringBuilder创建
s1+s2!=s3
但是 s4=”a”+”b”;//javaC在编译期的优化 结果在编译期已经确定为ab
s3==s4
但是在常量池中找ab的操作是一样的
字符串变量也是延迟成为对象的

10.StringTable特性:

a. 常量池中的字符串仅是符号,第一次使用的时候才会变成对象
b. 利用串池的机制,来避免重复创建字符串对象
c. 字符串变量拼接的原理是StringBuilder(1.8)
d. 字符串常量拼接的原理是编译期优化
e. 可以使用intern方法,主动将串池中还没有的字符串对象放入串池

1.8将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入到串池中,会把串池中的对象返回
1.6 将这个字符串对象放入串池,如果有则不会放入,如果没有则复制一份在放入到串池中,会把串池中的对象返回

10.1 StringTable 垃圾回收机制:

底层是个hash表
StringTable中内存不够的时候会自动触发垃圾回收机制

10.2 StringTable的性能调优

将stringTable的数量尽可能往大的调这样hash分布就会均匀 运行时查找StringTable的中的值也会变快

什么情况下会用到StringTable
当字符串对象巨多的时候,会进行使用,如果有很多字符串对象创建时,占用巨多的内存,将字符串数据放入常量池中,会自动将重复的数据进行删除这样会使内存占用大量减少。

11. 直接内存

直接内存是操作系统的内存
常见于NIO操作时,勇于数据缓冲区
分配回收成本较高,但是读写性能较高
不受JVM内存回收管理
Unsafe 对象分配内存空间
直接内存是通过Unsafe对象来进行管理的 分配和释放的

使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收了,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory方法

二. 垃圾回收

1. 垃圾会搜

1.1 如何判断对象可以垃圾回收:

a. 引用计数法:
被引用计数器+1 被释放则-1
但是如果两个对象相互引用就会造成bug不会被回收

b. 可达性分析算法
确定根对象:那些不能够被垃圾回收的对象就是根对象
进行对象扫描 如果被根对象引用则不会被回收 如果未被引用则就会被回收

1.2 可达性分析算法:

Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
扫描堆中的对象,看是否能够沿着GC Root对象为七点的引用链找到该对象,找不到则表示可以被回收
那些对象可以作为GC Root?
a. System Class 系统类(核心的)
b. Native Stack 操作系统调用的java对象
c. Busy Monitor(同步锁中的被加锁的对象)
d. 活动线程中使用中的对象
栈帧类的对象
局部对象所引用的对象
方法参数所引用的对象

1.3 四种引用:

a. 强引用:平常所引用的对象都是强引用
特点:只要沿着GC Root的引用链能找就不会被垃圾回收
全部断开才能被垃圾回收
b. 弱引用: 当没有强引用 引用时,垃圾回收的时候必定会垃圾回收弱引用引用的对象
c. 软引用: 没有被强引用的时候就会只能软引用就会可能被回收(内存不够时就会被回收)
d. 虚引用(必须配合引用队列): 当进行垃圾回收机制的时候stringBuffer下的直接内存空间不会被垃圾回收但是stringBuffer已经被回收 所以配合引用队列 将直接内存空间定时释放(直接内存的地址在创建的时候就会传递给虚引用Cleaner 然后用Unsafe.freeMemory进行释放)
e. 终结器引用(必须配合引用队列):当没有强引用的时候准备进行垃圾会收的时候将终结器引用放入引用队列中,然后当调用了finallize方法被调用的时候,下一次进行垃回收的时候就会激进行拉圾回收,用一个优先级特低的线程finallizeHandle进行垃圾回收(当被调用完成下一次就会被垃圾回收机制进行回收)

a. 强引用:
只有所有的GC Root对象都不通过(强引用)引用该对象,该对象才能被垃圾回收
b. 软引用:


仅有软引用 引用该对象时,在垃圾回收后,内存仍然不足时会再次触发垃圾回收机制,回收软引用对象
可以配合引用队列来释放软引用自身

c. 弱引用
仅有弱引用 引用该对象时,在垃圾回收时,无论内存是否充足, 都会回收弱引用对象
可以配合引用队列来释放弱引用自身

d. 虚引用:(必须配合引用队列来使用)
主要配合Byte Buffer使用,被引用对象回收时,会将虚引用入队,由reference Handle线程调用虚引用相关方法释放直接内存
e. 终结器引用
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(别引用对象暂时没有被回收),再由Finalizer 线程通过终结器引用找到被引用对象并调用它的finalize 方法,第二次GC时才能回收被引用对象

2. 垃圾回收算法:

a. 标记清除

b. 标记整理

c. 复制

2.1 标记清楚算法:


优点:速度快
缺电:容易产生内存碎片

2.2 标记整理:

首先查看未被GC Root引用的内存

优点:没有内存碎片
缺点:对象在整理过程中进行位置的移动,效率变慢

2.3 复制算法:

将内存区划分为大小相等的两个区域,


优点:不会产生多余的内存碎片
缺点:占用了双倍的内存空间块

2.4 分代回收:

分为新生代和老年代
新生代又分为:伊甸园 幸存区from 幸存区To
用完丢弃:新声代
可能会被重复使用更有价值 的对象:老年代

  • 老年代:等到堆内存空间占用完成时才会清理
    工作机制:
    ------------当创建一个对象的时候会使用伊甸园中的空间,当伊甸园快满的时候,在创建对象的时候会触发新时代垃圾回收(Minor GC),会采用可达性分析算法,采用复制算法 当存活区放入幸存区To,将幸存区的对象成活寿命+1,然后交换幸存区from 和幸存区to的位置
    -------------当一个对象的寿命已经超过设定的阈值了,就可以晋升到老年代中去
    -------------当新生代和老年代的内存空间已经占满的时候,先触发新生代垃圾回收机制,如果内存依旧不够,就会触发老年代垃圾回收机制(Full GC)
    ------------如果老年代进行垃圾回收后内存空间依旧不足,那么就会触发out heat Memory 来进行内存整理
    Minor GC会引发stop the world 当进行垃圾回收的时候,先暂停其他的用户线程,由垃圾回收线程进行垃圾回收,当回收完毕,其他用户的线程继续执行
    老年代垃圾回收机制会使用 标记+清除/标记+整理

相关VM参数

3 垃圾回收器

a. 串行
单线程的
堆内存较小的时候使用 适合个人电脑
b. 吞吐量优先
多线程
堆内存较大 多核cpu
(单位时间呢STW的时间最短)
c. 响应时间优先
多线程
堆内存较大 多核cpu
(尽可能让STW的单次时间变短)

3.1 串行垃圾回收器


3.2 吞吐量优先

3.3 响应时间优先


用户线程会在初始标记和重新标记的时候进行暂停 其余时间不会对用户线程产生影响
CMS但是有一个缺陷:当内存碎片过多时,会导致并发失败,就会产生一个单线程的碎片整理,为接下来的操作做准备,这样时间就会直线上升

4 Garbage First垃圾回收器(Garbage One)jdk9.0

使用的是标记整理算法
但是两个region之间使用的是复制算法
适用场景:
同事注重吞吐量和低延迟
超大堆内存,会将堆划分为多个大小相等的Region(区域)1 2 4 8MB
整体上是标记+整理算法,两个区域之间的复制算法
相关的JVM参数

第一个参数是启动开关
第二个参数是设置区域的大小
分为三个阶段:

  • a. Young Collection 新生代回收
  • b. Young Collection+ Concurrent Mark 新生代收集的同时会进行标记
  • c. Mixed Collection 混合收集
    新生代回收的时候:
    会STW
    Young Collection+CM
    在Young GC时会进行 GC Root的初始标记
    老年代占用对空间比例达到阈值时,进行并发标记(不会进行STW)由下面的JVM参数进行设定
    Mixed Collection
    会对E S O进行全面垃圾回收
    最终标记(Remark)会STW
    拷贝存货(Evacuation)会STW
    会挑选老年区回收价值比较高的区域进行回收

5. Full GC

SerialGC
a. 新生代内存不足发生的垃圾收集—minor gc
b. 老年代内存不足发生的垃圾收集—full gc
ParallelGC
a. 新生代内存不足发生的垃圾收集—minor gc
b. 老年代内存不足发生的垃圾收集—full gc
CMS
a. 新生代内存不足发生的垃圾收集—minor gc
b. 老年代内存不足
G1
a. 新生代内存不足发生的垃圾收集----minor gc
b. 老年代内存不足
① 老年代内存占堆内存的占比达到45%及以上的时候会触发标记的阶段和混合升级的阶段 在回收的速度比产生垃圾的速度快的时候是并发垃圾收集的阶段,,,,,
② 当垃圾回收速度没有产生垃圾的速度快的时候,并发收集失败 这时候就会退化成串行收集,此时就称作FullGC

6. Young Collection

新生代回收的跨代引用(老年代引用新生代)问题
找根对象的时候 采用卡表的技术,老年代中有对象引用了新生代中的对象,就称该对象是赃卡 这时候GCRoot遍历的时候只需要遍历赃卡区域

在每次遍历完更新post-write barrier + dirty card queue

7. Remark

(重标记)

在扫描完白色的时候白色又被强引用,为了防止被误收回,进行remark,当对象的引用状态发生改变时,JVM会给对象加个写屏障,只要状态发生改变,写屏障的代码就会执行,然后放入一个写屏障的队列中,白色的状态发生改变,在并发阶段结束,这时候STW让其他的进程停止,进行再次重新标记进程进行再次检查,这样就不会被误当成垃圾了

8. JDK 8u 20字符串去重

优点:节省大量内存
缺点:略微多占用了cpu的时间,新生代回收时间略微增加
-XX: UseStringDeduplication

将所有的新分配的字符串放入一个队列
当新生代回收时,G1并发检查是否有字符串重复
如果他们值一样,就让他们引用同一个char[]
注意:
与String.intern()不一样
String.intern()关注的是字符串对象
而字符串去重关注的是char[]
在JVM内部,使用了不同的字符串表

9. Jdk 8u 40并发标记 类卸载

所有对象都经过并发标记后,就可以知道那些了不再被使用,当一个类加载器的所有类都不在使用所加载的所有类

10. JDK 8u60 回收巨型对象

一个对象大于region的一半时,称之为巨型对象
G1不会对巨型对象进行拷贝
会输是被优先考虑
G1会跟踪老年代所有的incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时被处理掉

11. JDK 9 并发标记起始时间的调整

并发标记必须在对空间占满前完成,否则退化为FullGC
JDK 9之前需要使用—xx:InitiatingHeapOccupancyPercent
JDK 9可以动态调整
–xx:InitiatingHeapOccupancyPercent 用来设置初始值
进行数据采样并动态调整
总会添加一个安全的空档空间
FullGC是多线程的 但是应该尽量避免FullGC发生的几率

12. JDK9 更高效的回收

250+增强
180+bug修复

三. 垃圾回收调优

预备知识:
掌握GC相关的VM参数,会基本的空间调整(在Oracle的官网上有)
掌握相关工具
明白一点:调优跟应用 环境有关,没有放之四海而皆准的法则

1. 调优的领域:

a. 内存
b. 锁竞争
c. Cpu占用
d. Io

2. 确定目标

a. 低延迟还是高吞吐量,选择合适的回收器
b. CMS G1 ZGC
c. ParallelGC
d. Zing(零停顿 超大内存)

3. 查看FullGC前后的内存占用,考虑下面几个问题:

a. 数据还不是太多

b. 数据表示是否太臃肿
对象图
对象大小
c. 是否存在内存泄露
Static Map map=
软引用
弱引用
第三方缓存实现

四.新生代调优

1新生代的特点:

a. 所有的new操作的内存分配非常廉价
TLAB thread-local allocation buffer(每个 线程的私有区域自动分配)
b. 死亡对象的回收代价是零
c. 大部分对象用过即死
d. Minor GC的时间远远低于Full GC

2新生代的调优方法:

a. 可以将新生代的空间变大
但是越大越好吗??
如果新生代的内存空间太大 那么老年代的内存空间就会变小,那么一定程度上会使垃圾回收的Full GC变的更加频繁
新生代能容纳所有【并发量*(请求-响应)】的数据
b. 幸存区大到能保留【当前活跃对象+需要晋升对象】
c. 晋升阈值配置得当,让长时间存货对象尽快晋升

五.老年代调优

1. 以CMS为例

CMS的老年代内存越大越好)(为了防止浮动垃圾产生对CMS造成影响)
先尝试不做调优,如果没有Full GC 那么已经,,,,否则先尝试调优新生代
观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3

六. 类文件结构

  • 1 . 魔数
    0—3字节,表示它是否是class类型的文件
    1. 版本
      4—7字节,表示类的版本 00 34(52)表示java8

1 常量池:

8—9字节,表示常量池的长度,00 23(35)表示常量池中#1~#34项,注意#0不计入,也没有值














2 访问标识和继承信息


3. Field信息

4. Method信息


5. Main方法

七.Java的内存模型(JMM)

含义:简单的来说:JMM定义了一套在多线程读写共享数据时(成员变量 数组)时,对数据的可见性,有序性和原子性的规则和保障

1 Synchronized(同步关键字)既可以保证原子性也可以保证可见性

语法:
Synchronized(对象){
要作为源自操作的代码
}
用synchronized解决并发问题

首先 当一个线程需要时间片时,如果没有其他线程进行争抢,就会进入Owner,然后当有线程在Owner中其他需要争抢时间片的线程需要在EntryList进行等待(阻塞),当Owner区的线程执行完毕,会通知EntryList的线程,然后EntryList中的线程进行争抢进入Owner中。

2. 可见性

2.1 退不出的循环


跳不出来为什么呢??
a. 初始状态,t线程刚开始从主内存读取了run的值到工作内存

b. 因为t线程要频繁从主存中读取run的值,JIT编译器会将run的值缓存到自己工作内存的高速缓存中去,减少对主存中run的访问提高效率

c. 1秒之后,main线程修改了run的值,并同步至主存,而t是在自己工作区的高速缓存中读取的这个值,永远是旧值,所以退不出来

2.2 退不出的解决办法:

Volatile(易变关键字)
他可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作区的高速缓存中查找变量的值,必须到主存中获取变量的值,线程操作volatile变量都是直接操作主存

前面的例子体现的实际就是可见性,他保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用一个写线程,多个读线程的情况
注意:
Synchronized语句块既可以保证代码块的原子性,同时保证代码块内变量的可见性,但是缺点是synchronized属于重量级操作,性能相对更低
如果在前面的示例的死循环加入system.out.println()会发现即使不加volatile修饰符,线程t也能正确看到对run变量的修改了。(因为println底层是用synchronized实现的)

3. 有序性

3.1 诡异的结果:



还有可能是0先执行线程二的第一条实用语句,然后执行线程一的第一条
这种现象叫做指令重排,是JIT编译器在运行时的一些优化

3.2 用volatile修饰的变量就可以禁用指令重排

3.3 有序性理解:

在同一个线程内,JVM会在不影响正确性的前提下,可以调整语句的执行顺序

可以看到,至于是先执行i还是先执行j,对最终的结果不会产生影响,所以上面的代码真正执行的时候是

这种特性称之为指令重排,多线程【指令重排】会影响正确性,单例模式:

以上实现的特点:
懒惰实例化
首次使用getInstance()才使用synchronized加锁,后续使用时无需加锁
但在多线程环境下,上面的代码是有问题的

3.4 happens-before

Happens-before规定了那些写操作对其他线程的读操作可见,它是可见性与有序性的一套规则的总结
线程解锁m之前对变量的写,对于接下来对m加锁的其他线层对该变量的读可见

线程对volatile变量的写,对接下来其他线程读该变量的读可见

线程start前对变量的写,对该线程开始后对改变量的读可见

线程结束前对变量的写,对其他线程得知他结束后的读可见(比如其他线程调用t1.isAlive()或者t1.join()等待他结束)

线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后变量的读可见(t2.interrupte或t2.interrupted)

对变量默认值(0,false,null)的写,对其他线程对该变量的读可见
具有传递性 如果x hb->y 并且y hb->z 那么有x hb->z
变量指的是成员变量或者静态成员变量

4. CAS与原子类

含义: CAS即Compare and Swap 它体现的是一种乐观锁的思想,比如对多个线程要对一个共享的整型变量执行+1操作:

获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁并发,适用于竞争不激烈,多核CPU的场景下
a. 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
b. 但事实如果竞争激励,可以想到充实必然频繁发生,反而效率受影响

4.1 CAS底层实现:

CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令

4.2 乐观锁和悲观锁

CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算修改了也没关系,自己再重试
Synchronized是基于悲观锁的思想:最悲观的估计,得防着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁你们才有机会

4.3 原子操作类

Juc(java.util.concurrent)中提过了原子操作类,可以提供线程安全的操作,例如 : AtomicInteger、 AtomicBoolean等,他们底层就是采用CAS技术+volatile来实现的
可以使用AtomicInteger改写之前的例子

5 synchronized优化

Java HotSpot虚拟机中,每个对象都有对象头(包括class指针和Mark Word)Mark Word平时存储这个对象的哈希吗,分代年龄 ,当加锁时,这些信息就根据情况被替换为标记位,线程锁记录指针,重量级锁指针、线程Id等内容

5.1 轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间 是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
比如:学生(线程A)用课本占座,上了半节课,出门了(CPU时间),回来一开发现课本没有变,说明没有竞争,继续上它的课
如果这个期间有其他学生(线程B来了),会告知(线程A)有并发访问,线程A随机升级重量级锁的流程
而重量级锁就是没用课本占座那么简单了,可以想象线程A走之前,把座位用一个铁栅栏围起来,假设有两个方法同步块,利用同一个 对象加锁
每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word


5.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上那个了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁


5.3 重量级锁的优化(自旋优化 就是阻塞前的一种状态)

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会搞,就多自旋几次;反之,就会少自旋甚至不自旋,总之比较智能
a. 自旋会占用CPU的时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
b. 好比等红灯时汽车还不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)
c. Java7 只后不能控制是否开启自旋功能

5.4 偏向锁的优化

轻量级锁在没有竞争时(就是自己这个线程),每次重入仍然 需要执行CAS操作,java 6 中引入了偏向锁来做进一步的优化;只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS
a. 撤销偏向需要将持锁线程升级为轻量级锁,这个过程线程需要暂停(STW)
b. 访问对象的hashCode也将撤销偏向锁
c. 如果对象虽然被多个线程访问,但是没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重置对象的Thread ID
d. 撤销偏向和冲偏向都是批量进行的,以类为单位
e. 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的
f. 可以主动使用-XX-UserBiasedLocking禁用偏向锁

5.5 其他优化

  1. 减少上锁的时间
    同步代码块中尽量短
    2 减少上锁的粒度

本文标签: 详解JVMJava