一、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 中递归函数能够调用更多的次数,可以考虑以下几种方法:
- 优化递归算法:尝试将递归转换为迭代,或者使用尾递归优化(如果编程语言支持)。尾递归是指在函数的最后一步操作中进行递归调用,这样可以减少栈空间的消耗。
- 增加栈空间大小:通过 JVM 参数来增加线程的栈空间大小。例如,使用
-Xss
参数可以指定线程栈的大小,但要注意不要设置得过大,以免导致内存浪费。 - 避免不必要的对象创建和数据存储:在递归函数内部,尽量减少创建新的对象或者存储大量的数据,以降低内存占用。
- 分治策略:将问题分解为更小的子问题,每次递归只处理一部分,而不是一次性处理整个问题。
- 缓存中间结果:如果在递归过程中存在重复计算的部分,可以使用缓存来存储已经计算过的结果,避免重复计算。
例如,如果是计算斐波那契数列的递归函数,可以使用一个缓存数组来存储已经计算过的斐波那契数,避免每次递归都重新计算。
需要注意的是,过度的递归可能会导致性能问题和栈溢出错误,在实际应用中要谨慎使用,并根据具体情况选择合适的优化策略。
四、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倍以上。