JVM原理总结

1 jvm虚拟机图

1.1 JDK,JRE,JVM关系模型

在这里插入图片描述
JDK(java develop kit,java开发工具包)包含开发需要环境工具,编译(javac:java compiler)反编译工具(javap:可以反编译,也可以查看java编译器生成的字节码),监控工具和JRE
jdk对javap的解释
JRE(java runtime environment,java运行环境)包含JVM和运行java程序所需环境和JVM
JVM(java Java Virtual Machine,java虚拟机)可以理解成一个在电脑中虚构的机器,真正执行java程序的机器,可以跨平台(windows,linux,unix)等的核心所在

1.2 JVM模型图

在这里插入图片描述

2 JVM具体块区作用和涉及知识点

2.1 首选需了解内容

2.1.1 class文件结构

class文件为java文件编译后形成的二进制字节码文件
根据其中内容可以清楚它是用来描述类的信息,因为没有非常必要理解,大致理解其内容即可,深入理解可在官网
javaVirtualMacineTechnology

2.1.2 类加载机制

1>加载:将class文件读取到jvm,创建一个表示该类型的java.lang.Class类的实例,作为方法区这个类的各种数据的访问入口
2>验证:验证class文件合法(通过class头的描述信息验证),主要验证内容为文件格式、元数据、字节码、符号引用。可以通过设置参数略过。
3>准备:准备方法和变量的空间,设置为初始值(0值)。这些变量使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
4>解析:将符号引用(java描述中的变量名)改为直接引用(jvm内地址信息)
5>初始化:给static变量赋值,此处按照从父到子,从上到下的原则,有个例子CSDN颜群一道JVM面试题,答错率超90%
6>使用:对象的初始化、对象的垃圾回收、对象的销毁
7>卸载

2.1.3 jvm种类

Sun Classic VM
已经淘汰,是世界上第一款商用虚拟机,只能使用纯解释器(没有JITJust in time编译器)的方法来执行Java代码。
Exact VM
Exact Memory Management 准确式内存管理;
编译器和解释器混合工作以及两级即时编译器。
HotSpot VM
热点代码技术,使用最多的虚拟机产品,并非由Sun公司开发。官方JDK均采用HotSpot VM。
KVM
kilobyte 简单、轻量、高度可移植,在手机平台运行,运行速度慢。
JRockit
BEA公司开发,是世界上最快的Java虚拟机,专注于服务端应用,全部靠编译器执行。
J9
IBM开发 原名:IBM Techn0ology for Java Virtual Machine IT4j
Davik
不是java虚拟机,寄存器架构而不是栈结构,执行dex(dalvik Executable)文件。
Microsoft JVM
只能运行在windows下面。
Azul VM Liquid VM
高性能的java虚拟机,在HotSpot基础上改进,专用的虚拟机。
Taobao VM
淘宝公司开发

2.2 虚拟机栈

虚拟机栈用于存放虚拟机线程信息,用于加载类
1、栈帧:可以匹配上eclipse中断点的栈,每个栈帧会有自己的局部变量表、动态链接、方法出口
2、局部变量表大小在编译时决定

2.3 方法区

存储类的方法,接口,版本,字段。其中有常量池存储常量信息,比如字符串和数字等类中的常量

2.4 本地方法栈

调用的native方法(C语言方法)服务

2.5 程序计数器

占用较小分配空间,记录栈方法的动态地址,native方法为undefined。cpu分时分段工作,重新切换回具体方法时,不从头执行方法,继续栈帧里面的具体行执行可以理解为程序计数器的作用

2.6 堆,内存分配及GC

2.6.1 堆结构

堆分为新生代和老年代,HotSpot有永久代
1、新生代:分为eden(伊甸园)和survivor(幸存者区),一般为老年代的一半大小,survivor分为两个相等大小区(用于支持复制算法GC)
2、老年代:old,一般为堆内存大小的2/3(参数可控制),用于存放survivor区中超过年龄数的变量(参数可控制),或者过大对象可直接存放在老年代(参数可控制),老年代一般使用标记整理算法
3、永久代:

指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域. 它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。

2.6.2 对象分配

new对象会分配到堆内存中,首先分配给eden,若变量过大,有可能通过分配担保机制直接分给old,eden满了之后会进行一次minorGC,之后将仍然存活的对象分配到survivor,老年代通过majorGC和fullGC清理
经历过多次minorGC后的对象,会从survivor中分配给老年代

空间分配担保
-XX:+HandlePromotionFailure 开启
-XX:-HandlePromotionFailure 禁用
取之前每一次回收晋升到老年代对象容量的平均值大小作为经验值,与老年代的剩余空间进行比较,决定是否FullGC来让老年代腾出更多空间。

2.6.3 GC判断回收对象

1、引用计数法
根据被引用的计数判断,若引用数为0则可以回收,问题缺陷:两个变量互相引用,则均为1,其他环状引用都会导致此问题
2、root根可达性分析
根据GCroot的引用表,向外找引用,未被引用的对象或对象环着表示可以清理
可作为GCRoot的对象根节点

  • 虚拟机栈
  • 方法区的类属性所引用的对象
  • 方法区中常量所引用的对象
  • 本地方法栈中引用的对象

2.6.4 GC算法和收集器

1、复制算法
survivor中主要算法,回收可回收对象后,将其余内存空间整体复制到surviivorTo中

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。

2、标记清除算法
用可达性分析标记可回收的对象,然后清理,此算法会造成产生内存碎片,导致无法存储大变量引发GC

3、标记整理算法
old区主要算法,标记后整体整理排序,使剩余空间连续,相比其他算法效率慢
4、垃圾收集器

  • serial
    新生代的主要收集器,单线程,采用复杂算法,stoptheword(回收时停止其他一切线程)
  • parNew
    serial的多线程版本,stoptheword
  • parallel Scavenge
    新生代的收集器,复制算法,侧重于吞吐量和缩短用户线程的停顿时间,适用于多CPU后台服务
  • serial old
    serial的老年代版本,采用标记整理算法,单线程,stoptheword(回收时停止其他一切线程)
  • parallel old
    Parallel Scavenge的老年代算法,采用标记整理算法
  • CMS
    Concurrent Mark Sweep,也称为并发低停顿收集器,基于"标记-清除"算法(不进行压缩操作,产生内存碎片)。以获取最短回收停顿时间为目标。并发收集、低停顿。但是需要更多的内存(因为多次标记,初始标记,并发标记,重新标记)可理解为动态标记(初始标记stoptheword,重新标记为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录stoptheword)会导致产生浮动垃圾,并发清楚
  • G1
    目前最优的垃圾收集器,并行并发、分代收集、多种算法不产生空间碎片、追求高吞吐量和用户低停顿。面向服务端应用,针对具有大内存、多处理器的机器
    模型:
    在这里插入图片描述

2.7 整合知识点

1、若创建一个类对象,实例在堆内存中创建,类的方法和静态变量创建在方法区
2、jvm参数
iteye leichenlei jvm参数(全)
3、堆内存设置的大,fullGC时间也就会随之增长,30G内存fullGC一次4s算比较正常
4、思维导图
在这里插入图片描述

3 测试内存溢出

3.1 堆内存溢出

1、字符串长度方式溢出:
测试代码:

private static void testOldHeap() {
		List<String> list = new ArrayList<String>();
		String s = "ssa";
		list.add(s);
		while(true){
			list.add(s+list.toString());
		}
		
	}

测试原理:通过不断扩大list的大小和长度,以及单个list节点的长度试图塞满整个old区溢出
测试结果:成功,抛出 java.lang.OutOfMemoryError: Java heap space
2 数组长度大小方式溢出:
测试代码:

private static void testOldHeap2() {
		List<String> list = new ArrayList<String>();
		String s = "ssa";
		list.add(s);
		while(true){
			list.add(s);
		}
	}

测试原理:通过不断扩充list大小,将堆内存塞满,引起OutOfMemory
测试结果:成功 1

3.2 栈深度溢出

测试代码:

package testJVM;

public  class BlowUpJVM {  
	public void testHeapDeepOOM(){
		this.testHeapDeepOOM();
	}
} 
//		堆深度溢出  
		BlowUpJVM a = new BlowUpJVM();
		a.testHeapDeepOOM();

测试原理:通过不断调用自身构造方法创造递归深度
测试结果:成功抛出java.lang.StackOverflowError

3.3 虚拟机栈内存溢出

测试代码:

private static void testStockOOM() {
		while (true) {
			Thread thread = new Thread(new Runnable() {
				public void run() {
					while (true) {
					}
				}
			});
			thread.start();
		}
	}

测试原理:通过不断的创建线程,使虚拟机栈塞满溢出
测试结果:发现大概jdk1.7之后,此区域放在了直接内存区(系统内存),导致直接将电脑内存占满,然后死机了

付上完整代码:

package testJVM;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class testMain {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
//		jdk1.8		
//		JVM栈内存溢出  此方法极度消耗内存,会死机
//		testStockOOM();
		
//		堆深度溢出  java.lang.StackOverflowError  堆存储方法,所以堆内存溢出最简单的方法就是递归调用自身
//		BlowUpJVM a = new BlowUpJVM();
//		a.testHeapDeepOOM();
		
//		老年代堆满     java.lang.OutOfMemoryError: Java heap space
//		testOldHeap();
		testOldHeap2();
		
		//动态代理类放在了堆内存中,会被回收,没办法溢出
//		testPergemOutOfMemory3();
		
	}

	private static void testOldHeap2() {
		List<String> list = new ArrayList<String>();
		String s = "ssa";
		list.add(s);
		while(true){
			list.add(s);
		}
		
	}
	private static void testOldHeap3() {
		List<String> list = new ArrayList<String>();
		String s = "ssa";
		list.add(s);
		while(true){
			list.add(list.get(list.size()-1));
		}
		
	}
	private static void testOldHeap() {
		List<String> list = new ArrayList<String>();
		String s = "ssa";
		list.add(s);
		while(true){
			list.add(s+list.toString());
		}
		
	}

	private static void testStockOOM() {
		while (true) {
			Thread thread = new Thread(new Runnable() {
				public void run() {
					while (true) {
					}
				}
			});
			thread.start();
		}
	}
	public static void testPergemOutOfMemory3(){ 
		   while(true){ 
		   final BlowUpJVM oom = new BlowUpJVM(); 
		   Proxy.newProxyInstance(oom.getClass().getClassLoader(), oom.getClass().getInterfaces(), new InvocationHandler() { 
		         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
		            Object result = method.invoke(oom, args); 
		            return result; 
		         } 
		      }); 
		   } 
		} 

} 
package testJVM;

public  class BlowUpJVM {  
	public void testHeapDeepOOM(){
		this.testHeapDeepOOM();
	}
}

4 性能调优

4.1 调优工具

Sun公司自带了许多虚拟机工具,在bin目录下,其exe文件所依赖的源码在tools.jar包下,利用jar包中的文件可自己开发。
jps
Java process status
可查看本地虚拟机唯一id lvmid (local virtual machine id):
enter description here
可以加入参数:
enter description here
jps -m 运行时传入主类的参数;
jps -v 虚拟机参数;
jps -l 运行的主类全名 或者jar包名称;
也可以一块使用 jsp -mlv。
Jstat
监视虚拟机运行时的状态信息,包括监视类装载、内存、垃圾回收、jit编译信息。官方文档有操作命令:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
jstat -gcutil 3344 1000 10 每隔1000毫秒执行一次 10次
enter description here
注:元空间的本质和永久代类似,都是对jvm规范中的方法区的实现。不过元空间与永久代之间最大的区别是:元空间并不在虚拟机中,而是使用本地内存。因此在默认情况下,元空间的大小仅受本地内存限制。
jinfo
实时查看和调整虚拟机的各项参数
enter description here
第一条指令为:是否启用Serial垃圾回收,输出-,表示不启用;
第二条指令为:是否启用G1垃圾回收,输出+,表示启用。
jmap
命令jmap是一个多功能的命令。它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。
可实现与-XX:+HeapDumpOnOutOfMemoryError相同的效果
jhat
JVM heap Analysis Tool(分析堆)十分占据内存与CPU,使用较少。
jstack
生成线程快照,定位线程长时间停顿的原因,命令格式为:
jstack [option] vmid
可以通过Thread.getAllStackTraces()方法获取StackTraceElement,来代替jstack方法。
JConsole
JConsole是一种基于JMX的可视化监视、管理工具可进行内存管理、线程管理、查看死锁等。

4.2 案例

案例一

背景:绩效考核系统,会针对每一个考核员工生成一个各考核点的考核结果,形成一个Excel文档,供用户下载。文档中包含用户提交的考核点信息以及分析信息,Excel文档由用户请求的时候生成,下载并保存在内存服务器一份。64G内存。

问题:经常有用户反映长时间卡顿的问题。

处理思路:

优化SQL(无效) 监控CPU 监控内存发现经常发生Full GC 20-30s
运行时产生大对象(每个教师考核的数据WorkBook),直接放入老年代,MinorGC不会去清理,会导致FullGC,且堆内存分配太大,时间过长。
解决方案:部署多个web容器,每个web容器的堆内存4G,单机TomCat集群。

案例二

背景:简单数据抓取系统,抓取网络上的一些数据,分发到其它应用。

问题:不定期内存溢出,把堆内存加大,无济于事,内存监控也正常。

处理方法:NIO使用了堆外内存,堆外内存无法垃圾回收,导致溢出。

优化参考
博客园淡然~~浅笑性能优化 | JVM性能调优篇——来自阿里P7的经验总结

引用:

文章引用CSDN作者TJtulong文章深入理解java虚拟机(全章节完整)
文章引用博客园冰河团队【Java】几种典型的内存溢出案例,都在这儿了!
文章引用博客园chenxiangxiangJava虚拟机垃圾回收(三) 7种垃圾收集器
图片引用CSDN岛城小哥常见JVM面试题及答案(综合型)


  1. 此处参考他引中冰河团队的测试用例,但是有结果冲突,我这里成功抛出了oom,此处我认为list变量存储在堆内存老年代中,即使String.intern返回实际字符串,所以冰河团队的测试应有问题,这里可以抛出oom
    成功oom截图:
    在这里插入图片描述 ↩︎

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值