1、JDK体系结构
1.1、JVM:
JVM(JAVA虚拟机)是运行Java字节码的虚拟机,通过编译.java文件为.class文件得到字节码文件 . .class文件包含JVM可以理解的字节码。
在现实世界中,JVM是一种规范,它提供可以执行Java字节码的运行时环境。不同的供应商提供这种规范的不同实现,可访问Different JVM implementations查看
最受欢迎的JVM的实现是Hotspot,它是由Oracle公司拥有并提供维护。
JVM使用许多高级技术为Java程序提供最佳性能,包括 先进的内存模型,GC(垃圾回收器)和adaptive optimizer(自适应优化器)
JVM有两种不同的风格-client和server。虽然Server和Client VMs相似,但是Server VM经过了特殊的调优,可以最大限度地提高峰值运行速度。它用于执行长时间运行的服务器应用程序,这些应用程序需要尽可能快的运行速度,而不是快速启动时间或更小的运行时内存占用。开发人员可以通过指定-client或-server来选择他们想要的系统。
JVM之所以称为虚拟机,是因为它提供了一个不依赖于底层操作系统和机器硬件体系结构的机器接口。
这种与硬件和操作系统的独立性使得Java程序“写一次,到处运行”(write-once-run-anywhere).
Class Loader:待更新
1.2、JRE
Java运行时环境(JRE)是一个软件包,它捆绑了libraries(jar)和JVM,以及用Java编写的应用程序的其他组件。JVM只是JRE发行版的一部分。执行任何Java程序,需要在机器上安装JRE,这是最低要求。
1.3、JDK
JDK是JRE的超集,JDK包含了JRE的所有开发,调试和监视应用程序的工具。当要开发Java应用程序时,需要安装JDK。
下面是JDK附带的一些重要组件:
apt 注解处理工具
javadoc 文档生成器,可以自动从源代码生成说明文档
jar 归档器,将相关的类库打包到一个JAR文件中。还可以帮助管理JAR文件
jConsole Java监控和管理平台
jhat Java堆分析工具
jstack 打印Java线程的堆栈信息
keytool 策略创建和管理工具
jarsigner Java签名和验证工具
2、JVM内部组成
2.1、栈
方法调用过程中,被调用方法会产生变量,而这些会随着方法结束完成运算得到结果的变量就存放在栈,栈遵循先进后出的原则,所以调用方法就相当于压栈,而方法结束返回结果就相当于 出栈,这一特性和数据结构中的栈莫名的契合,所以栈用来存放线程中的方法的局部变量。
注意:局部变量0值当前对象的内存地址。
2.2、程序计数器
程序计数器是每个线程独有,用来存放运行代码的行号。程序计数器真正存放的是方法区的线程行号的内存地址。
程序计数器的目的是为了让JVM在多线程切换过程中,线程恢复时可以定位到之前执行的行号,不至于乱行。
2.3、方法区
JDK1.8之前称之为方法区,之后称之为元空间,元空间无上限(理论上只受限于物理内存)。用来存放:类信息+常量+静态变量,1.8之后字符串常量迁移到堆
2.4、堆
堆用来存放对象。
需要理解的是,堆内存的分配关系其实蕴含了垃圾回收算法的似思想:
1、假如没有幸存区,直接从新生代往老年代回收,就只存在一种GC,而这种垃圾回收必然是需要标记清除并且整理的,因为不能产生碎片话的数据。 这也是为什么有幸存区的原因,即引入YGC对量级很重的FGC进行缓冲。
2、为什么幸存区的比例是1:1,这是因为幸存区的垃圾回收采用了copy算法,如此一来我我们只需要将对象从幸存区1、2之间来回copy,如此一来YGC非常迅速。
3、每次新生代对象的来回copy都会进行计数,这些信息其实包含在Object Header中,当超过15之后便会被存放到老年代。
4、当老年代也满了之后,系统迫不得已进行FJC,此时系统停止,这也是最不希望看到的。这也是GC调优的核心所在减少FGC次数。
除此之外还有其他情况对象会直接存放进入老年代:
1、长期存活对象:CMS手机其实6岁,默认是15岁。
2、对象动态年龄判断:当对象大于s区域的50%后,这个参数可以设置,直接进入老年代,这个机制是在YFC之后触发
3、大对象,eden区已经存放不下,这个对象大小的阈值可以通过参数设置但只针对serial 和ParNew。为了避免大对象的复制内存操作降低copy算法的效率。
4、老年代空间分配担保机制
3、JVM垃圾收集机制
3.1、GC的基础概念
较为低级的语言中仍然存在内存的申请和销毁,比如C: malloc(申请内存),free(释放内存); c++ 中的new和delete。但是在java中我们只会new 对象 却从未手动释放内存。 这得益于JVM的一个重要功能,内存自动回收。优点也是显而易见的,因为如果是手动回收会带来两个不可避免的缺点:1、忘记回收,2、多次回收。会带来内存的溢出或者空指针。
3.2、什么是垃圾
在JVM为我们定义如果一个对象不再被任何其他对象引用,则称之为垃圾对象。一堆垃圾对象指的是互相引用但没有任何其他对象指向你。
3.3、如何定位垃圾
常用的有两种算法:
1、Reference count(引用计数算法)
给对象中添加一个引用计数器,每当一个地方引用它,计数器就加1;当引用失效,计数器减1;任何时候计数器为0的对象就是不再被使用的对象。
这个方法实现简单效率高,但是存在无法处理循环引用的问题,所以主流JVM都没有选择这个算法。
2、Root searching(根可达算法)
将“GC Roots”对象作为起点,从这些节点往下搜索引用的对象,找到的就标记为非垃圾,其余标记为垃圾对象。GC Roots 包括:线程栈的本地变量、静态变量、本地方法栈的变量等等。
3.4、常见的垃圾回收算法
Mark-Sweep(标记清除) 找出垃圾,然后清除掉。位置不连续,产生碎片。
Copying 一分为二,copy空白区域一次清除所有内存。浪费空间
Mark-compact(标记压缩)在标记和压缩的同时做了一次整理,从而产生连续的内存。效率低下。
3.5、常见的垃圾收集器
JVM底层通过C++实现的垃圾收集线程。
Serial 串行回收,比较老的版本 年轻代,单线程
A stop-the-world,copying collerctor which uses a single GC
Parallel Scavernge(PS)并行回收
A stop-the-world, copying collector which uses multiple GC threads
有了串行和并行,就有了如下分类:
ParNew 配合使用CMS的并行回收
Serial old
Parallelold
Concurrent Mark Sweep old
并发的,垃圾回收和应用程序同时运行,降低STW time(200ms)
G first(10ms)
ZGC zero stop the world(1ms)
Shenandoah
Epsilon
1.8默认的垃圾回收器是PS+Parallel Old,目前流行的是G1.
4、GC Turning
4.1、JVM命令行分类:
标准 : - 开头 java -version
非标准 -X 开头 java
不稳定 -XX 下个版本可能取消 java -XX:Print
常用
XX:+PrintFlagsFinal (设置最终生效值)
-XX:+PrintFlagsinital(默认值)
-XX:+PrintCommandLineFlags ming(命令行设置)
4.2、性能监控工具:
1、Java自带的 jvisualvm 可视化监控进程资源以及JVM状态。
2、阿里巴巴 arthas java诊断工具,开源免费。甚至可以反编译先上代码查看错误。
3、top -c 查找 cpu 消耗排行,然后找到该进程下cpu利用率最高的线程,转化内十六进制,使用 jstack 查看进行信息,然后就可以根据行号找到代码。
以上三种可以用来排查和处理线上的内存溢出问题。
4.3、案例
系统平时很平稳,每当系统存在促销时,后端频繁的FGC。
通过如下命令初始化JVM内存大小
java -Xms3G -Xmx3G -Xss1M -XX:MetaspaceSize=512M -XX:MaxmetaspaceSize=512M
这里存在的问题是,由于触发年龄的动态判断,导致这些大于60M的对象都无法进入Eden区进行copy清除,而且由于这些Order对象本身只存在于订单产生过程,实际也就存活几秒根本没必要进入老年代,所以解决方案也很简单,增大Eden区和S区的大小,适当减少old区避免对象年龄动态判断。