JVM的构成
类加载子系统(负责将类读取到内存,校验类的合法性,对类进行初始化)
运行时数据区(负责存储类信息,对象信息,以及执行计算的区域)
执行引擎(负责从指定地址对应的内存中读取数据然后执行,同时还自带GC操作)
本地库接口(负责Java语言与其它语言之间进行通讯)
![](https://i-blog.csdnimg.cn/blog_migrate/147b41d175fb89a0c10c42d1370b24c2.png)
类加载子系统
主动加载:访问本类成员或方法时触发的类的加载。
被动加载:访问本类对应的父类属性、方法时,本类属于被动加载,父类属于主动加载。
类加载器
BootStrapClassLoader(负责加载基础类库中的类,例如Object,String,....)
ExtClassLoader(负责加载扩展类库中的类ext/...)
AppClassLoader(负责加载classpath路径下我们自定义的类)
自定义ClassLoader(可以指定自己要加载的路径或加载方式),所以即使是同一个类,但是它的类加载器不同,生成的字节码对象也可以不同。场景:
指定加载源头?(例如从数据库中读取类)
保证类的安全?(例如可以在类编译时加密,加载时解密)
打破双亲委派模型?(对自己写的类不想使用双亲委派机制了)
class SimpleClassLoader extends ClassLoader{
...
}
![](https://i-blog.csdnimg.cn/blog_migrate/b12c3de940dec231253bc43ecdf88be7.png)
双亲委派类加载模型
双亲委派可以简单理解为依次向上询问类是否已经加载过,假如加载过则无需再次加载。假如没有加载过则从顶层(BootStrapClassLoader)向下依次尝试是否可以加载此类,假如可以则对类进行加载。
通过这种机制可以更好保证一个类在内存中只被加载一次,例如java.lang.Object这个基础,类库中的类不需要反复加载。
弊端是假如不同项目部署到了同一个web服务中,但是不同项目中有包名、类名相同的类(内容不同),这样可能会导致有一个类不会被加载。假如要想解决这个问题可以自己定义类加载规则,但是无论你怎么定义,建议基础类库还是要使用双亲委派方式进行加载。
![](https://i-blog.csdnimg.cn/blog_migrate/5bfde1c1f50b5e9ff67225dafa0399aa.png)
类加载的基本步骤
查找类(例如从指定路径找到包名+类名对应的文件)
读取类(通过字节输入流对类进行读取)
校验类(对内存中读取到的类信息进行校验、默认初始化等)
创建字节码对象(java.lang.Class)
![](https://i-blog.csdnimg.cn/blog_migrate/c1c9b01f5fbfaf0e39793da6d8d9448c.png)
触发类加载的时机
直接通过类加载器(ClassLoader)的loadClass去加载。
基于Class.forName("包名.类名")方式去加载。
直接访问类的属性,方法,构建类的对象。
运行时数据区
方法区(Method Area): 存储类的字节码信息、常量池。
堆区(Heap Area): 存储对象
Java方法栈(Stack Area): 所有方法运行时,会创建一个栈帧对象,然后进行入栈
本地方法栈(Stack Area):用C语言写的,方法执行时候,会进入本地方法栈
程序计数器(Pc Register): 用于记录当前线程要执行的下一条字节码指令的地址
![](https://i-blog.csdnimg.cn/blog_migrate/69eaefb93be999135e8c2ea20aec8080.png)
方法区
方法区是逻辑上一种定义,是一种规范,可被所有线程所共享,不同JVM对方法区的落地实现可能不同,例如在JDK7中方法区称之为持久代,在JDK8中方区叫元空间(Metaspace),并且这个元空间,可以是JVM堆外的一块内存,不占用操作系统为JVM分配的内存。
包含了.class字节码文件(包括静态变量、所有方法)
堆
Java中的堆是用于存储对象的一块区域,可被所有线程所共享。这块区域又可以分为年轻代和老年代,年轻代又分伊甸园区和2个幸存区(有一块区域始终是空的)。
包含了new出来的对象(包括实例变量、数组的元素)
对象的内存分配过程
编译器通过逃逸分析(JDK8已默认开启)判定对象是在堆上分配还是在栈上分配。逃逸的在堆上,未逃逸的栈上
假如确定是在堆上分析,则可首先选择TLAB区,检测这块区域是否可以存储这个对象,可以则存储。
假如TLAB区无法存储新创建的对象,则可以考虑在TLAB之外的Eden区加锁分配。
如果Eden区无法存储对象,则执行Yong GC (Minor)-年轻代的垃圾回收
假如Eden区执行Yong GC之后,仍然不足以存储对象,则直接分配老年代。
假如老年代也不可以存储这个对象,则执行Full Gc,这个过后还不能存储则抛出异常。
![](https://i-blog.csdnimg.cn/blog_migrate/340a0fbdc82deafd085a61275ffcef92.png)
设置年轻代和老年代主要是为了减少GC的空间范围,提高GC效率。
当伊甸园区设置的比例比较小时候,因为我们创建的对象,大部分是要存储在伊甸园区。假如伊甸园区比较小,可能会增加GC的频率。进而影响系统的执行性能。
当年轻代中幸存区设置的比例比较小时候,年轻代的伊甸园区对象越来越多时,会启动Yong GC,此时伊甸园区的对象就要拷贝到幸存区。假如这个幸存区比较小,无法存储从伊甸园区拷贝过来的幸存对象,此时这些对象就可能直接分配到老年代,就会导致老年代对象越来越多,触发老年代GC的频率就变高,老年代的GC一般为FullGC,这个GC的时间会比较长,会影响系统的吞吐量和执行效率。伊甸园区与两个幸存区的比例通常是8:1:1
调优中推荐初始堆大小和最大堆大小要设置为一样,这是为了避免程序在运行过程中,因对象的多少或GC后内存的变化,进而导致的内存大小的调整,这个内存大小调整的过程可能会带来更大的系统开销。
不经过年轻代直接存储到老年代的场景
创建的对象比较大,年轻代没有空间存储这个对象。
经过多次GC,没有被回收的对象,随着年龄的增加可能会移动到老年代。
随着技术的进步,对象都是分配在堆上这个说法不准确了,对象可以分配在栈上了(小对象、未逃逸可以分配在栈上)。
逃逸分析:逃逸分析本质上是一种数据分析算法,基于这个算法判定对象是否发生了逃逸,未逃逸的对象可以直接分配在栈上、也可以执行标量替换。这样可以有效减少对象在堆上的分配,进而减少阻塞、GC频率,提高执行效率。
标量替换是一种将对象打散(将对象中成员以局部变量方式进行设计)分配在栈上的技术,减少对象在堆中创建次数,进而降低GC频率,提高其性能。
Java方法栈
Java中每个线程都有一个虚拟机栈(Java方法栈),每个方法的执行都会对应着一次入栈(Push)和出栈(Pop)操作,栈中的元素为一个一个的栈帧(Stack Frame)对象,这个栈帧的构成主要有如下几部分:
操作数栈(用于执行运算)
局部变量表(用于存储方法内部的局部变量)
方法的返回值(存储方法的返回值)
动态链接(方法中要访问的一些常量池数据,要调用的方法,都会对应一个链接)
其它信息
方法运行示例图
![](https://i-blog.csdnimg.cn/blog_migrate/062eaf6cbcd429e004608d264d39dcaa.png)
![](https://i-blog.csdnimg.cn/blog_migrate/3d83d1e0308c26ff1a8ef72e5928a61c.png)
程序计数器
程序计数器用于记录当前线程要执行的下一条指令的偏移量地址,每个线程都有一个程序计数器,这个计数器也是所有内存中唯一一个不会出现内存溢出的区域。
内存溢出
内存中剩余的内存空间不足以分配给新的内存请求,此时就会出现内存溢出。内存溢出可能会导致系统崩溃,具体可能导致内存溢出的原因有:
创建的对象太大。(堆内存溢出)
创建的对象太多了,又有大量的内存泄漏。(堆内存溢出)
方法区的类太多了,没有足够的空间存储一些新的类型了。(方法区内存溢出)
方法出现了无限递归调用,可能会导致栈内存溢出。(栈内存溢出)
内存泄漏以
程序中的对象在使用完毕后,对象占有的内存空间,没有得到及时的释放,一直占用着内存空间,这个现象就称之为内存泄漏。常见内存泄漏可能有:
缓存使用不当(例如缓存中对对象的引用都是强引用)
内部类的使用不当(例如实例内部类会默认保存外部类引用)
大量的IO,链接操作没有得到及时关闭。
大量的使用static变量(这样的变量的生命周期不依赖于变量所在类对象)
四大引用
Java中为了更好的控制对象的生命周期,提高对象对内存的敏感度,设计了四种引用类型,按其在内存中的生命力强弱,可分为强引用、软引用、弱引用、虚引用。其中,强引用引用的对象生命力最强,其它引用引用的对象生命力依次递减。JVM的GC系统被触发时,会因为对象的引用不同,执行不同的回收逻辑。
GC系统
GC(Garbage Collection)称之为垃圾回收,在JVM的执行引擎中自带这样的一个GC系统,此系统会按照一定的算法对内存进行监控和垃圾回收。
判定垃圾对象
引用计数法:每个对象都一个引用计数器,只要有引用引用着这个对象,这个计数器就会加1,没有引用引用计数器的值就是0,缺陷是当出现循环引用的时候无法回收
可达性分析:从一些GC Root对象可以无法找到这个对象,此对象就是垃圾.GC Root对象包括:
类变量、常量直接引用的对象
实例变量直接引用的对象
局部变量、参数变量直接引用的对象
GC算法
标记清除:扫描内存,对活着的对象进行标记,再次扫描内存,对未标记对象进行清除
![](https://i-blog.csdnimg.cn/blog_migrate/16ac8e7b17f6ce9e6ddd8fb93015f3b7.png)
标记复制:扫描内存,将活着对象标记同时拷贝到一块空闲区,然后将原有内存全部清空
![](https://i-blog.csdnimg.cn/blog_migrate/28449904e678ee94af7e0317ff22e9fd.png)
标记整理:扫描内存,将活着对象向一侧移动,然后将边界外的内存进行清空
![](https://i-blog.csdnimg.cn/blog_migrate/ce8eee1c233d9cfdbfb653ca63d7eb66.png)
GC时的线程策略
串行(整个GC过程只有1个线程执行)
并行(允许多线程利用多核CPU优势执行并行GC)
并发(允许GC线程和业务线程并发执行)
垃圾回收器
-XX:+PrintCommandLineFlags//查看垃圾回收器
Serial收集器
内部只使用一个线程进行垃圾回收,不能执行并行化(不能充分利用多核CPU优势)
GC时所有正在执行的业务的线程都要暂停(Stop The World - STW)
新生代使用标记复制算法,老年代使用标记整理算法。
-XX:+UseSerialGC//时候用Serial收集器
![](https://i-blog.csdnimg.cn/blog_migrate/d14a5f12b67206c96db5f561acdedbff.png)
Parallel收集器
内部有多个线程进行垃圾回收,可以利用多核CPU优势进行并行GC操作,可以减少业务暂停时间。
GC时所有正在执行的业务线程都要暂停(Stop The World - STW)
新生代使用标记复制算法,老年代使用标记整理算法。
-XX:+UseParallelGC//使用Parallel收集器
![](https://i-blog.csdnimg.cn/blog_migrate/a17e9556a0f673989a0ac034860b663e.png)
CMS收集器
内部有多个线程进行垃圾回收,可以利用多核CPU优势并行GC操作,可以减少业务暂停时间。
用户业务线程和GC线程可以并发执行。
新生代使用标记复制算法,老年代使用标记清除算法(不整理内存,响应速度会更快)。
-XX:+UseConcMarkSweepGC//使用CMS收集器
![](https://i-blog.csdnimg.cn/blog_migrate/f3a4e14922d854248cc51c710c2786e4.png)
G1收集器
工作于服务器模式,主要面向多核、大内存的服务器应用。
整个堆不再分成连续的年轻代和老年代,而是划分为了多个小堆区。
GC时不会每次收集整个堆,而是以增量方法进行GC操作,每次只是对一个小堆区进行GC。
可以在吞吐量和响应时间上达到一种相对的平衡。
年轻代使用标记复制算法,老年代使用标记整理算法。
-XX:+UseG1GC//使用G1收集器
![](https://i-blog.csdnimg.cn/blog_migrate/312e1bc7bafd5e3626141a06a74bcdfa.png)
JVM调优策略
更换CPU,内存; 调内存大小、比例参数、调整GC收集器
常用参数
检查类加载
-XX:+TraceClassLoading
方法区参数配置
-XX:MetaspaceSize
-XX:MaxMetaspaceSize
常用堆参数配置
-Xms2048m(设置初始堆大小为2048m)
-Xmx2048m(设置最大堆大小为2048m)
-Xmn1g(设置年轻代大小为1g)
-XX:NewRatio=4(设置年轻代与老年的比例大小)
-XX:SurvivorRatio=4(设置年轻代中的Eden区与Survivor区比值,这里的4表示4:1:1)
-XX:MaxTenuringThreshold=15(年轻代对象转换为老年代对象最大年龄值,默认值15)
常用栈参数配置
-Xss128k(设置每个线程的栈大小)
GC日志参数配置
-XX:+PrintGC (GC简单日志)
-XX:+PrintGCDetail (GC详细日志)
垃圾回收器参数配置
Serial垃圾收集器(新生代)
开启:-XX:+UseSerialGC
关闭:-XX:-UseSerialGC
Parallel垃圾收集器(老年代)
开启 -XX:+UseParallelGC
关闭 -XX:-UseParallelGC
CMS垃圾收集器(老年代)
开启 -XX:+UseConcMarkSweepGC
关闭 -XX:-UseConcMarkSweepGC
G1垃圾收集器
开启 -XX:+UseG1GC
关闭 -XX:-UseG1GC