该文章总结于《深度解析java虚拟机》,若有侵权,请联系作者删除。
一、运行时数据区域
1.程序计数器
是一块较小的内存,负责在字节解码器工作的时候通过改变计数器的值获取下一条需要执行的字节码的指令,如分支,循环,跳转,异常处理等基础功能。在多线程中每个线程都有自己独立的程序计数器,它是线程私有的内存。程序计数器是JVM中唯一没有OutOfMemmoryError异常的内存。
2.Java虚拟机栈
Java虚拟机栈也是线程私有,他的生命周期和线程相同,每个方法执行的同事都会创建一个栈帧用于存放局部变量、操作数栈、动态链接、方法出口等信息。Java虚拟机栈也叫局部变量表,在程序编译期间完成内存分配,在运行期不改变局部变量表的大小。如果线程请求大于栈深将抛出StackOverflowError异常,如果扩展时请求不到足够的空间抛出OutOfMemmoryError异常。
3.本地方法栈
Java虚拟机栈和本地方法栈的像似,区别在于Java虚拟机为虚拟机执行字节码服务,本地方法栈为虚拟机使用到的Native方法服务。本地按方法栈也可以抛出StackOverflowError异常和OutOfMemmoryError异常。
4.Java堆
Java堆是Java虚拟机分配的最大的一块内存。Java堆被所有线程共享的一块内存,在虚拟机启动时被创建。用于存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域“GC堆”,按照收集器的分代收集方法Java堆还分为:新生代和老年代。再分细点:Eden空间、From Survivor空间、To Survivor空间。Java堆可以是物理上不连续但逻辑上连续的一块内存。
5.方法区
方法区和Java堆一样,是各个线程共享的内存区域,它用于存储被虚拟机加载的信息、常量、静态变量、及时编译器编译后的代码数据等,虽然Java虚拟机吧方法区描述卫队的一个逻辑部分,但是他的一个别名Non-Heap(非堆),目的是为了将它与堆区分开来。
6.运行时常量池
运行时常量池是方法区的一部分。用于存放编译期生成的各种字面量和符号引用。当常量池无法申请到足够内存时抛出OutOfMemmoryError异常
二、Java虚拟机对象
1.创建对象
当虚拟机遇到一条new指令时,(1)首先去检查这个指令参数书否能在常量池中找到其符号引用,并检查这个符号引用代表的类是否被加载、解析、初始化过如果没有那就先执行相应的内加载。在类加载检查通过后就将在堆中用“指针碰撞”(内存连续)或“空闲列表”(内存不连续)方式分配空间。(2)初始化,在对象头(Object Header)中初始化信息如:对象是哪个类、怎样找到元数据信对象hush、GC年龄等设置。(3)执行
2.对象的内存分布
在HostSpot虚拟机中,对象在内存中的存储布局分三块区域:对象头、实例数据和对齐填充。
3.对象的使用
对象的使用取决于虚拟机的实现方式目前主要有使用句柄和直接指针两种。
使用句柄
直接指针
三、几种内存溢出
1.Java堆溢出
Java堆用于存储对象实例,只要不断创建并且避免垃圾回收机制清除这些对象,当对象达到堆得最大容量就会产生溢出现象。
2.虚拟机栈和本地方法栈溢出
1)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
2)如果在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemmoryError异常。
结论;新建线程时分配的栈内存越大剩下的内存就越小,在继续创建线程分配栈内存时反而容易产生内存溢出现象。
3.方法区和运行时常量池溢出
可以看到,运行时常量池溢出,在OutOfMemoryError后面的提示信息是“PermGen space”。
四、内存分配策略
3个问题:
1)哪些内存需要回收?
2)什么时候回收?
3)如何回收?
1.判断内存是否回收
1)引用计数法
给对象添加一个引用计数器,每当有一个地方用它就加1,引用失效时就减1。任何时刻计数器为0的对象是不可能再被使用的。但主流的Java虚拟机没有选用这种方法,其原因主要是因为它很难解决对象之间相互循环引用的问题。
如上,虚拟机还是回收了内存,这也证明了虚拟机并没有使用引用计数算法回收内存。
2)可达性分析算法(根搜索法)
通过一系列的称为“GC Root”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用连链“Reference Chain”,当一个对象到GC Roots没用任何引用连相连时,证明斥对象不可用。
3)再谈引用
JDK1.2之后Java对引用的概念进行了扩充:强引用、软引用、弱引用、虚引用。引用强度依次递减。
对象及时在可达性分析算法中视为不可达也还有生存的机会。它的死亡经历两个阶段:当对象没有覆盖finalize()方法或finalize()方法被虚拟机调用过,将没有必要马上回收内存它将会被第一次标记。如果这个对象被判定为有必要执行finalize()方法,它将会被放置到F-Queue队列中它将会第二次标记。如果它还不创建对象关联,那将会被回收,及finalize()方法被调用。
2.垃圾清除方法
首先垃圾回收的主要对象是堆区,少量在方法区和方法区的常量池。而虚拟机栈、本地方法栈和程序计数器随线程共存亡不用Java虚拟机回收。
1)标记清除算法。
如同它的名字,标记清除法分为标记和清除两个阶段。两个弊端1.效率问题,先标记再清除,效率自然降低。2.清除后会留下大量不连续的内存碎片。
2)复制算法
为了解决效率问题,复制算法出现了。它将可用的内存分为两块,每次只使用一块。这块用完了就将还活着的对象复制到另一块内存上,然后再把先前使用的内存一次性清理掉。弊端,内存缩小了原来的一半。
3)标记-整理法
根据老年代的特点,标记-整理算法出现了。让存活的对象都向一端移动,然后直接清理掉端边界意外的内存。
4)分代收集算法
当前商业虚拟机都采用“分代收集”算法,这种算法没什么新思想,只是根据随想存活周期的不同将内存分成几块。
五、垃圾收集器
上图是7种不同的垃圾收集器,连线表示两种垃圾收集器可以协同工作。
1.Serial收集器
Serial别名“Stop World”,单线程。在它执行垃圾回收操作师会在用户看不见的情况下停止所有正在正常运行的所有线程。用户体验不太好,但是即便这样它依然是虚拟机运行在Client模式下的默认新生代收集器。专心做收集工作所以效率高。
2.ParNew收集器
Par New就是Serial收集器的多线程版本。除了多线程为其他和Serial相同,所以它们有很多的相同代码,相比Serial收集器它并么有什么创新之处。
3.Parallel Scavenge收集器
Parallel Scavenge是新生代、多线程的收集器,它可以达到一个可控制的吞吐量。
吞吐量=用户代码运行时间/虚拟机运行时间
(其中虚拟机运行时间=用户代码运行时间+垃圾收集时间)
Parallel Scavenge收集器提供了两个参数用于精准控制吞吐量:①控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis参数 ②直接设置吞吐量大小-XX:GCTimeRatio参数。
4.Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记整理”算法
5.Parallel Old收集器
Praallel Old是Parallel Scavenge收集器的老年代收集器
6.CMS收集器
CMS收集器是一种以获取最短停顿时间为目标的收集器。它是基于“标记-清除”算法实现的具体步骤包括:
1)初始标记
2)并发标记
3)重新标记
4)并发清理
7.G1收集器
G1是一款面向服务器端应用的垃圾收集器。收集范围与其他GC垃圾收集器相比。G1具备如下特点:
1)并行与并发
2)分代收集
3)空间整合
4)可预测的停顿
G1将整个堆划分为等大的(Region),根据允许的收集时间优先收集价值最大的Region,保证在有限时间尽可能的提供收集效率。
8.直接进入老年代的几种情况
1)大对象直接进入老年代
2)长期存活的对象直接进入老年代
最后java虚拟机调优常用参数地址如下:
https://www.jianshu.com/p/5be9c56171dd