JVM(一)——JVM及Java内存模型

本文详细介绍了JVM的概念、生命周期、内部结构及Java内存模型等核心内容。涵盖了JRE、JDK与JVM的区别,JVM的启动流程,内存区域如堆、栈、方法区的作用,以及volatile关键字的使用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、JRE / JDK / JVM

(1)JRE(Java Runtime Environment,Java运行环境):运行环境,也就是Java的平台,所有的Java程序都在该平台下运行。

(2)JDK(Java Development Kit,Java开发工具):程序开发者用来编译、调试Java程序。JDK也是Java程序,需要在JRE上运行。为了保证JDK的独立性,在JDK的安装过程中也需要安装JRE。在JDK目录下有一个目录:jre,也就是JRE相关的包存放在该处。

(3)JVM(Java Virtual Machinel,Java虚拟机):是JRE的一部分,是一个虚拟出来的计算机。在该虚拟计算机上模拟出Java的运行环境:堆、栈……及指令系统。

——虚拟机:

虚拟机指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。如VMWare、VisualBox、JVM。

区别:

VMWare和VisualBox是使用软件模拟物理CPU的指令集,显示的系统。

JVM使用软件模拟Java字节码的指令及,只是单纯的软件模拟硬件,在现实中并没有这样一台机器。

二、JVM虚拟机的生命周期

(1)虚拟机产生的起点:

Java虚拟机实例通过调用某个初始化类的特殊(main())方法:这个方法必须是共有的(public),无返回值的(void),静态的(static),并且可以接受一个字符串的数组作为参数(String[] args)。

如果方法名不是main是否可以实例化虚拟机??

(2)虚拟机结束点:

main()方法执行结束。System.exit()也可以使虚拟机结束。线程、内存空间部分区域(堆)也会进行划分。

三、JVM运行机制

(1)JVM启动流程

<1>使用java + 启动类命令启动JVM。

<2>装载配置:在当前路径中寻找跟系统版本匹配的配置文件。

<3>根据配置文件寻找JVM.dll(JVM的主要实现):初始化JVM,获得JNIEnv接口。JNIEvn接口提供了大量JVM交互操作,例如查找一个类。

<4>找到main()方法。

(2)JVM内部结构

<1>PC寄存器

每一个线程拥有一个PC寄存器,当线程开始时会分配一个PC寄存器,总是指向下一条指令的地址,执行本地方法时PC的值为未定义的(undefined)。

<2>方法区

保存装载的类的信息,包括类型常量池、字段、方法信息、方法字节码。

永久区(Perm):保存相对静止、相对稳定的数据。

<3>堆

和程序开发密切相关。应用系统对象都保存在堆中,所有线程共享Java堆。

不同的GC对应不同的堆,对分代GC来说,堆也是分代的。

GC的主要工作区间:

<4>

线程私有的。栈是由一系列帧组成的,也叫帧栈。帧放一个方法的局部变量、操作数栈、常量池指针。每一次方法调用创建一个帧,并压入栈中。

局部变量表:包含函数的参数和局部变量。

栈溢出?

Java没有寄存器,所有的参数调用都使用操作数栈。

栈上分配:new的对象都存放在堆上,在使用结束之后存在回收问题。在函数中至声明一个对象则是在栈上分配,不会出现内存泄漏。小对象在没有逃逸の情况下直接分配在栈上,可以自动回收,减轻GC压力。大对象或者逃逸对象无法分配在栈上。

<5>本地方法栈

(3)栈、堆、方法区的交互

(*问题)

为了能让递归函数调用的次数更多一些,方法应该怎么做?

为了让 JVM 中递归函数能够调用更多的次数,可以考虑以下几种方法:

  1. 优化递归算法:尝试将递归转换为迭代,或者使用尾递归优化(如果编程语言支持)。尾递归是指在函数的最后一步操作中进行递归调用,这样可以减少栈空间的消耗。
  2. 增加栈空间大小:通过 JVM 参数来增加线程的栈空间大小。例如,使用 -Xss 参数可以指定线程栈的大小,但要注意不要设置得过大,以免导致内存浪费。
  3. 避免不必要的对象创建和数据存储:在递归函数内部,尽量减少创建新的对象或者存储大量的数据,以降低内存占用。
  4. 分治策略:将问题分解为更小的子问题,每次递归只处理一部分,而不是一次性处理整个问题。
  5. 缓存中间结果:如果在递归过程中存在重复计算的部分,可以使用缓存来存储已经计算过的结果,避免重复计算。

例如,如果是计算斐波那契数列的递归函数,可以使用一个缓存数组来存储已经计算过的斐波那契数,避免每次递归都重新计算。

需要注意的是,过度的递归可能会导致性能问题和栈溢出错误,在实际应用中要谨慎使用,并根据具体情况选择合适的优化策略。

四、Java内存模型

每一个线程有一个工作内存和主内存。工作内存存放主存中变量值的拷贝。

当数据从主内存中复制到工作存储时,必须出现两个操作:

(1)由主内存执行读(read)操作。

(2)由工作内存执行相应的load操作。

当数据从工作内存拷贝到主内存时,也出现两个操作:

(1)由工作内存执行存储(store)操作。

(2)由主内存执行相应的写(write)操作。

每一个操作都是源自的,即执行期间不会中断。对于普通变量,一个线程中更新的值不能马上反应在其他变量中。如果需要早其他线程中立即可见,需要使用volatile关键字。

(*问题)线程内存、本地内存、主内存的联系?

五、volatile关键字

一般认为volatile比锁(重量级锁)性能好(不绝对,锁的优化)。选择使用volatile的条件是:语义是否满足应用。

(1)可见性:

一个线程修改了变量,其他线程可以立即知道。

(*)保证可见性的方法

(1)volatile

(2)synchronized——(unlock之前写变量值回主内存)

(3)final——(一旦初始化完成,其他线程就可见)

(2)有序性

在一个线程内,操作都是有序的。

在线程之外观察,操作都是无序的(指令重排或者主内存同步延时)。

(3)指令重排

破坏线程间的有序性。

编译器不考虑多线程间的语义:可重拍、不可重排。

保证有序性的方法:synchronized

(*)指令重排的基本原则

(1)程序顺序原则:一个线程内保证语义的串行性。

(2)volatile规则:volatile变量的写,先发生于读。

(3)锁规则:解锁必然发生在随后的加锁前。

(4)传递性:A先于B,B先于C,那么A必然先于C。

(5)线程的start()方法优先于它的每一个动作。

(6)线程的所有操作优先于线程的终结(Thread.join())。

(7)线程的中断(interrept())先于被中断线程的代码。

(8)对象的构造函数执行结束先于finalize()方法。

六、字节码(bytecode)运行的两种方式

(1)解释运行:读一句执行一句。

(2)编译运行(JIT):将字节码编译成机器码,直接执行机器码,即运行时编译。

编译运行编译后性能有数量级的提升,一般是10倍以上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值