一、Java简介
1、Java开发及运行版本
JRE(Java Runtime Environment,运行环境)
所有的程序都要在JRE下才能够运行。包括JVM和Java核心类库和支持文件。
JDK(Java Development Kit,开发工具包)
用来编译、调试Java程序的开发工具包。包括Java工具(javac/java/jdb等)和Java基础的类库(java API )。
JVM(Java Virtual Machine,虚拟机)
JRE的一部分,是Java的核心和基础,用来加载字节码(.class)文件、管理并分配内存、执行垃圾收集。解释自己的指令集(即字节码)并映射到本地的CPU指令集和OS的系统调用。不同的操作系统会有不同的JVM映射规则,完成跨平台性。
2、Java程序的应用版本
Java SE
标准版(桌面程序、控制台开发、嵌入式环境...),Java的基础与核心,也是JavaEE和JavaME技术的基础。
javaSE包含:面向对象、多线程、IO流、javaSwing
Java EE
在javaSE的基础上,创建了规范和框架,提供Web服务、组件模型、管理和通信API、服务器开发,如开发B/S架构软件
javaEE包含:serclet、jstl、jsp、spring、mybatis
Java ME
机顶盒、移动电话和PDA之类嵌入式消费电子设备提供的Java语言平台,包括虚拟机和一系列标准化的Java API。
Java Card
支持一些java小程序(Applets),运行在小内存设备上的平台。
二、Java的类加载
1、类加载器种类
启动类加载器(Bootstrap ClassLoader)
负责加载%JAVA_HOME%/lib目录下的 jar 包和类。它只能加载自己能够识别的类。
扩展类加载器(Extendtion ClassLoader)
它负责加载<JAVA_HOME>\lib\ext目录中的或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以使用扩展类加载器。
应用程序类加载器(Application ClassLoader)
负责加载ClassPath上所指定的类库,如果应用程序没有自定义过自己的类加载器,一般情况下这就是程序的默认类加载器。
自定义类加载器(CustomerClassLoader)
自己定义的类加载器,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。
2、类加载步骤
1.加载(类加载器完成)
1.通过类的全限定名来获取定义此类的二进制字节流
2.将这个类字节流代表的静态存储结构转为方法区的运行时数据结构
3.在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。
2.校验(连接阶段)
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不危害虚拟机的自身安全。
1.文件格式验证:基于字节流验证。
2.元数据验证:基于方法区的存储结构验证。
3.字节码验证:基于方法区的存储结构验证。
4.符号引用验证:基于方法区的存储结构验证。
3.准备(连接阶段)
为类变量分配内存,并将其初始化为默认值。在方法区中分配这些变量所使用的内存空间。
例1:
public static int value = 123;
此时在准备阶段过后的初始值为0而不是123;
例2:
public static final int value = 123;
此时value的值在准备阶段过后就是123。
4.解析(连接阶段)
把类型中的符号引用转换为直接引用,主要四种:
1.类或接口的解析
2.字段解析
3.类方法解析
4.接口方法解析
5.初始化
为类的静态变量赋予正确的初始值、执行类的静态代码块。
如果有父类,则先运行父类中的变量赋值语句和静态语句。
6.注意
1.类在使用时才会加载:反射触发、 new 对象。
2.加载类的子类会先加载父类。
3.static final修饰的常量属性会存到常量池中,类不会加载。
三、JVM的内存模型
1、程序计数器
存放下条指令所在单元的地址。
2、线程栈(虚拟机栈)
1.局部变量表:存储基本数据类型(int、float、byte等),如果是引用数据类型,则存储的是其在堆中的内存地址,也就是指向对象的一个指针。
2.操作数栈:操作数运算时一块临时的空间来存放操作数。
3.动态链接:将代码的符号引用转换为在方法区(运行时常量池)中的直接引用。
4.方法出口:存储了栈帧中的方法执完之后回到上一层方法的位置。
3、本地方法栈
与虚拟机栈结构一致,本地方法栈执行的是Java底层由C++编写的native方法。
4、元空间(方法区)
主要包括:常量、静态变量、类信息(对象头)、运行时常量池,操作的是直接内存。
默认情况下是off heap的(堆的一个逻辑分区)内存,大小不受jvm大小的限制,属于操作系统内存。
运行时常量池:虚拟机启动,将各个Class文件中的常量池载入到运行时常量池中(加载到内存中)。Class常量池只是一个媒介场所。
5、堆
虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
5.1 对象结构
四、垃圾回收
1、回收策略
1.四种引用类型
强引用
使用最普遍的引用。垃圾回收器绝对不会回收它,内存不足时宁愿抛出 OOM 导致程序异常,平常的 new 对象就是。
软引用
垃圾回收器在内存充足时不会回收软引用(SoftReference)对象,不足时会回收它,特别适合用于创建缓存。
弱引用
在扫描到该对象时无论内存是否充足都会回收该对象。ThreadLocal 的 Key 就是弱引用。
虚引用
如果一个对象只具有虚引用(PhantomReference)那么跟没有任何引用一样,任何适合都可以被回收。主要用跟踪对象跟垃圾回收器回收的活动。
2.判定堆中对象是否为垃圾
引用计数法:在对象中添加一个引用计数器,当有地方引用这个对象时,引用计数器值+1,当引用失效时,计数器值-1。两个对象循环引用的时,各自的计数器始终不会变成0,导致无法回收,引起内存泄露。(一般不采用)。
可达性分析法:从GCRoots的对象(虚拟机栈、方法区的类属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的对象)的引用连来判断是否为垃圾。
3.回收算法
1.标记–清除
首先通过根节点,标记所有从根节点开始的可达对象。清除阶段,清除所有未被标记的对象。
2.标记–整理(压缩)
标记-压缩算法从根节点开始,对所有可达对象做一次标记。将所有的存活对象压缩到内存的一端,清理边界外所有的空间。
3.标记-复制
将内存区域均分为了两块(记为S0和S1),创建对象的时只用其中的一块(如S0),当S0使用完之后,将S0上面存活的对象全复制到S1上去,然后将S0全部清理掉。
4.分代收集
目前大部分JVM的采用的算法。根据对象存活周期将内存划分为几块。根据每块内存区间的特点,使用不同的回收算法,以提高垃圾回收的效率。
将Java堆划分为新生代 (Young Generation)和老年代(Old Generation)两个区域。新生代又分为Eden区、From Survivor和To Survivor。
4.三色标记法
1.三色
黑色:表示根对象,或者该对象与它引用的对象都已经被扫描过了。
灰色:该对象本身已经被标记,但是它引用的对象还没有扫描完。
白色:未被扫描的对象,如果扫描完所有对象之后,最终为白色的为不可达对象,也就是垃圾对象。
2.三色标记过程
1.初始时,全部对象都是白色的
2.GC Roots直接引用的对象变为灰色
3.从灰色集合中获取元素;将本对象直接引用的对象标记为灰色;然后将当前的对象标记为黑色。
4.重复步骤3,直到灰色的对象集合全部变为空
5.结束后,仍然被标记为白色的对象就是不可达对象,就视为垃圾对象。
2、各类回收器
垃圾收集器 | 工作方式 | 作用空间 | 算法 | 特点 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 适用于单CPU环境下的client模式 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境Server模式下与CMS配合使用 |
Parallel | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
Serial Old | 串行 | 老年代 | 标记-整理(压缩)算法 | 响应速度优先 | 适用于单CPU环境下的Client模式 |
Paraller Old | 并行 | 老年代 | 标记-整理(压缩)算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
CMS | 并发 | 老年代 | 标记-清除算法 | 响应速度优先 | 适用于互联网或B/S业务 |
G1 | 并发、并行 | 新生代、老年代 | 标记-整理(压缩)算法 | 响应速度优先 | 响应速度优先 |
1、对象分配
1.优先分配到Eden。
2.如果对象在Eden出生并经过第一次 Minor GC后仍然存活,且能被 Survivor容纳的话,将被移动到 Survivor空间中。对象在Survivor区中每熬过一次 Minor GC,年龄就增加1岁,当到达阈值(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中。
3.大对象直接分配到老年代,尽量避免程序中出现过多的大对象。
4.长期存活的对象分配到老年代。
5.动态对象年龄判断,如果 Survivor区中相同年龄的所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxtenuringThreshold中要求的年龄。
2.GC收集
新生代收集(Minor GC/ Young GC):新生代的垃圾收集。
老年代收集(Major GC/ old GC):老年代的垃圾收集。目前,只有 CMS GC会有单独收集老年代的行为。注意,很多时候 Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集,目前,只有G1会有这种行为。
整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。
1.年轻代GC(Minor GC)触发机制
(1)当年轻代空间不足时,就会触发 Minor GC,这里的年轻代满指的是Eden代满,,Survivor满不会引发GC。(每次 Minor Gc会清理年轻代的内存。)
(2)因为Java对象大多都具备朝生夕灭的特性,所以 Minor gc非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
(3)Minor gc会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
2.老年代GC(Major GC/FullGC)触发机制
(1)指发生在老年代的GC,对象从老年代消失时, Major GC”或“Fu11GC发生了。
(2)出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行 Major GC的策略选择过程)。
(3)也就是在老年代空间不足时,会先尝试触发 Minor GC。如果之后空间还不足,则触发 Major GC。
(4)Major GC的速度一般会比 Minor gc慢10倍以上,STW的时间更长。
3.触发FullGc执行的情况
(1)调用 System. gc()时,系统建议执行Fu11GC,但是不必然执行。
(2)老年代空间不足。
(3)方法区空间不足。
(4)通过 Minor GC后进入老年代的平均大小大于老年代的可用内存。
(5)由Eden区、 from区向 from区复制时,对象大小大于 To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
3、G1回收器
G1 作为 JDK9 之后的服务端默认收集器,不再区分年轻代和老年代进行垃圾回收。
G1 默认把堆内存分为 N 个分区,每个 1~32M。提供了四种不同区域标签 Eden、Survivor 、Old、 Humongous。H(Humongous)区可以认为是 Old 区中一种特列专门用来存储大数据的。
1.G1的运行过程
1.初始标记:标记下GC Roots能直接关联到的对象。这个阶段需要短暂停顿线程。
2.并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
3.最终标记:对用户线程做一个短暂的暂停,用于处理并发标记阶段仍遗留下来的最后那少量的SATB记录(漏标对象)。
4.筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,采用复制清除算法,把有用的复制到新Region,再清理掉整个旧Region的全部空间。须暂停用户线程,由多个收集器线程并行完成。
2.G1的特点
优点:
1、支持较大的内存。
2、暂停时间可控。
3、压缩空间,避免产生内存碎片。
4、简单配置就能达到很好的性能。
5、内存模型方面:G1采用物理分区,逻辑分代,不连续的内存区域Region组成。而CMS中Eden,Survivor,Old区是连续的一整块内存。
6、G1既可以收集年轻代,也可以收集老年代,而CMS只能在老年代使用。
缺点:
1、记忆集RSet会占用比较大的内存,因此不建议在小内存下使用G1,推荐至少6G。
2、对CPU的负载可能会更大一点。
3、由于采用复制算法,GC垃圾回收过程对象复制转移会占用较多的内存,更容易出现回收失败(Allocation (Evacuation) Failure)的问题。
4、可能会降低吞吐量。
虽然 G1收集器的垃圾收集暂停时间通常要短得多,但应用程序吞吐量也往往略低一些。
相当于把一次垃圾回收的工作,分开多次进行执行(主要指老年代),单次暂停的时间虽然更加可控,但是由于每次垃圾回收的空间会更少,
总体来说垃圾回收的效率会更低,暂停的总时间会更长,所以吞吐量往往会略低一些。
3.对比CMS
CMS的老年代回收采用的是标记-清除算法
初始标记:标记GC root能直接关联的对象(短暂STW)。
并发标记:GCRootsTracing,从并发标记中的root遍历,对不可达的对象进行标记。
重新标记:修正并发标记期间因为用户操作导致标记发生表更的对象,采用的incremental update算法,会出现比较多的STW。
并发清除:由于是直接清理,不涉及对象的复制转移,所以阶段可以并发执行。
五、JVM的工具及调优
1、工具
jps:输出 JVM 中运行的进程状态信息
jstack:生成虚拟机当前时刻的线程快照
jstat:虚拟机统计信息监控工具
jinfo:实时地查看和调整虚拟机各项参数
jmap:生成虚拟机的内存转储快照,heapdump 文件
JConsole:可视化管理工具,常用
2、调优
在没有全面监控、收集性能数据之前,调优是盲目的(一般项目加个 xms 和 xmx 参数就够)。
日常分析 GC 情况优化代码比优化 GC 参数要多得多。一般如下情况不用调优的:
minor GC 单次耗时 < 50ms,频率 10 秒以上。说明年轻代 OK。
Full GC 单次耗时 < 1 秒,频率 10 分钟以上,说明年老代 OK。
GC 调优目的:GC 时间够少,GC 次数够少。
3、调优建议
-Xms5m 设置 JVM 初始堆为 5M,-Xmx5m 设置 JVM 最大堆为 5M。-Xms 跟-Xmx 值一样时可以避免每次垃圾回收完成后 JVM 重新分配内存。
-Xmn2g:设置年轻代大小为 2G,一般默认为整个堆区的 1/3 ~ 1/4。- Xss 每个线程栈空间设置。
-XX:SurvivorRatio,设置年轻代中 Eden 区与 Survivor 区的比值,默认=8,比值为 8:1:1。
-XX:+HeapDumpOnOutOfMemoryError 当 JVM 发生 OOM 时,自动生成 DUMP 文件。
-XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。
-XX:MaxTenuringThreshold 设定对象在 Survivor 区最大年龄阈值,超过阈值转移到老年代,默认 15。
开启 GC 日志对性能影响很小且能帮助我们定位问题,-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:gc.log
4、JVM 参数
参数 说明 实例
-Xms 初始堆大小,默认物理内存的1/64 -Xms512M
-Xmx 最大堆大小,默认物理内存的1/4 -Xms2G
-Xmn 新生代内存大小,官方推荐为整个堆的3/8 -Xmn512M
-Xss 线程堆栈大小,jdk1.5及之后默认1M,之前默认256k -Xss512k
-XX:NewRatio=n 设置新生代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 -XX:NewRatio=3
-XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:8,表示Eden:Survivor=8:1:1,一个Survivor区占整个年轻代的1/8 -XX:SurvivorRatio=8
-XX:PermSize=n 永久代初始值,默认为物理内存的1/64 -XX:PermSize=128M
-XX:MaxPermSize=n 永久代最大值,默认为物理内存的1/4 -XX:MaxPermSize=256M
-verbose:class 在控制台打印类加载信息
-verbose:gc 在控制台打印垃圾回收日志
-XX:+PrintGC 打印GC日志,内容简单
-XX:+PrintGCDetails 打印GC日志,内容详细
-XX:+PrintGCDateStamps 在GC日志中添加时间戳
-Xloggc:filename 指定gc日志路径 -Xloggc:/data/jvm/gc.log
-XX:+UseSerialGC 年轻代设置串行收集器Serial
-XX:+UseParallelGC 年轻代设置并行收集器Parallel Scavenge
-XX:ParallelGCThreads=n 设置Parallel Scavenge收集时使用的CPU数。并行收集线程数。 -XX:ParallelGCThreads=4
-XX:MaxGCPauseMillis=n 设置Parallel Scavenge回收的最大时间(毫秒) -XX:MaxGCPauseMillis=100
-XX:GCTimeRatio=n 设置Parallel Scavenge垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) -XX:GCTimeRatio=19
-XX:+UseParallelOldGC 设置老年代为并行收集器ParallelOld收集器
-XX:+UseConcMarkSweepGC 设置老年代并发收集器CMS
-XX:+CMSIncrementalMode 设置CMS收集器为增量模式,适用于单CPU情况。