Java实践5之浅谈JVM 内存模型 理解jvm运行时数据区 类加载器的作用

Java实践5之浅谈JVM 内存模型 理解jvm运行时数据区 类加载器的作用

关于jvm 我个人认为哈,觉着它是很重要,但是实际项目用的恨少,但很多面试都问的非常的深入,猜测可能是由于面试者太多为了卡人设置的门槛。很多面试官和应聘者说的也都非常官方,很多跟网上一模一样,不知是否真的理解。据我所知就算是大厂,jvm除了一些调整内存等基础参数外,其他基本用的很少很少,只有一个或几个专门做优化、或针对业务场景的一些大厂开发小组,或者开发基础组件,才会用到,一般的业务基本用不到。
对于jvm和只能大家分享一下,我在开发中用到的一些的简单知识点。(太高深的,虽然我也背过八股文,但由于不理解很快就忘了,所以太高深的也不是很清楚。)

jvm是什么?

我对它的理解是,jvm是一个应用服务,运行在java和系统的中间,相当于多了一层代理。当我们想运行java程序时,它来帮我们对接系统。
在这里插入图片描述如果没有这层代理,那么会比较麻烦,1机器不认java的代码,2就算改成机器承认,那么无法兼容,linux和widows还有其他系统,各系统指令不一样,硬件不同的cpu指令也不一样。像C语言针对不同的平台,需要开发不同的代码。有了jvm后,我们无需特别关心系统、硬件,jvm会帮我们去处理,我们只需按照规范进行java编程即可。
这个代理的设计我认为非常重要,例如 spring配置面向切面,在执行方法前做点什么在执行方法后做点什么,这里就用到了代理。我们做的管理系统等等系统,如果没有系统,那么业务就需要操作数据库,学习编程语言,这种方式就非常不友好了,还有做Nginx做负载等等,很多地方都用到了代理这种设计。

jvm内存模型

对于jvm的内存结构,这个我认为要理解的。这里说的是理解,而不是背概念。只有理解了jvm的内存结构,我们才能进行更好的去开发,对代码或参数,进行有针对性的优化。
记得曾经碰到过一位同事,面试时关于jvm,回答的非常好,跟网上一样,堪称教科书式回答。入职后,一次在部署项目时,程序启动后经常会内存溢出,然后判定内存不够,接着不断修改配置,增加堆大小,折腾很久也没解决问题。最后讨论时,才发现,原来是方法区分配的大小是128MB,而war包中依赖使用的jar文件达到了180MB。
jvm管理的内存主要分为 虚拟机栈、本地方法栈、堆、程序计数器、方法区。
在这里插入图片描述

程序计数器:

程序计数器是记录程序运行的位置,每个线程都有。这个我觉着知道一下即可,在开发时它不需要调整或优化。
为什么需要程序计数器呢?因为cpu为每个任务分配的执行时间是非常短暂的,并且一个核心在同一时间只会执行一个线程的指令,在执行线程指令时,如果执行不完,则会先切换到其他线程进行执行,然后再切回来。为了线程切换可以恢复到正确执行位置,所以需要程序计数器。
这样我们也理解了,线程不可以太多,否则cpu切换线程太多会非常耗时。也理解了每个线程由于执行的速度都不一样,位置也不一样,所以每个线程都需要自己独立的程序计数器,所以是线程私有的。

虚拟机栈、堆、方法区(元空间)

虚拟机栈:这个用的不是很多,我觉着简单理解即可。借用官方的概念,每个线程在创建的时候,都会创建一个虚拟机栈,其内部保存的就是一个一个的栈帧(Stack Frame),对应着一次一次的Java方法调用。
虚拟机在运行的单位就是栈,我们的方法在运行前会入栈,运行时出栈运行,栈中无数据后,方法执行完毕。它的里面存放着,局部变量(int、byte、char、long、float等基础类型数据)、动态连接、对象引用(它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄)、返回地址等信息。
:这个就很重要了,在优化时我们调整内存、注意对象引用、垃圾回收,这块用的是最多的,出问题也是最多的。理解了它在开发中我们会避免很多麻烦。
堆是线程共享的内存区域,它是虚拟机管理内存中最大的一块。此区域的存放对象实例,Java世界里几乎所有的对象实例都在这里分配内存。
方法区:这个用的也不是很多,基本根据代码大小,静态变量等,调整一次就不用动了,出的问题比较少。
方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

说了那么多概念,具体他们是什么东西呢?(只是个人理解哈,希望会有帮助)

在这里插入图片描述
运行流程描述(绿色为方法区、蓝色为栈、红色为堆)
1、首先会加载类(class文件非java文件)和相关的类信息,放入方法区中。(绿色为方法区)
2、加载完成后,创建栈,把代码(method1、data、method2)入栈,入栈后进行出栈执行。(蓝色为栈)
3、先执行method1()方法,连接到方法区寻找相关代码,找到代码后,method1也要创建栈,入栈,然后出栈执行 查询方法 ,并把查询到的数据放入堆中。然后然会数据在堆中的地址。
4、main栈中,拿到地址把地址付给data。然后执行method2方法。
5、执行method2流程同method1,method2执行后,栈已无数据,方法结束。

理解上面的图、描述和每块存放的东西。那么在在开发时,需要注意哪些,就很容易就推算出来了。

1、首先虚拟机栈,我们注意每个方法的代码不要写很多,调用中调用的方法内容也不要太多,否则栈无法申请到足够的内存空间 会报OutOfMemoryError 内存溢出。调用的层级不要太多,比如递归层级太深,超过所允许的最大深度,会出StackOverflowError栈溢出。
(栈上分配:这个也比较简单从图中可以看到 比如method2中的j=1 局部变量,他是基础类型的,他直接就在栈里,不在堆中。出栈即释放资源,不需要gc来回收)
2、然后是方法区,Java1.8之前是永久代,Java1.8之后是元空间,这个我认为不是很重要(感兴趣的可以查一下,这个比较简单)。重要的是要知道里面存放的数据是什么,里面放着我们的class信息、一些静态变量等等,这些信息是比较固定的,由于存放类信息等,避免重复加载,所以他是公共的每个线程都可以访问。
现在我们知道了假如我们的class或静态变量等信息已经180MB,那么给方法区分配的空间量要大于180MB,并且还要估算静态变量、常量等信息,增加一些冗余,也不用设置很大,造成内存空间浪费。
3、最后是堆,这里的优化就比较多了,我们new的对象实例,比如从数据库查询到的数据、new出来的arraylist,new数组,等等没有static的话,那么它的实际数据会在堆中。如果数据量过大,那么就容易造成内存溢出。这时我们就要调整堆的内存,或者数据太大,每次取小块数据去处理。
堆中还区分年轻代、老年代,我们要根据业务去调整,它们的大小占比。并且还可以设置它们使用的垃圾回收器。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的。调用 native 本地方法使用。这个了解下即可,我们也不去修改,虽然也会出现StackOverflowError 和 OutOfMemoryError错误,但大概率都不是本地方法错误,而是我们的代码有问题。

垃圾回收器

关于垃圾回收器,这里就不多说了,有感兴趣的可以搜索一下实现细节,我认为实际使用中,大多情况使用默认的即可,像年轻带使用复制算法回收器、老年代使用CMS就可以了,一般有问题基本都是代码的问题。所以我认为这个虽然很重要,但实际使用的很少,只不过现在实在是太卷了,需要问的难一点细一点才好卡人,真正使用时我们不会去改他的源码。
比如像复制算法、标记清除、标记整理和对应的实现(比如CMS、G1回收器等等),大概理解一下 他们的回收步骤和优缺点 足够了,我们也不会去修改CMS或G1的内部代码进行调优。只是在部署时,配置下参数调整要使用的垃圾收集器而已。当然理解它的算法详细的细节更好,可以运用在别的地方。

Classloader

关于classloader在实际项目中我用的真心不多,很少很少,我咨询过一些做大项目的研发,他们一般关注的也是代码,只有一些特殊的需求,才会用到classloader。具体的双亲委派机制等等概念,也比较简单,就是有几个垃圾收集器,带层级的,先交给上一级处理,上一级处理不了下一级再处理,具体大家可以自行去搜索一下了,这里简单介绍一下我使用到的场景。

场景一:jar冲突

场景是这样的简单描述一下,有1.0jar包和2.0jar包,A项目用的1.0jar包,B项目用的2.0的jar包。

1、SelfModel 1.0代码(打jar包):

public class SelfModel{
  public void hello() {
    System.out.println("i am hello");
  }
}

2、SelfModel 2.0代码(打jar包) 可以看到hello方法已经没有了:

public class SelfModel {
  public void hello2() {
    System.out.println("i am hello2");
  }
}

3、新建A(maven)工程 创建类,引用SelfModel 1.0,打成jar包。

public class Test2 {
	public void sayhello() {
		SelfModel sm=new SelfModel();
		sm.hello();
	}
}

4、新建B(maven)工程 创建类,引用SelfModel 2.0的 jar和引用B工程的jar。

public static void main(String[] args) throws Exception { 		SelfModel sm = new SelfModel(); 		
sm.hello2();
Test2 t=new Test2(); 		
t.sayhello();
}

运行后发现报错,SelfModel和Test2中引用的SelfModel版本不一致,冲突了。
解决方式,就是把依赖工程Test2工程从pom中删掉。自定义加载器去加载加载它,然后通过反射了去执行。
5.修改后:

public static void main(String[] args) throws Exception { 		SelfModel sm = new SelfModel(); 		
sm.hello2(); 		
File f2 = new File("SelfModel 1.0.jar"); 		
File f = new File( 				
"Test2的A工程.jar"); 		
URL[] urls = new URL[] { f.toURI().toURL(), f2.toURI().toURL() }; 		URLClassLoader urlClassLoader = new URLClassLoader(urls, null); 		
Class clazz = urlClassLoader.loadClass("Test2class文件"); 		Method m2 = clazz.getMethod("sayhello"); 		m2.invoke(clazz.newInstance());       
 //这样就不冲突了
  		sm.hello2(); 	
}

关于jar冲突,这个我用的很少,我认为正常的解决方式应该是去排除冲突的jar,或应该对一方进行升级,再或者对源码进行修改,让他们变成一致。

场景二:动态加载

这个场景我个人用的也不多,也就1次到2次,动态加载,就是我们替换class或者jar包后,不用重新启动项目,让程序可以自动加载新的class。

public class SelfClassLoader extends ClassLoader {
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		Class clazz = null;
		try {
			String classFile = "C:\\project\\Test.class";
			//读取class
			RandomAccessFile raf = new RandomAccessFile(classFile, "r");
			FileChannel fc = raf.getChannel();
			ByteBuffer buffer = ByteBuffer.allocate((int) raf.length());
			fc.read(buffer);
			clazz = defineClass(name, buffer.array(), 0, buffer.position());
		} catch (Exception e) {
		}
		return clazz;
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static void main(String[] args) throws Exception {
		while (true) {
			SelfClassLoader scl = new SelfClassLoader();
			Class clazz = scl.findClass("Test");
			Method m2 = clazz.getMethod("hello");
			m2.invoke(clazz.newInstance());
			//每隔10s去加载一次
			Thread.sleep(10000);
		}
	}
}

大家可以根据实际业务,去定时加载新的class、或使用WatchService监控文件的变化、避免每次重新加载,也可以做缓存等等。

场景三:class解密

根据上面例子,那么class的加密解密也很简单,这里就不上代码了。把需要的class加密,再使用时自定义类加载器 去解密,然后调用defineClass即可。

总结

本章JVM 实际上JVM知识点其实有很多,但实际工作中用到的不多。我认为比较重要的有:
1、jvm 内存模型: 栈、堆、方法区(元空间)都是什么。这个是重点 一定要理解,主要是知道里面到底存放的是什么东西。还有jvm的一些配置参数。大家也要有个基础的印象,比如修改堆内存等等一些基础参数,需要时可以去查询。
2、垃圾收集器 大概了解下即可。
3、Classloader这个用的也不多大概了解下有印象即可。使用时再查。

好了这篇分享完了,希望对大家会有帮助,文章中有哪些错误,或者理解的不对,欢迎大家多多和我交流、批评和指正

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值