文章目录
一、JVM内存模型
1.JVM与JVM内存结构
(1)JVM构成
(2)Java程序执行流程
Java程序执行流程:java源文件通过编译器javac.exe编译成.class文件(JVM文件),然后通过java.exe执行。JVM可以跨平台,可以不完全受硬件的性能制约。
(3)类加载器和双亲加载机制
①类加载器: 作用是可以任意位置加载。
共三层加载:
第一层是bootstrap,加载系统函数,由JDK实现无法获取到,如String类的str.getClass().getClassLoader()=null;
第二层是平台加载器(JDK1.9)/扩展类(extClassLoader)加载器(JDK1.8);
第三层是自定义类App加载器,如User类的user.getClass().getClassLoader()获取到XXXAppClassLoader@XXX,在其之上.getParent()可以获得到第二次的加载器。
②双亲加载机制:优先将加载任务委派给父类,依次递归。系统类由系统加载器负责,自定义类由其他类加载器负责。可以避免系统类的重复加载,防止API库被篡改。
(4)JVM内存结构(Java运行时数据区)
①方法区:最重要的内存区域,多线程共享,保存了类的信息(名称/成员/接口/父类),反射机制是重要的组成部分,动态进行类操作的实现。
②堆内存(Heap):保存对象的真实信息,该内存涉及到释放问题(GC)。
③栈内存(Stack):线程的私有空间,在每一次方法调用都会存在栈帧,采用先进后出的设计原则。
内存空间一定时,栈越大线程数越少。不需要GC,效率较堆更高。
(1)局部变量表:储存局部参数或形参,允许保留32位的插槽(solt),如果超过了32位的长度就需要开辟两个连续性的插槽(Long,Double)
(2)操作数栈:执行所有方法计算操作。先进后出。
(3)常量池引用:String/Integer类实例
(4)方法出口:方法执行完毕后恢复执行的点
(5)动态链接:如接口通过它调用其实现的方法
比较重要的是局部变量表和操作数栈:如a=1,b=2,c = (a+b)*4。①先把1放入操作数栈的栈底,②然后存到局部变量表中,2也同理,其中局部变量表也记录了顺序(比如1是第一个int值),然后将1和2取到栈中,然后在cpu中进行计算,再把3放回栈中,在拿出3和常量池中的4在cpu中计算,然后放回栈,再把12放到局部变量表中。
④程序计数器:执行指令的顺序编码,如上面例子的①和②,所占比例几乎可以忽略。
⑤本地方法栈:与栈内存作用相似,区别是仅为本地方法服务(native修饰)。
注:运行时数据区中方法区和堆是线程共享的,虚拟机栈、本地方法栈和程序计数器是线程隔离的,每个线程都有自己的栈。
(5)内存溢出诊断和优化
①栈内存溢出(StackOverflowError)
a.栈帧过多:如递归调用没有正确设置结束条件
b.栈帧过大:json数据转换、对象嵌套对象
常见报错1:java.lang.OutOfMemoryError: unable to create new native thread
报错原因:Stack空间不足以创建额外的线程,要么是创建的线程过多,要么是Stack空间确实小了
常见报错2:java.lang.StackOverflowError
报错原因:这也内存溢出错误的一种,即线程栈的溢出,要么是方法调用层次过多(比如存在无限递归调用),要么是线程栈太小。
线程运行诊断1:CPU占用过高:
获取进程编号,查找占用高的进程
top
命令获取线程的进程id,线程id,cpu占用
ps H -eo pid,tid,%cpu | grep 进程号
将线程号转化成16进制的数 :如6626->19E2,获取进程栈信息, 查找‘nid=0X19E2’的线程
jstack 进程id
问题线程的最开始‘#数字’表示出现问题的行数,回到代码查看
线程运行诊断2:死锁问题
程序运行很长时间没有结果有可能是死锁问题
获取进程栈信息
jstack 进程id
查看最后20行左右有无‘Fount one Java-level deadlock’,查看下面的死锁的详细信息描述和问题定位
然后回到代码中定位代码进行解决。
②堆内存溢出(OutOfMemoryError)
常见报错:java.lang.OutOfMemoryError: Java heap space
报错原因:java堆内存不够,或是程序中有死循环
堆内存诊断:
一般使用dump文件分析
方法一:jvm启动时增加的参数生成文件
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/xxx/logs/
方法二:直接导出
jmap -dump:file=文件名.dump [pid]
可使用JDK自带的jvisualvm分析dump文件
③JVM调优三大参数
-Xss:规定了每个线程虚拟机栈的大小(影响并发线程数大小)
-Xms:堆大小的初始值(超过初始值会扩容到最大值)
-Xmx:堆大小的最大值(通常初始值和最大值一样,因为扩容会导致内存抖动,影响程序运行稳定性)
内存抖动:当内存不足时需要进行伸缩曲的控制,内存充足时需要将伸缩区内存释放,会造成额外的计算性能影响。可以设置相应执行参数提升程序的性能,-Xmx:分配的最大初始化内存,-Xms:最大的分配内存,-Xmx减去-Xms等于伸缩区大小,可以将两个值设置成相等大小,提高性能。
2.GC垃圾回收
(1)垃圾回收的条件
垃圾回收发生的地方:运行时数据区中方法区和堆是线程共享的,方法区主要回收废弃的常量和无用类,虚拟机栈、本地方法栈和程序计数器是线程隔离的。所以需要关注的重点关注堆。
判断对象是否需要垃圾回收:
①关注程序计数器,被引用次数为0时可以回收,但无法解决循环引用的情况,故不常用。
②可达性分析算法(hot spot使用):依赖gc roots(包括虚拟机栈中/方法区静态属性或常量/本地方法栈中JNI-java native interface引用的对象),未能与gc roots进行联通的可以被回收
(2)Java的引用类型
①强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
②软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中
③弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象被回收掉之后,再调用get方法就会返回null
④虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)
(3)GC处理流程
①对象实例化需要利用new关键字完成,所有新对象在伊甸园区开辟,伊甸园区内存不足会发生MinorGC(新生代GC回收)。
②若某对象多次MinorGC后还存在,这些对象将进入存活区。存活区有两个,一个负责保存存活对象,一个负责晋升,两个区域可以互相转化,永远都有一个空内存。
③若空间还不够使用,则进行老年代GC回收,执行MajorGC(Full GC,性能很差),如果可以回收空间,则继续进行MinorGC。
④若MajorGC失败,则内存溢出,报OOM异常。
⑤若新创建的对象过大将直接被保存在老年代之中。
(4)GC回收算法
年轻代回收算法:
采用复制清理算法(因为对象存活效率低,故采用这个效率较高的算法)。将内存分成多块,内存使用完后,将活着的对象复制到另一块内存中,存活的内容会保存在老年代之中。HotSpot(当前JVM标准)虚拟机使用了BTP(单一CPU时代所有对象依次保存)/TLAB(拆分为不同的块,依据CPU的核心个数拆分)技术形式进行处理。
老年代回收算法:
采用标记-清除算法。将垃圾标记后一次性清理掉。在标记过程中暂停程序执行,进行可达性分析。。效率低且会产生内存碎片的问题。
采用标记-压缩算法。基于标记-清楚算法,将垃圾标记后全部移动到一端,然后进行清理。
跨代引用问题:
老年代的对象指向了新生代对象,在新生代的垃圾回收时就会误将此对象回收。解决方案–在新生代建立记忆集的指针集合,用来记录老年代区域哪些对象指向了新生代,而不是扫描整个老年代区域。注意记录的是区域,而不是具体的对象(维护成本高),这种精度为卡精度,故把这个记忆集称之为卡表,卡表每一项对应老年代的512个字节。卡表也需要动态维护,hotspot通过写屏障维护卡表,发生引用对象的赋值操作时产生一个环形通知。将维护代码放在写后屏障中。
(5)垃圾回收器分类
垃圾回收器大致分类为串行收集器和并行收集器:串行即单线程的意思。除了比较早的Serial /Serial old垃圾回收器是串行的,其余垃圾回收器都是并行的。
并行收集器中并发标记原理:
需要解决对象消失问题–扫描过程中某对象(1)插入了新的有效引用并(2)断开了原来的有效引用。
解决问题(1):增量更新,记录新增引用,在并发标记结束后重新扫描增量更新的–CMS收集器使用
解决问题(2):原始快照(SATB),记录删除的引用关系,并发结束结束后重新判断被删除的引用有无其他有效引用–G1收集器使用
①Serial /Serial old垃圾回收器:串行收集器。单线程,垃圾回收时其他线程都得停下来。新生代采用复制算法(Serial ),老年代采用标记整理算法(Serial old)。
②ParNew:并行收集器。只负责新生代的垃圾回收,除STW时单线程改成多线程时,其他的与serail一致。
老年代的需要使用serialOld或CMS。对于单核cpu时,serial效率好于parNew。
③CMS(concurrent mark swap)垃圾回收器:并发收集低停顿,缺点是占用cpu资源,无法清除浮动垃圾,若老年代预留内存不够使用时会并发失败(此时serialold会替代cms进行回收),内存碎片问题–没法满足连续对象分配时会发生FULLGC。
使用-XX:UseCMS-CompactAtFullCollection默认开启,可以在fullGC发生时进行内存碎片整理(压缩)。
使用-XX:UseCMSFullGCsBeforeCompaction,默认0,决定Full GC几次后进行压缩
并发标记过程:初始标记(判断对象是否能跟gc roots连接)-并发标记-重新标记(修正并发标记中不准备的点,如对象消失问题)-并发清理。其中初始标记和重新标记需要STW。
④Pararell Scavenge/Parallel old:并行,使垃圾回收达到一个可控制吞吐量的状态。
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
-XX:MaxGCPauseMills控制最大垃圾收集停顿时间,-XX:GCTimeRatio控制吞吐量大小
-XXUseAdaptiveSizePolicy gc自适应调整最合适的停顿时间和吞吐量。
配合Parallel old处理老年代。在注重吞吐量及cpu资源敏感时使用。
⑤G1垃圾回收器:支持大内存/支持多CPU/减少STW(Stop the world,MajorGC标记时间)时间,可以保证并发状态下的程序的执行。与CMS主要区别见并发标记原理。
JDK1.9之后默认使用,1.7-1.8:-XX:+UseG1GC。
配合使用:
Serial+Serial Old
Parallel Scavenge+Paraller Old
ParNew+CMS
注:JDK1.8默认会根据系统的不同选择不同的GC回收策略,单核为串行回收,多核为并行回收。JDK1.9-1.11的默认操作是G1。
二、Java内存模型
1.基本概念
JMM(Java Memory model):是一套多线程读写共享数据时,对数据的可见性,有序性和原子性的规则(并发编程三要素)。
2.volatile关键字
a.可见性(最重要的特性)。对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。
b.原子性。对任意(包括64位long类型和double类型)单个volatile变量的读/写具有原子性。但是类型于a++/ a = b这种复合操作不具有原子性,AtomicInteger(volatile和CAS结合)可以保证原子性。
c.有序性,也就是防止指令重排序。
volatile关键字通过“内存屏障”来防止指令被重排序
内存屏障插入策略:
在每个 volatile 写操作的前面插入一个 StoreStore 屏障(禁止前面的写与volatile写重排序)。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障(禁止volatile写与后面可能有的读和写重排序)。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障(禁止volatile读与后面的读操作重排序)。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障(禁止volatile读与后面的写操作重排序)。
其中StoreLaod屏障,它是确保可见性的关键,因为它会将屏障之前的写缓冲区中的数据全部刷新到主内存中。
3.synchronized、volatile、CAS比较
Sychronized、lock可以解决可见性、原子性和有序性的问题,volatile可以解决可见性和有序性atomic可以解决原子性问题。
(1)synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。
(2)volatile提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS是基于冲突检测的乐观锁(非阻塞)