一、从源码到类文件
当我们写好一个java文件,如下图:
class Person{
private String name;
private int age;
private static String address;
private final static String hobby="Programming";
public void say(){
System.out.println("person say...");
}
public int calc(int op1,int op2){
return op1+op2;
}
}
并且执行编译命令javac Person.java生成Person.class文件时,实际上经历了如下过程:Person.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> Person.class文件。
二、类文件到虚拟机(类加载机制)
1、装载
1.通过一个类的全限定名获取定义此类的二进制字节流。
2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
2、链接
1. 验证:包括文件格式验证、元数据验证、字节码验证、符号引用验证。
2. 准备:为类的静态变量分配内存,并将其初始化为默认值。
3. 解析:把类中的符号引用转换为直接引用。
3、初始化
对类的静态变量,静态代码块执行初始化操作。
4、类加载器
类加载器的作用是通过一个类的全限定名获取定义此类的二进制字节流。分为启动类加载器、拓展类加载器、应用类加载器和自定义类加载器,功能和父子关系如图:
5、类加载顺序——双亲委派机制
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归。比如,Java中的Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的Object类。
可以通过继承ClassLoader类,然后重写其中的loadClass方法来破坏双亲委派机制。其他方式待拓展。
三、运行时数据区
运行时数据区体现了JAVA程序运行时的内存布局和操作,结构如图:
1、程序计数器
程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)。如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,则这个计数器为空。
2、虚拟机栈
虚拟机栈也是线程私有的,每个方法被执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等,每个方法被调用直至执行完成的过程,就是栈帧在虚拟机栈中从入栈到出栈的过程。
1. 局部变量表存放基本数据类型和对象引用,long和double类型的数据会占用2个局部变量空间(Slot),其余只占用一个。
2.操作数栈,当一个方法开始执行的时候操作数栈是空的,在方法执行的过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈操作。
3.动态链接,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
4.方法出口,当一个方法被执行后,需要返回到方法被调用的位置,程序才能继续执行,方法返回时需要保存一些信息,用来恢复上层的方法执行状态。
3、本地方法栈
本地方法栈与虚拟机栈非常类似,区别在于它是用来执行Native本地方法的。
4、堆
堆是Java虚拟机锁管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此区域的唯一目的就是存放对象实例。此外,堆还是垃圾收集器管理的主要区域。堆可分为新生代(包括Eden空间、From Survivor空间和To Survivor空间)和老年代。
5、方法区
方法区也是被所有线程共享的一块内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量。在jdk1.7及以前用永久代实现,在jdk1.8以后用metaspace实现。垃圾回收在这个区域较少实现。运行时常量池是方法区的一部分,主要用来存放各种字面量及符号引用。
四、内存溢出示例
1、堆溢出
Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达 路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生 内存溢出异常。
package com.fuqy;
import java.util.ArrayList;
import java.util.List;
/**
* VM:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public class HeapOOM {
static class OOMObject{}
public static void main(String[] args) {
List<OOMObject> OOMObjectList = new ArrayList<>();
while(true){
OOMObjectList.add(new OOMObject());
}
}
}
运行结果:
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
2、栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。示例中定义了大量的本地变量,增大此方法帧中本地变量表的长度。
package com.fuqy;
/**
* VM:-Xss128k
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
运行结果:
stack length:994
Exception in thread “main” java.lang.StackOverflowError
3、方法区溢出
package com.fuqy;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* JDK7:-XX:PermSize=10M -XX:MaxPermSize=10M
* JDK8:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
*/
public class JavaMethodAreaOOM {
static class OOMObject{}
public static void main(String[] args) {
while (true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor(){
public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable{
return proxy.invokeSuper(object, args);
}
});
enhancer.create();
}
}
}
运行结果
Exception in thread “main” java.lang.OutOfMemoryError: Metaspace
五、内存模型
1、对象的内存布局
一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充。
2、堆和方法区的内存分布
1.共享内存分为两块一块是堆区,一块是方法(非堆)区。
2.堆区分为两大块,一个是Old区,一个是Young区。
3.Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区。 Eden:S0:S1=8:1:1。
4.S0和S1一样大,也可以叫From和To。
3、对象分配策略
1.对象优先在Eden分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
2.大对象直接进入老年代。所谓的大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
3.长期存活的对象将进入老年代。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。
4.动态对象年龄判定。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
六、垃圾回收
1、垃圾回收对象(如何判断对象是垃圾)
1.引用计数法。对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。引用计数法的缺陷是无法解决循环引用的问题。
2.可达性分析。通过GC Root的对象,开始向下寻找,看某个对象是否可达。可以作为GC Root的对象有类加载器、虚拟机栈的本地变量表的对象、方法区中静态变量、常亮引用的对象、本地方法栈中的对象。
2、垃圾回收时机
1.Eden区或者S区不够用了——Minor GC。
2.老年代空间不够用了——Major GC。
3.方法区空间不够用了——Full GC。
4.System.gc()——Full GC。
3、垃圾回收算法
1.标记-清除法。如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程 序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾 收集动作。
2.复制算法。为解决效率问题,“复制”收集算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原 来的一半,未免太高了一点。
3.标记-整理算法。根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4.分代收集算法。这种算法是根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清除”或者“标记—整理”算法来进行回收。
七、垃圾收集器
1、Serial收集器
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。
优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器
2、ParNew收集器
ParNew收集器可以器理解为是Serial收集器的多线程版本。
优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器
3、Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集
器,看上去和ParNew一样,但是Parallel Scanvenge更关注系统的吞吐量 。
优点:可控制的吞吐量
缺点:停顿时间可能受影响。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下执行批量作业
-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,
-XX:GCTimeRatio直接设置吞吐量的大小。
4、Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算
法",运行过程和Serial收集器一样。
5、Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理算法"进行垃圾回收。
6、CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间为目标的收集器。
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量
算法:标记-清除算法
适用范围:老年代
应用:对响应要求较高的Web服务器
7、G1收集器
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1特点:
- 并行与并发
- 分代收集(仍然保留了分代的概念)
- 空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
- 可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)
调优指南:
- 不要手动设置新生代和老年代的大小,只要设置整个堆的大小G1收集器在运行过程中,会自己调整新生代和老年代的大小 其实是通过adapt代的大小来调整对象晋升的速度和年龄,从而达到为收集器设置的暂停时间目标 如果手动设置了大小就意味着放弃了G1的自动调优。
- 不断调优暂停时间目标,一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就不太合理。暂停 时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以对这个参数的调优是一个持续 的过程,逐步调整到最佳状态。暂停时间只是一个目标,并不能总是得到满足。
- 可以适当增加并发GC时堆内存占用百分比,-XX:InitiatingHeapOccupancyPercent=45 G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示“一直执行GC循环)’. 默认值为 45 (例如, 全部的 45% 或者使用了45%)。
- 使用-XX:ConcGCThreads=n来增加标记线程的数量。
- 适当增加堆内存大小
8、垃圾收集器分类
- 串行收集器->Serial和Serial Old 只能有一个垃圾回收线程执行,用户线程暂停。 适用于内存比较小的嵌入式设备 。
- 并行收集器[吞吐量优先]->Parallel Scanvenge、Parallel Old 多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 适用于科学计算、后台处理等若交互场景 。
- 并发收集器[停顿时间优先]->CMS、G1 用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。 适用于相对时间有要求的场景,比如Web 。
9、理解吞吐量和停顿时间
- 停顿时间->垃圾收集器进行垃圾回收终端应用执行响应的时间
- 吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间)
10、如何选择合适的垃圾收集器
- 优先调整堆的大小让服务器自己来选择。
- 如果内存小于100M,使用串行收集器。
- 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选。
- 如果允许停顿时间超过1秒,选择并行或JVM自己选。
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器。
八、常用命令和工具
1、jps
查看java进程。
jps
jps -l
2、jinfo
实时查看和调整JVM参数。
jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID
jinfo -flags PID
3、jstat
查看虚拟机性能、类装载、垃圾回收等信息。
jstat -gc PID 1000 10
4、jstack
查看线程堆栈信息,可用于排查死锁。
jstack PID
5、jmap
打印堆内存信息,以及手动生成堆存储快照(dump文件)。
-XX:+PrintFlagsFinal -Xms300M -Xmx300M
jmap -heap PID
jmap -dump:format=b,file=heap.hprof 44808
一般在开发中,JVM参数可以加上下面两个参数,这样内存溢出时,会自动dump出该文件:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
6、jconsole
JConsole工具是JDK自带的可视化监控工具。查看java应用程序的运行概况、监控堆信息、永久区使用
情况、类加载情况等。
7、jvisualvm
VisualVM(All-in-One Java Troubleshooting Tool)是到目前为止随JDK发布的功能最强大的运行监视和故障处理程序,并且可以预见在未来一段时间内都是官方主力发展的虚拟机故障处理工具。
8、Memory Analyzer Tool
Java堆分析器,用于查找内存泄漏,分析内存溢出原因。
9、GCViewer
用于查看分析GC日志。
九、常见问题思考
1、内存泄漏与内存溢出的区别
内存泄漏:对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
内存溢出:内存泄漏到一定的程度就会导致内存溢出,但是内存溢出也有可能是大对象导致的。
2、young gc会有STW吗?
不管什么 GC,都会有 stop-the-world,只是发生时间的长短。
3、major gc和full gc的区别
major gc指的是老年代的gc,而full gc等于young+old+metaspace的gc。
4、G1与CMS的区别是什么
CMS 用于老年代的回收,而 G1 用于新生代和老年代的回收。
G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生。
5、什么是直接内存
直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。
6、不可达的对象一定要被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
7、方法区中的无用类回收
方法区主要回收的是无用的类,需要同时满足下面 3 个条件才能算是 “无用的类” :
1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
2.加载该类的 ClassLoader 已经被回收。
3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
8、不同的引用
1.强引用就是指类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
2.软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。在JDK 1.2之后,提供了SoftReference类来实现软引用。例如缓存。
3.弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够, 都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
4.虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。为一 个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在 JDK 1.2之后,提供了PhantomReference类来实现虚引用。
9、常见JVM参数及含义
10、JVM性能优化思路