前言
“Write Once Run anywhere” 是得益于JVM,工作了将近一年的时间也明白了,最重要的还是思想结构和底层的实现,因为就算新技术层出不穷,它们也只不过是在锦上添花而已。
本文是我是从《深入理解Java虚拟机》总结而来,如果有什么说的不对的地方,还请各位看官指出,我还进行改正
正文
JDK,JRE,JVM三者之间的关系
JDK包含JRE,JRE包含JVM
内存溢出诊断
通过一个 VM argument进行设置 -xx: +HeapDumpOutOfMemoryError
这个命令会导出一个分析文件,需要下载一些工具对这个文件加以分析。
还可以通过JDK自带的可视化工具 console.exe 进行监控。
JVM分类
JAVA虚拟机内存管理
java虚拟机在执行程序的时候会把它所管理的区域划分成不同的数据区。
内存区域可以分为两个部分:
1.线程共享区
2.线程独占区
内存区域之程序计数器
是一块较小的内存空间,是一个当前线程所执行的字节码的行号指示器
字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
如果执行的是线程的java方法,计时器记录的是虚拟机字节码的指令地址,
如果执行的是native方法,那么这个计数器的值是空的(Undefined)
那么另一个问题:为什么需要用程序计数器保存执行的行号呢,是为了线程上下文切换,保证了线程不会错乱,线程只是执行操作,而并不会保存数据。
内存区域之虚拟机栈
描述的Java方法运行的内存模型
每一个方法的调用到完成都对应着栈帧在虚拟机栈中入栈和出栈的过程。
栈的区域是固定的,当我们不断调用方法,就会不断产生栈帧进入栈中,如果超出了栈的大小,就会出现stackOverflow的异常,想象平常最容易出现的场景就是递归,如果没写好的话,无止境的递归。
内存区域之本地方法栈
本地方法栈为虚拟机执行native方法服务
而虚拟机栈为虚拟机执行java方法服务
这就是二者唯一区别的区别,在*hot spot VM中这两个区域并没有明显的区分。
之所以开辟这个区域,是为了方便和系统交互,使用java和操作系统交互,有不便之处,所以最顶层的ClassLoader采用的C++编写。
内存区域之堆
内存中最大的一块。
存放对象的实例。
垃圾收集器管理的主要区域,所以很多人称之为GC堆
如果堆内存溢出,会产生OutOfMemeory的Error
-Xmx -Xms, 这两个VM参数,可以修改堆大小
内存区域之方法区
很多人称之为永生代。
垃圾收集在这个区域比较少见。
存储虚拟机加载的类信息(类的版本,字段,方法,接口),常量,静态变量,即时编译器(JIT)编译后的代码等数据。
可能出现OutOfMemory
运行时常量池
属于方法区的一部分
存放编译时生成的字面量,以及符号引用
小例子:
String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
在这里 a与b的地址是相同的,这收益于常量池,而c与a,b是不相同的因为new是直接在堆中开辟了一条内存空间,不受常量池影响的
直接内存大多时候也被称为堆外内存,自从 JDK 引入 NIO 后,直接内存的使用也越来越普遍。通过 native 方法可以分配堆外内存,通过 DirectByteBuffer 对象来操作。
对象创建
给对象分配内存的方式:
具体使用哪一种方式,是由堆内存是否规整决定的,是否规整是由垃圾回收机制决定的,如果垃圾回收会把区域变得相对完整则使用指针碰撞,如果是零散的,则使用空闲列表。
对象的结构
对象的大小必须是八的整数倍。
对象结构:
对象的访问定位
- 使用句柄:有点像是一种间接寻址的感觉在堆内存中,存在一个句柄池,引用指向了句柄池中的一块地址,然后句柄池再指向堆内存,使用这种方法最大的好处就是存储的是稳定的句柄地址。
- 使用指针:引用直接指向堆内存。最大的好处就是速度快,节省了一次指针定位的开销
虚拟机参数
1.-Xms 设置堆的最小值
2.-Xmx 设置堆的最大值
3.-Xss 设置栈容量
垃圾回收
打印GC的详细信息 需要在VM 参数中加入下面两个参数:-verbose:gc -xx:PrintGCDetail
如何判定对象为垃圾对象?
1.引用计数法
在对象中添加一个引用计数器,当有地方引用时+1,当引用失效-1。
但是一般并不使用,因为如果存在引用的相互依赖那么引用计数法将会失效。
2.可达性分析
从GC root 节点,向下搜索,如果一个地址对于GC root对象(包括 虚拟机栈中的局部常量表中的引用的对象,方法区中类静态属性引用的对象,方法区中常量所引用的的对象,本地方法栈中引用的对象),再也没有任何可走的路径,那么将会把在堆中的整个内存都给回首掉。
这里说的最多的字眼就是引用,Java 中一共包括四种引用方法:
- 强引用
垃圾回收器永远不会回收掉强引用的对象
- 软引用
有用但是非必须,在内存即将溢出之前如果有软引用会调用第二次GC,如果还是溢出,才会曝出异常。使用SoftReference来使用软引用
- 弱引用
非必须的引用,只能存活到下一次GC之前,无论内存是否足够,使用WeakReference 来使用弱引用。
- 虚引用
这种引用存在的目的是,当这个引用的对象被回收的时候,我们会得到一个通知,提供PhantomReference来实现虚引用。
大多数情况下,回收的都是堆区,很少收集方法区,那是因为这样做性价比很低。
如果回收方法区的话,主要回收两种:废弃常量,无用的类。
回收策略:
通过可达性分析算法,首先标记有哪些是需要清理的,然后再将它们进行清除。这个算法简单,但是存在的问题就是效率问题和空间问题。被标记可能十分分散,清理后,在内存里就会出现特别零散的空间,不利于日后开辟空间使用。
复制算法,将堆内存划分成两份,然后操作其中的一份,当需要进行垃圾回收的时候,复制算法会讲没有被回收的实例复制到另一份中去然后,将原来的所有的(不管有没有被回收的都删除掉)删除掉,然后在另一份中进行继续操作,下次在GC的时候,就和刚刚的操作一样,简单的说就是两个区域交替的工作。这样有效的解决了标记-清除算法的效率问题。但是这个问题,造成了堆内存中有空间浪费的情况
现在大多数虚拟机新生代都是采用了这种回收策略。
多说一点,在hotspot中,新生代会有一个eden区,两个survior区,比例为8:1,每次一个eden区和一个survior区被占用,也就是说只会有一个survior区被浪费掉。
一般应用与老年代,因为复制算法消耗空间,可能需要内存担保。
这个算法是将不需要GC的对象移向内存的一段,然后将除了一端区域界线外的对象全部清除掉
根据不同的内存区域(新生代,或者老年代),选择不同的GC算法。
新生代使用复制算法,而老年代时候标记-清理,或标记-整理,可以做到每一块都因地制宜。
垃圾回收器:
不同的垃圾回收器对应不同的使用场景。
垃圾收集器的不断推尘出新,其实就是一个不断缩短垃圾回收时间的过程。
-
Serial
这就导致了一个问题,多线程并发运行,但是需要垃圾回收了,那么所有线程都被阻塞,只有垃圾回收线程在运行,直到回收完毕,其他线程才继续运行。
这个回收器对于运行在client端是一个好的选择。
-
Parnew
开多个线程"打扫卫生",效率更高,多个线程共同工作的时候,还是会导致阻塞。
- 复制算法(新生代收集器),可以与Cms(老年代收集器)共同使用
-
Parallel scavenge
- 复制算法(新生代收集器),不可以与Cms共同使用
- 多线程收集器
- 达到可控制的吞吐量(吞吐量:cpu用于运行用户代码的时间与cpu消耗的总时间的比值)
吞吐量 = 执行用户代码的时间/(执行用户代码的时间 + 垃圾回收的时间)
- Cms(Concurrent Mark sweep)
可以边扔垃圾边打扫。
-
G1
- 使用标记-整理算法
-
优点
-
工作过程
-
工作原理
- G1与其他的收集器在内存布局上有很大的差别,它是将内存划分成了一块一块可以不连续的region,虽然保留新生代,老年代,但是已经不在物理隔离。在后台会维护一个优先列表,每次根据允许的收集时间,回收掉价值最大的region区,所以这个收集器叫 Garbage first。
上面有一个概念,容易让人混淆,那就是并发和并行,举个例子,并行就是你去看病,医院有多个看病的医生,而并发就是有多个病人找了同一个医生。
内存分配
内存分配原则:
第四十九节:虚拟机工具
虚拟机工具:
-
JPS:java process status
- -m 运行时传入的参数
- -v 虚拟机传入的参数
- -l 详细的类信息,或者jar包信息
- Jstate:监控虚拟机的各种运行状态的,例如类装载,内存,垃圾回收,JIT编译等数据的
-
Jinfo:实时查看和调整虚拟机各项参数
- Jmap:用于生成堆转储快照,一般称为heapdump或dump
- Jhat:结合jmap生成的文件进行分析,形成可视化洁面
- Jstack:生成当前时刻线程快照
- HSDIS:生成JIT的反编译代码
- Jconsole:代替了JPS,并且可以查看远程进程的状态。
- VisualVM:多合一故障处理工具
性能调优例子
问题为将用户绩效考核信息处理为一个Excel,但是时不时的不定时间会出现卡顿。
解决思路:
根本原因为把一台tomcat的堆设置的太大,而且用户生成excel的时间比较集中,导致大对象不断的生成,导致老年代告急,所以经常Full GC,产生full gc后所有其他的工作线程被阻塞,所以导致会有时间空档期。
解决方案:在一台服务器上部署多个服务器构成集群,每个集群的堆分配4G。
后记
在今后的不断学习中,我会不断的更新这篇文章。