JVM总体架构图
类加载子系统
加载:在内存中生成一个代表这个类的java.lang.class对象放入元空间(永久代)
验证:验证Class文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证虚拟机的安全;
准备:类变量赋默认初始值,如果 是常量在这个时候就直接赋值了;
解析:把符号引用翻译为直接引用;
初始化:当我们new一个类的对象,访问一个类的静态属性,修改一个类的静态属性,调用一个类的静态方法,用反射API对一个类进行调用,初始化当前类,其父类也会被初始化…那么这些都会触发类的初始化;
使用:使用这个类
卸载:
1.该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;
2.加载该类的ClassLoader已经被GC;
3.该类的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法;
类加载器
类加载器请求步骤
1、类加载器收到类加载的请求;
2、将这个请求向上委托给父类加载器去完成,一直向上委托, 直到启动类加载器;
3、启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则, 抛出异常,通知子加载器进行加载;
4、重复步骤3。
也就是说,先在启动类加载器中寻找是否有需要加载的类,若没有,再往下寻找,都没有找到,则抛出异常:ClassNotFoundException。
这就是双亲委派机制
双亲委派机制有什么优点:
1.避免了类的重复加载
2.保护了程序的安全性,防止核心的API被修改
JVM内存结构划分
堆、元空间(方法区)为线程共享区;
其他区域是线程私有的;
程序计数器
1、线程私有;
2、无GC回收;
3、该区域不存在内存溢出;
虚拟机栈
1、线程私有;
2、方法执行会创建栈帧,存储局部变量表等信息;
栈针包含:
局部变量表、操作数栈、动态链接、返回地址
3、方法执行入虚拟机栈,方法执行完出虚拟机栈; (先进后出)
4、栈深度大于虚拟机所允许StackOverflowError;(在递归时可能遇到)
5、栈需扩展而无法申请空间OutOfMemoryError (比较少见) ; hotspot 虚拟机没有; (一直创建线程才报错)
6、栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值、对象值等)都存放到堆上的;
7、栈一般都不设置大小,栈所占的空间其实很小,可以通过-Xss1M进行设置,如果不设置默认为1M;
常量池
- JDK1.6时,常量池在永久代中;
- JDK1.7时,常量池在堆内存中;
- JDk1.8时,常量池在元空间中;
本地方法栈
1、与虚拟机栈基本类似;
2、区别在于本地方法栈为Native方法服务;
3、HotSpot 虚拟机将虚拟机栈和本地方法栈合并;
4、有StackOverflowError和OutOfMemoryError (比较少见) ;
5、随线程而生,随线程而灭;
6、GC不会回收该区域;
堆
1、线程共享的一块区域;
2、虚拟机启动时创建;
3、虚拟机所管理的内存中最大的一块区域;
4、存放所有实例对象或数组;
5、GC垃圾收集器的主要管理区域;
6、可分为新生代、老年代;
7、新生代更细化可分为Eden、From Survivor、To Survivor, Eden:Survivor = 8:1:1
8、可通过-Xmx、-Xms 调节堆大小;
9、无法再扩展java.lang.OutOfMemoryError: Java heap space,
10、如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB),以提升对象分配时的效率;
JVM中对象如何在堆内存中分配?
1、指针碰撞(Bump The Pointer) :内存规整的情况下;
2、空闲列表(Free List) : 内存不规整的情况下;
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact) 的能力决定;
因此,当使用Serial、ParNew 等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;
而当使用CMS这种基于清除(Sweep)算法的收集器时,理论.上就只能采用较为复杂的空闲列表来分配内存; .
3、本地线程分配缓冲(Thread Local Allocation Buffer, TLAB) :对象创建在虚拟机中频繁发生,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况;
那么解决方案有两种: .
(1)同步锁定,JVM是采用CAS配上失败重试的方式保证更新操作的原子性;
(2)线程隔离,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX: +/-UseTLAB 参数来设定;
-XX:TLABSize=512k 设置大小; 默认大小为0,由虚拟机自动分配。
堆内存溢出
在运行Java程序时,可设置JVM参数,下面两个参数作用:
在程序堆内存溢出时,将溢出前的快照信息保存到目标文件,再通过Eclipse MAT 内存分析工具,找到可能造成内存溢出的原因
-XX: +HeapDumpOnOutOfMemoryError
-XX :HeapDumpPath=d:/dev/heapdump.hprof
判断是否为垃圾的两种方式
- 引用计数法: 为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为0,则说明该对象已经死亡,便可以被回收了。
缺点: 不仅仅多占内存,并且对每个类的引用数量计算也是个繁琐的操作,还存在一个重大bug,对循环引用的对象无法处理。
所以目前 Java 虚拟机的主流垃圾回收器采取的是可达性分析算法。 - 根可达算法: 将一系列被称为GC Root的对象作为初始的存活对象,将所有从该对象出发,能被引用的对象加入集合中,而未能加入的集合中的对象则宣告死亡。
JVM新生代垃圾回收过程(Minor GC—新生代的垃圾回收)
- 所有对象首先都会进入堆中新生代的Eden区,两个幸存者空间(s0、s1区)都是空的;
- 当Eden区占满之后会触发Minor GC进行垃圾回收;
- 还存在引用的对象就移动到幸存区,之后会清空Eden区,将删除未引用的对象;
- 在下次Minor GC时Eden区发生同样的事情,未引用的对象将被删除,引用的对象移动到survivor区,在当前的情况,会将这些还有引用对象移动到空的survivor区(s1区),并且会判断另一个survivor区(s0区)存在的对象是否还被引用,存在引用的移到s1区,并且对象的年龄增加1,不存在引用的被清空掉;
- 下一次Minor GC时情况一致,但是这次survivor区空间切换了,幸存的对象从s1区移动到空的s0区且对象的年龄递增1,接着清空Eden区、s1区;
- 重复前面的过程,当老化对象达到某个年龄的阈值时,他们将从新生代移入到老年代;
注: JVM默认垃圾收集器的年龄阈值为15,到达15后对象会从新生代移入老年代;而CMS垃圾收集器默认的年龄阈值为6.
老年代空间担保机制
- 在每次Minor GC之前,JVM都会进行判断当前老年代的可用内存是否大于新生代所有对象的总大小,因为极端情况下,新生代进行Minor GC后,所有对象都需要存活,那就造成所有对象都要进入老年代;
- 如果老年代可用内存大于新生代所有对象总和,此时可以放心大胆进行Minor GC,因为就算需要存活的对象survivor区放不下,也可以都放进老年代中;
- 如果执行Minor GC前,老年代可用内存大小已经小于新生代所有对象总和,那么就会进入下一个判断,判断老年代可用空间大小,是否大于之前每次Minor GC后,进入老年代对象的大小的平均值,如果判断发现老年代可用空间大小大于之前每一次Minor GC后进入老年代的对象的平均大小,则会进行一次冒险的尝试,进行Minor GC,但是此时的尝试真的有风险,可能需要存活的对象的大小大于survivor区的大小,也大于老年代可用空间的大小,老年代都放不下这些存活的对象,此时就会触发一次“Full GC”;
- 如果老年代进行Full GC后还是放不下Minor GC后的存活对象,那么此时就会导致“OOM”内存溢出;
所以老年代空间分配担保机制的目的?
为了避免频繁进行Full GC;
JVM调优的其中一项就是减少Full GC的执行次数。
参数-XX:+HandlePromotionFailure用于开启是否要进行空间担保;
什么情况下对象会进入老年代?
- 当对象在survivor区躲过了15次Minor GC后进入老年代,可通过JVM参数"-XX:MaxTenuring Threshold"来设置年龄,默认是15岁;
- 当survivor区所使用的内存大于50%时,将50%之后的年龄对象移入老年代;(动态年龄判断)
- 老年代空间担保机制;
- 大对象直接进入老年代。
大对象是指需要大量连续内存空间的Java对象,比如很长的字符串或者是很大的数组或者List集合,大对象在分配空间时,容易导致内存明明还有不少空间时就提前触发垃圾回收以获得足够的连续空间来存放它们,而当复制对象时,大对象又会引起高额的内存复制开销,为了避免新生代里出现那些大对象,然后屡次躲过GC而进行来回复制,此时JVM就直接把该大对象放入老年代,而不会经过新生代;
我们可以通过JVM参数"-XX:PretenureSizeThreshold"设置多大的对象直接进入老年代,该值为字节数,比如“1048576”字节就是1MB,该参数表示如果创建一一个大于这个大小的对象,比如一个超大的数组或者List集合,此时就直接把该大对象放入老年代,而不会经过新生代;
-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,其他新生代垃圾收集器不支持该参数,如果必须使用此参数进行调优,可考虑ParNew+CMS的收集器组合;
元空间
元空间的特点和作用?
- 在JDk1.8才出现元空间的概念,之前都叫方法区/永久代;
- 元空间和Java堆类似,也是线程共享的区域;
- 存储被加载的类信息、常量、静态变量、常量池、即时编译后的代码数据;
- 元空间采用的是本地内存,本地内存有多少剩余的空间,它就能扩展多大的空间,也可以设置元空间的大小;
-XX: MetaspaceSize=20M 初始大小 -XX : MaxMetaspaceSize=20M 最大大小 - 元空间很少有GC垃圾收集,一般该区域回收条件苛刻,能回收的信息比较少,所以GC很少来回收;
- 元空间内存不足时,将抛出OutOfMemoryError;
说几个与JVM内存相关的核心参数?
- -Xms Java堆内存大小
- -Xmx Java堆内存的最大大小
- -Xmn Java堆内存中的新生代的大小,扣除新生代剩下的技术老年代的大小
- -XX:MetaspaceSize 元空间的大小
- -XX:MaxMetaspaceSize 元空间最大大小
- -Xss 每个线程的栈内存大小
- -XX:SurvivorRatio=8 设置eden区和survivor区大小的比例,默认是8:1:1
- -XX:MaxTenuringThreshold=5年 龄阈值
- -XX:+UseConcMarkSweepGC 指定CMS垃圾收集器
- -XX:+UseG1GC 指定使用 G1垃圾回收器
- –查看默认的堆大小及默认的垃圾收集器
java -XX:+PrintCommandLineFlags -version
堆为什么要分成年轻代和老年代?
因为年轻代和老年代的特点不同,因此需要采取不同的垃圾回收算法;
年轻代的对象,它的特点是创建之后马上就会被回收,所以需要一种垃圾回收算法;
老年代的对象,它的特点是需要长期存活,所以需要另外一种垃圾回收算法;
所以分成两个不同的区域来存放不同的对象;
1、绝大多数对象都是朝生夕灭的;
如果一个区域中大多数对象都是朝生夕灭,那么把它们集中放在一起, 每次回收时只关注如何保留少量存活对象,而不是去标记那些大量将要被回收的对象,就能以较低的代价回收到大量的空间;
2、熬过越多次垃圾回收的对象就越难以被回收;
如果是需要长期存活的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用;
3、JVM划分出新生代、老年代之后,垃圾收集器可以每次只回收其中某一个或者某些部分的区域,同时也有了"Minor GC” "Major GC” "Full GC"这样的回收类型的划分;
Minor GC/Young GC :新生代收集
Major GC/OId GC:老年代收集
Full GC:整堆收集,收集整个Java堆和元空间/方法区的垃圾收集;
Mixed GC:混合收集,收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为;
4、针对不同的区域对象存亡特征采用不同的垃圾收集算法:
标记-复制算法
标记-清除算法
标记-整理算法