一、Java的四种引用类型
1、强引用
最常见的引用,类似Object obj = new Object()、String str ="hello"。
如果一个对象具有强引用,垃圾回收器绝对不会回收它。即使抛出"OutOfMemoryError"错误,程序终止,也不会随意回收具有强引用的对象来解决内存不足的问题。只有 GC Roots 对象都不通过强引用引用该对象,该对象才能被垃圾回收。
2、软引用
用来描述有用但是不是必需的对象。
在Java中用++Java.lang.ref.SoftReference++类表示,如果内存空间足够,垃圾回收器就不会回收它;如果内存空间不足,就会回收这些对象的内存。通常,软引用用于网页缓存、图片缓存等。
java
User user = new User();
SoftReference softReference = new SoftReference(user);
3、弱引用
同样用来描述非必需的对象,但是它的强度比软引用更弱一些。
Java中用++java.lang.ref.WeakRefence++类表示。当垃圾收集器工作时,无论内存是否足够,都会回收掉只被弱引用关联的对象。(可以用于:单例持有一个activity引用时,会造成内存泄漏,把activity声明为弱引用,在activity被销毁后,垃圾收集器扫描到activity对象后,会回收对象的引用)
注:ThreadLocal中ThreadLocalMap对象中的key使用的是弱引用。
4、虚引用
与其他引用并不同,虚引用不影响对象的生命周期,也无法通过虚引用来取得一个对象实例。
在Java中,用++java.lang.ref.PhantomReference++ 类表示。如果一个对象与虚引用关联,在任何时候都可能被垃圾回收器回收。虚引用++必须和引用队列(ReferenceQueue)联合使用++。
java
User user = new User();
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference phantomReference = new PhantomReference(user,queue);
二、内存划分
1、内存划分
Java运行时数据区:<程序计数器,虚拟机栈,本地方法栈,堆,方法区>;
其中,程序计数器、虚拟机栈、本地方法栈,为线程独有;堆和方法区为所有线程共有。
【程序计数器 】用来记录++当前线程执行到哪个指令++(线程私有,每个线程都有),可以看作是当前线程所执行的字节码的行号指示器。
【虚拟机栈 】执行引擎每调用一个方法,就为这个方法创建一个栈帧。每个方法从调用到执行结束,都对应一个栈帧的入栈和出栈。为++Java代码方法++提供服务。
- 伴随着方法被调用进行入栈,方法执行完成出栈,释放内存,先进后出。
- 栈内存溢出情况:①栈帧过多(例,递归调用);②栈帧过大。
- 异常类型:StackOverFlowError
【本地方法栈 】与虚拟机栈类似,不同的是本地方法栈为++Native方法++提供服务。
【堆 】被所有的线程共享的一块区域,在虚拟机启动时创建,所有的++对象实例及数组++都在堆上分配。使用new关键字,表示在堆上开辟一块新的存储空间。无法扩展时,会抛出OOM异常。
- 线程共享的区域:保存对象实例、数组等;
- 组成:年轻代 + 老年代。年轻代包括Eden区和两个大小相同的Survivor区;
- Jdk1.7和1.8的区别:
- 1.7有一个永久代,存储内容为:类信息、静态变量、常量、编译后的代码;
- 1.8移除了永久代,把数据存储到了本地内存的元空间中。
【方法区/元空间 】又叫做静态区,被各个线程共享。用于存储已经被虚拟机加载的++类信息、常量、静态变量、即时编译器编译后的代码++等数据。
- 主要存储类的信息、运行时常量池;
- 常量池:可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 当类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
- 虚拟机启动的时候创建,关闭虚拟机时释放;
- 如果方法区域中的内存无法满足分配请求,则会抛出 OutOfMemoryError: Metaspace
2、字符串常量池位置
JDK1.6及以前,常量池++在方法区++,对应为"永久代";
JDK1.7,方法区合并到了堆中,这个时候常量池可以理解为++在堆内存++中;
JDK1.8及之后,方法区从堆中分离出来,这时候的方法区对应叫做"元空间",直接++使用物理内存++。因为原来的永久代大小不易评估,同时调优的效率较低。
三、垃圾回收机制
1、确定可被回收对象的方法
(1)引用计数法
一个对象被引用了一次,在当前的对象头上递增一次引用次数。如果这个对象的引用次数为0,代表这个对象可回收。缺点:无法解决循环引用的问题。
(2)可达性分析
JVM应用的GC算法为可达性算法,从 GC Root 开始,标记包括 Root 及 Root 引用的东西,并且只会清除没有标记的东西。因此,Root 其实就是 JVM 认为一定有用的东西。
【可作为 GC Roots 的对象】
- 虚拟机栈(栈帧的本地变量表)中引用的对象;
- 本地方法栈中JNI(即一般说的 Native 方法)引用的对象;
- 方法区中类静态属性、常量引用的对象。
注:目前虚拟机均采用的可达性分析法。
2、回收算法
(1)标记 - 清除
方法:首先标记出需要回收的对象,标记完成后统一回收掉所有的被标记的对象。
优点:标记和清除的速度较快;
缺点:会产生很多不连续的内存碎片,不利于后期变量的内存分配。对于大对象,可能会找不到足够的连续空间,进而会触发新的垃圾回收动作。
(2)标记 - 整理
方法:此方法在标记清除算法上做了改进,标记阶段同样需要标记出所有需要回收的对象,标记之后不直接对可回收对象进行整理,而是让所有的对象朝着一端移动。在移动的过程中,清理掉可回收的对象。
优点:内存被整理后不会产生大量不连续的内存碎片问题;
缺点:由于对象需要异动,效率较低。
多在老年代中使用。
(3)复制
方法:将内存容量划分为相等的两部分。每次只使用其中的一部分,当垃圾回收动作被触发时,将有用的存活的对象复制到另一部分的内存中。清除掉原来的那部分内存的所有数据。
优点:无碎片;
缺点:内存使用率低,可用的内存降为了原来的一半。
多在年轻代中使用。
(4)分代 - 收集
概述:此方法是目前大部分JVM所采用的方法。核心思想是根据对象存活的不同周期将内存划分为不同的的区域,并对不同区域的对象采用不同的垃圾回收方法。
区域划分:年轻代,老年代,永久代(JAVA8之前) \ 元空间(>=Java8)。
分代原因及对象:堆内存是虚拟机管理内存最大的一块,也是垃圾回收最频繁的区域。程序所有的对象实例都存放在堆内存中 。因为++不同类型的对象在内存中存活的时间是不一样++ 的,所以给堆内存分代可以++提高对象内存分配和垃圾回收的效率++。
MinorGC、 Mixed GC、FullGC
- MinorGC,【young GC】发生在新生代的垃圾回收,暂停时间短(STW:stop the world);
- Mixed GC, ++新生代 +++ ++老年代部分区域++的垃圾回收,G1 收集器特有;
- FullGC, ++新生代+++++++ ++老年代完整垃圾回收++,暂停时间长(STW),应尽力避免
Stop The World:JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿,会带给用户不良的体验。
Java8 堆的分区
【年轻代、老年代、永久代(元空间)各自特点】
(1)年轻代:使用复制和标记-清除垃圾收集算法。因为绝大部分的对象都是短声明周期的对象,所以不需要将新生代内存划分为容量相等的两个部分。而是将划分为Eden 区、 Survivor from 、 Survivor to三个部分,比例为 8:1:1。后两个部分一定有一个区域是空白的。Eden和另一个Survivor区域共计90%的新生代容量用于为新创建的对象分配内存,只有10%的Survivor内存浪费。
- 当年轻代内存空间不足,需要进行垃圾回收时,仍然存活的对象会被复制到空白的Survivor内存区域中,Eden和非空白的Survivor进行标记-清除回收。两个Survivor轮换。
- 如果,空白的Survivor空间无法存放下仍然存活的对象时,使用内存分配担保策略直接将新生代存活的对象复制到年老代中。
- 创建大对象时,如果年轻代中没有足够的连续内存时,也直接在老年代分配内存空间。
- Survivor区域的对象年龄默认设置为1,这些对象没经历一次MinorGC,将年龄+1,当年龄达到15(一般),就将他们移入到年老代。另外,如果Survivor中相同年龄N的对象的大小总和大于Survivor区的一半时,那些年龄大于N的所有对象都会被提前(相比于15)移入老年代。
(2)老年代:使用标记-整理的垃圾回收算法。其中的对象一半都是长生命周期的对象。
- 对年老代的垃圾回收称为MajorGC 或 FullGC,次数相对较少,回收时间较长。
- 当新生代中没有充足的空间分配内存,年老代中内存回收也无法回收到足够的内存空间,并且新生代和年老代的空间无法扩展时,就会产生 OutOfMemory异常。
(3)# 永久代(方法区):Java虚拟机内存中的方法区 在Sun HotSpot虚拟机 中被称为永久代,是被各个++线程共享++ 的内存区域,它用于存储++已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码++等数据。永久代垃圾回收比较少,效率也比较低,但是也必须进行垃圾回收,否则会永久代内存不够用时仍然会抛出OutOfMemoryError异常。
"永久代"不是JVM规范中的一个部分,它只是HotSpot虚拟机特有的对于方法区的一种实现方式,即HotSpot虚拟机通过设置永久代来实现JVM要求的方法区;
在Java8及其之后,JVM已经取消"永生代"的使用,而是采用**"元空间"(** Metaspace )替代 了。元空间的本质和永久代类似,都是++对JVM规范中方法区的实现++ 。不过元空间与永久代之间最大的区别在于:++元空间并不在虚拟机中,而是使用++ ++本地内存++。因此,默认情况下,元空间的大小仅受本地内存限制。
采用元空间代替永生代的原因主要是:
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
方法区(永久代)的垃圾收集主要回收两部分的内容:废弃常量 、无用的类。
【废弃常量】:常量池中的字面量为例,一个字符串常量在没有任何 String 对象引用的情况;
【无用的类】:必须满足三个条件:
- 该类的++所有的实例都已经被回收++,即 java 堆中不存在该类的任何实例;
- 加载++该类的++ ++ClassLoder++ ++已经被回收++;
- 该类对应的 ++java.lang.Class++ ++对象没有在任何地方被引用++,无法在任何地方通过反射访问该类的方法。
3、空间担保策略
在发生Minor GC(新生代垃圾回收)之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代 ++所有对象总空间++,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC;
此处"风险"指的是:新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC
四、垃圾回收器
<Serial、Serial Old;Parallel new、Parallel Old;CMS、G1>,Java8 默认Parallel new + Parallel Old。
1、Serial 收集器
- Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;JDK1.3.1前是HotSpot新生代收集的唯一选择;
- 特点:针对新生代;采用复制算法;++单线程收集;进行垃圾收集时,必须暂停所有工作线程++,直到完成;会"Stop The World";
- 应用场景: 依然是HotSpot在Client模式下默认的新生代收集器;也有优于其他收集器的地方:简单高效(与其他收集器的单线程相比);对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的;
2、Serial Old 收集器
- Serial Old是 Serial收集器的老年代版本
- 特点:针对老年代;采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);单线程收集
- 应用场景:主要用于Client模式;而在Server模式有两大用途:在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
3、Parallel New 收集器
- ParNew垃圾收集器是Serial收集器的多线程版本;
- 特点:采用复制算法,++除了多线程外,其余的行为、特点和Serial收集器一样++;如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等; 两个收集器共用了不少代码;
- 应用场景: 在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
4、Parallel Old 收集器
- Parallel Old垃圾收集器是++Parallel Scavenge收集器的老年代版本++, JDK1.6中才开始提供
- 特点:针对老年代;采用"标记-整理"算法;多线程收集。
- 应用场景: JDK1.6及之后用来代替老年代的Serial Old收集器,特别是在Server模式,多CPU的情况下, 这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合。
5、CMS 收集器( JDK1.5 发布)
- 并发标记-清除(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器。
- 特点:++针对老年代++;基于"标记-清除"算法(不进行压缩操作,产生内存碎片);以获取最短回收停顿时间为目标;并发收集、低停顿;需要更多的内存。
- 应用场景:与用户交互较多的场景;希望系统停顿时间最短,注重服务的响应速度;以给用户带来较好的体验;如常见WEB、B/S系统的服务器上的应用。
- CMS收集器运作过程 :
- 初始标记 (CMS initial mark)。仅标记一下GC Roots能直接关联到的对象,速度很快, 但需要 "Stop The World";
- 并发标记(CMS concurrent mark)。标记初始标记中关联的存活对象,进行GC Roots Tracing 的过程,应用程序也在运行,无法保证标记出所有的存活对象;
- 重新标记(CMS remark)。为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录,需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短,采用多线程并行执行来提升效率。
- 并发清除(CMS concurrent sweep)。 回收所有的垃圾对象。
- 整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作,所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
6、G1 收集器
- G1(Garbage-First)是JDK7推出商用的收集器,应用与新生代和老年代,JDK9默认使用。
- 运行过程:
- 初始标记(Initial Marking):标记GC Roots能直接关联到的对象,并修改TAMS(NEXT TOP at MARK Start)的值,让下一阶段用户程序并发执行时,能在正确的Region中创建对象,需要停顿;
- 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,找出存活对象,可与用户线程并发执行;
- 最终标记(Final Marking):为了修正并发标记期间因用户程序运行导致标记产生变化的那部分标记记录,需要停顿线程,可并行执行;
- 筛选回收 (Live Data Counting and Evacuation):对各个Region的++回收价值和成本进行排序++,根据用户期望的GC停顿时间制定回收计划。
- 特点:
- 并行与并发。能充分利用多CPU、多核环境下的硬件优势,也可以并发让垃圾收集与用户程序同时进行
- 分代收集,收集范围包括新生代和老年代。能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配,能够采用不同方式处理不同时期的对象。虽然保留分代概念,但Java堆的内存布局有很大差别,将整个堆 划分为多个++大小相等的独立区域(Region)++,新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合。
- 结合多种垃圾收集算法,空间整合,不产生碎片。++从整体看,是基于++ ++标记-整理++ ++算法, 从局部(两个Region间)看,是基于++ ++复制++ ++算法++,都不会产生内存碎片,有利于长时间运行。
- 可预测的停顿:低停顿的同时实现高吞吐量。G1除了追求低停顿外,还能建立可预测的停顿时间模型,可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒
- 应用场景:面向服务端应用,针对具有大内存、多处理器的机器,最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案, 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;
- G1可以建立可预测的停顿时间模型。这是因为:可以有计划地避免在Java堆的进行全区域的垃圾收集;G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);这就保证了在有限的时间内可以获取尽可能高的收集效率。
Q:JVM有哪些垃圾回收器
A:
在JVM中,实现了多种垃圾收集器,包括:
- 串行:Serial GC(新生代,复制算法)、Serial Old GC(老年代,标记清除)
- 并行:Parallel New GC、Parallel Old GC
- CMS(并发)垃圾收集器:CMS GC,作用在老年代
- G1垃圾收集器,作用在新生代和老年代
Q:介绍一下G1垃圾回收器A:
- 应用于新生代和老年代,在JDK9之后默认使用G1;
- 划分成多个区域,每个区域都可以充当 eden、survivor、old;humongous,其中 humongous 专为大对象准备;
- 采用复制算法
- 响应时间与吞吐量兼顾
- 分成三个阶段:新生代回收(stw)、并发标记(重新标记stw)、混合收集
- 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
五、类加载(器)与双亲委派机制
1、虚拟机类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
2、类加载过程(生命周期)
生命周期:加载 -- 验证 -- 准备 -- 解析 -- 初始化--使用--卸载。
其中,加载、验证、准备、解析、初始化,称为类的加载过程。验证、准备、解析,三个部分统称为连接。
(1)加载阶段:
- 通过一个++类的全限定名++ 来获取定义++此类的二进制流++;
- 将这个字节流所代表的静态存储结构转化为**++方法区++** ++的运行时数据结构++;
- 在内存中生成一个代表这个类的++java.lang.Class++ ++对象++ ,作为方法区这个类的各种数据的访问入口。
(2)验证阶段:
- 确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 大致包括4个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
(3)准备阶段:
正式为类变量 ++分配内存++ 并设置类变量++初始值++的阶段,这些类变量锁使用的内存都将在方法区中进行分配。
- 这个阶段进行内存分配仅包括类变量(被static修饰的变量),不包括实例变量;
- 初始值通常是数据类型的零值。
- public static int value = 123;在该阶段初始化为0;
- public static final int value = 123;在该阶段初始化为123.
(4)解析阶段:
将常量池中的符号引用替换为直接引用。
- 符号引用,即一个字符串,该字符串中给出了唯一标识方法、变量或者类的信息;
- 符号引用与虚拟机实现的内存布局无关,++引用的目标并不一定已经加载到内存中++。
- 直接引用,可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。直接指向对应的方法、变量或者类。
- 直接引用和虚拟机实现的内存布局相关,如果有了直接引用,引用的目标一定已经存在于内存中。
(5)初始化阶段:
根据程序员通过程序制定的主观计划去初始化类变量和其他资源:初始化阶段是执行类构造器<clinit>()方法的过程--主要是++对类变量进行初始化,是执行类构造器的过++程。
- 如果初始化一个类的时候,其父类还没有初始化,优先初始化其父类;
- 如果同时包含多个静态变量和静态代码块,则按自上而下的顺序依次执行。
Q:说一下类加载的执行过程
A:
- 加载:查找和导入class文件;
- 验证:保证加载类的准确性、安全性;
- 准备:为类变量分配内存并设置类变量初始值;
- 解析:把类中的符号引用转换为直接引用;
- 初始化:对类的静态变量,静态代码块执行初始化操作;
- 使用:JVM 开始从入口方法开始执行用户的程序代码;
- 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象。
3、什么是类加载器
JVM只会执行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而启动Java程序。
"通过一个类的全限定名 来获取描述此类的++二进制字节流(字节码文件)"加载到JVM中++,以便让应用程序自己决定如何去获取所需要的类,即用于装载字节码文件。实现这个动作的代码模块称为"类加载器"。
类加载器的分类:
- 启动类加载器 (BootStrap Class Loader):负责加载++%JAVA_HOME%\bin++ 目录下的所有jar包,或者是++-Xbootclasspath++参数指定的路径;
- 扩展类加载器(Extension Class Loader):负责加载%JAVA_HOME%\bin\ext目录下的所有jar包,或者是java.ext.dirs参数指定的路径;
- 应用程序类加载器(Application Class Loader):负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义加载器,那么此加载器就为默认加载。
- 自定义类加载器
4、双亲委派机制的实现
实现双亲委派的代码都集中在++java.lang.ClassLoader++ ++的++ ++loadClass()++ ++方法++中:首先检查是否已经被加载过,如果没有加载则调用父加载器的loadClass()方法,若父加载器为空,则默认使用启动类加载器作为父加载器。如果父加载器失败,抛出ClassNotFoundException
- 类加载器收到类加载的请求;
- 把这个请求++委托给父加载器去完成,一直向上委托,直到++ ++启动类加载器++;
- 启动器加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,抛出异常,通知子加载器进行加载;
- 重复上一步,直到完成加载,否则抛出异常。
即,++自底向上请求,自顶向下尝试加载++,加载成功则结束,否则抛出异常,交给下一级加载。
双亲委派机制的【优点】:
- 避免重复加载 。采用双亲委派模式的是好处是Java类随着它的类加载器一起++具备了一种带有优先级的层次关系++,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次;
- 防止核心库被篡改 。考虑到安全因素,java 核心 api 中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而++启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class++,这样便可以防止核心API库被随意篡改。
几个【注意点】:
- Java虚拟机的第一个类加载器是Bootstrap(启动类加载器),这个加载器很特殊,它不是Java类 ,因此它不需要被别人加载,它嵌套在Java虚拟机内核里面 ,也就是JVM启动的时候Bootstrap就已经启动,它是用C++写的二进制代码(不是字节码),它可以去加载别的类。这也是我们在测试时为什么发现System.class.getClassLoader()结果为null的原因,这并不表示System这个类没有类加载器,而是它的加载器比较特殊,是BootstrapClassLoader,由于它不是Java类,因此获得它的引用肯定返回null;
- 能不能自己写个类叫java.lang.System?
- 不能自己写以"java."开头的类,其要么不能加载进内存,要么即便你用自定义的类加载器去强行加载,也会收到一个SecurityException;
【如何打破双亲委派机制】
- 通过(继承并)重写ClassLoader类的loadClass()方法;
六、JVM调优
主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型。
1、调优指标与原则
相关参数设置位置:
- war包部署在tomcat中,TOMCAT_HOME/bin/catalina.sh 文件
- jar包部部署在启动参数中,通常在linux系统下直接加参数启动springboot项目
- 开头参数 nohup : 在系统后台不挂断地运行命令,退出终端不会影响程序的运行;
- 结尾参数 & :让命令在后台执行,终端退出后命令仍旧执行。
bash
nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &
(1)调优指标
<吞吐量、暂停时间、内存占用>
- 吞吐量:运行用户代码的时间占总运行时间的比例;
- 暂停时间:执行垃圾回收时,程序的工作线程被暂停的时间;
- 内存占用:Java对所占用的内存大小。
(2)调优原则
1)优先原则:优先架构调优和代码调优,最后才是JVM优化;
2、*调优参数
(1)堆设置
- 参数:-Xms,-Xmx 分别对应堆的初始大小和最大值。例,-Xms2648m -Xmx2648m
- 为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值;(最大大小的默认值是物理内存的1/4 ,初始大小是物理内存的1/64)
- 堆太小,可能会频繁导致年轻代和老年代的垃圾回收,产生stop the world,暂停用户线程;
- 堆太大,存在风险,如果发生了fullgc,会扫描整个堆空间,暂停用户线程的时间长;
- 设置参考推荐:尽量大,同时要考察一下当前计算机其他程序的内存使用情况;
(2)虚拟机栈设置
- 参数:-Xss,每个线程默认开启1M的内存,通常设置256k足够。存放栈帧、调用参数、局部变量等,减少每个线程的堆栈,可以产生更多的线程。示例:-Xss128k
(3)年轻代设置
- -XX:SurvivorRatio,Eden区和Survivor区(单个)的大小比例;例,-XX:SurvivorRatio=8,表示Eden区Survivor区所占空间的比例大小为 8:1:1;
- -XX:NewSize,-Xmn(-XX:MaxNewSize):JVM启动时分配的新生代内存和新生代最大内存。1-1.5倍FullGC之后的老年代空间占用;
- -XX:MaxTenuringThreshold=threshold,年轻代晋升老年代阈值,默认15,范围可选 0-15;
(4)# 老年代设置
- -XX:OldSize:设置JVM启动分配的老年代内存大小;
- -XX:NewRatio:指定老年代/新生代的堆内存比例。在hotspot虚拟机中,堆内存 = 新生代 + 老年代。
(5)# 方法区/元空间设置
- (jdk1.7永久代)参数:-XX:PermSize,-XX:MaxPermSize;
- (jdk1.8元空间)参数:-XX:MetaspaceSize,-XX:MaxMetaspaceSize
通常设置相同的值,避免运行时不断扩展,扩大为1.2-1.5倍FullGC后的永久代/元空间大小。
3、GC日志
日志包括"发生的时间、停顿类型--是否发生了Stop-The-World、发生的区域、GC前后区域使用容量及总容量、GC执行的时间(单位s)"
4、故障处理工具
(1)Sun JDK 监控和故障处理工具
|--------|----------------------------------------------------------------------------------------|
| 名称 | 主要作用 |
| jps | JVM Process Status Tool,虚拟机进程状态信息 |
| jstack | Stack Trace for Java,查看java进程的线程的堆栈信息 |
| jmap | Memory Map for Java,生成虚拟机的堆转内存快照(heapdump文件) |
| jhat | JVM Heap Dump Browser,用于++分析++ ++heapdump++ ++文件++,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果 |
| jstat | Statistics Monitoring Tool,用于收集HotSpot虚拟机各方面运行数据,包括垃圾回收信息、类加载信息、新生代统计信息等 |
| jinfo | Configuration Info forJava,显示虚拟机的配置信息 |
(2)JDK 的可视化工具
<JConsole、VisualVM>
1)JConsole(Java Monitoring and Management Console)是一种基于JMX的可视化监视、管理工具。
命令行窗口输入 jconsole 即可开启。可以选择连接本地或者远程的进程,即可查看相关信息。
|----------------------------------------------------------------------------|----------------------------------------------------------------------------|
| | |
2)VisualVM
支持运行监视、故障处理、性能分析等。注:许多版本的JDK不自带此工具,需要单独下载。
5、调优步骤
(1)监控GC的状态
(2)生成堆的dump文件
可以使用Java的++jmap++ ++命令++ 或者++通过++ ++JMX++ ++的++ ++MBean++ ++工具++生成当前的Heap信息,结果是一个hprof文件。
(3)分析dump文件
【可以使用的工具】:
- VisualVM
- IBM HeapAnalyzer
- JDK自带的Hprof工具
- Mat工具(Eclipse中的内存分析工具)
(4)分析结果,判断是否需要优化
(5)调整GC类型和内存分配
(6)不断分析和调整
6、OOM存在的几种情况
(1)堆内存溢出
异常信息:java.lang.OutOfMemoryError:Java heap space
java堆用于存储对象实例,只要程序不断的创建对象,且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆限制后产生内存溢出异常。
原因分析:
- 创建了一个超大的对象,通常是一个大数组;
- 超出预期的访问量/数据量,通常是请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖刺峰值;
- 过度使用终结器(Finalizer),使得对象没有被立即GC;
- 内存泄漏(Memory Leak),大量对象引用没有释放,JVM无法对其自动回收,常见于使用了File等资源没有回收。
解决方案:
通常情况只需要修改"-Xmx"参数,调高JVM堆内存即可。如果未解决:
- 如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,未做数量限制;
- 如果是业务峰值压力,可以考虑添加机器资源,或者做限流处理;
- 如果是内存泄漏,需要找到持有的对象,修改代码,例如关闭没有释放的连接。
(2)虚拟机栈 和本地方法栈溢出
如果线程请求的栈深度大于虚拟机允许的最大深度,将抛出StackOverFlowError异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
(3)运行时常量池溢出
异常信息:java.lang.OutOfMemoryError:PermGen space
如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们++可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量++。
(4)方法区溢出
异常信息:java.lang.OutOfMemoryError:PermGen space
方法区用于存放Class相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
解决方案:
根据 Permgen space 报错的时机,可以采取不同的解决方案:
- 程序启动时报错,修改"-XX:MaxPermSize"启动参数,调大永久代空间。
- 应用重新部署时报错,可能是应用没有重启,导致加载了多份class信息,只需重启解决。
- 运行时报错,应用程序可能会动态创建创建大量的class,而这些class的生命周期很短暂,但是Jvm不会卸载class。可以设置"-XX:CMSClassUnloadingEnabled"和"-xx:+UseConcMarkSweepGC"两个参数来允许JVM卸载class。
如果上述方法无法解决,可以通过jmap命令dump内存对象"jmap -dump:format=b,file=fump.hprof<process-id>",然后利用Eclipse MAT功能逐步分析开销最大的classLoader 和重复的class。
(5)Unable to create new native thread
JVM向操作系统创建native线程失败。
原因分析:
- 线程数超出操作系统最大线程数ulimit限制;
- 线程数超过kernel.pid_max(只能重启);
- native内存不足;
解决方案:
- 升级配置,为机器提供更多内存;
- 降低Java Heap Space的大小;
- 修复应用程序的线程泄漏问题;
- 限制线程池的大小;
- 使用"-Xss"参数减少线程栈的大小;
- 调高操作系统层面的最大线程数:执行"ulimit -a"查看最大线程数限制,使用"ulimit -u xxx"修改最大线程数限制。
(6)Out of swap space
表示++虚拟内存++已经被耗尽。虚拟内存(Virtual Memory)由物理内存(Physical Memory)和交换空间(Swap Space)两部分组成。当运行时程序请求的虚拟内存溢出时就会报该错误。
原因分析:
- 地址空间不足;
- 物理内存已耗完;
- 应用程序的本地内存泄漏;
- 执行"jmap -histo:live<pid>"命令,强制执行Full GC;如果几次执行后内存下降明显,那么基本可以确认为Direct ByteBuffer问题。
解决方案:
根据错误原因,
- 升级地址空间为64bit;
- 使用Arthas检查是否为Inflater/Deflater解压缩问题,如果是,则显式调用end方法;
- Direct ByteBuffer问题可以通过启动参数"-XX:MaxDirectMemorySize"调低阈值;
- 升级服务器配置,避免争用。
(7)Direct Buffer Memory
Java允许应用程序通过Direct ByteBuffer直接访问堆外内存,许多高性能程序通过Direct ByteBuffer结合内存映射文件(Memory Mapped File)实现高速IO。
原因分析:
Direct ByteBuffer默认大小为64MB,超出限制就会抛出"Direct Buffer Memory"错误。
解决方案:
- Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通过 Arthas 等在线诊断工具拦截该方法进行排查。
- 检查是否直接或间接使用了 NIO,如 netty,jetty 等。
- 通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值。
- 检查 JVM 参数是否有 -XX:+DisableExplicitGC 选项,如果有就去掉,因为该参数会使 System.gc() 失效。
- 检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用 sun.misc.Cleaner 的 clean() 方法来主动释放被 Direct ByteBuffer 持有的内存空间。
- 内存容量确实不足,升级配置。
7、几种问题的常用排查思路
1、堆内存溢出检查
(1)程序启动时,设置参数以便在内存溢出时生成dump文件:
-XX:Xms 20m; # 初始堆内存大小
-XX: Xmx 20m; # 最大堆内存大小
-XX:HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:/ # 发生内存溢出时dump文件存放的位置。
(2)使用jmap实时查看内存情况/导出dump文件
一般步骤:
- jps -l :找到当前进程id;
- jmap -heap pid : 查看当前进程的数据(老年代新生代的使用情况);
- jmap -dump:format=b,file=app.hprof pid(导出该进程的hprof文件,关于内存情况的映射)。file后面跟的为文件名称,路径自动生成,显示在cmd中,去对应路径查找文件即可。
2、Java进程CPU 占用过高检查
- 使用top命令找出CPU占用高的Java进程ID;
- 使用"top -H -p 进程id"找出CPU占用高的线程ID;
- 使用"stack 进程id"dump文件获取线程快照;
- 查看dump文件,使用线程id找到问题线程。
3、Java进程内存泄漏
主要使用jstat命令查看GC情况。
jstat -gc pid [interval]
参考:
《深入理解 Java 虚拟机》;