JDK > JRE = Java虚拟机 + Java核心类库
## JDK: JAVA开发工具包
bin:最主要的是编译器(javac.exe)
include:java和JVM交互用的头文件
lib:类库
jre:java运行环境
jdk主要面向开发者,具有java的编译功能。
jre主要面向用户,主要是class文件的运行,假如我们只有编译好的class文件和jre,那么就可以运行class了。
JVM组成结构:
(1)类加载器
(2)运行时数据区
(3)执行引擎
(4)本地库接口
Java程序运行的时候,编译器将Java文件编译成平台无关的Java字节码文件(.class),接下来对应平台JVM对字节码文件进行解释,翻译成对应平台匹配的机器指令并运行.
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到JVM 内存,然后再转化为 class 对象。
具体过程为:加载、链接、初始化
## 加载
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构(InstanceKlass),然后生成一个代表这个类的java.lang.Class对象。
## 链接
验证:确保加载的类信息符合JVM规范,没有安全方面的问题。
准备:正式为类变量(static)分配内存并设置类变量默认值的阶段,这些内存都将在方法区中进行分配。(静态变量的定义使用final关键字,这类变量会在此阶段直接进行初始化)
解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。符号引用就是一组符号来描述所引用的目标,直接引用就是直接指向目标的指针、相对偏移量或者是一个能直接定位到目标的句柄。
## 初始化
执行类构造器()方法的过程,类构造器()方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块的语句合并产生的。(直接访问父类的静态变量,不会触发子类的初始化。子类的初始化cinit调用之前,会先调用父类的cinit初始化方法。)
public class ZeroTest {
int i;
public void testMethod() {
int j;
System.out.println(i);
// Variable 'j' might not have been initialized
System.out.println(j);
}
}
// 因为i在初始化时有分配0,所有可以正常输出。但是j是局部变量,没有初始化就会报错。
个Class
时,该 Class
所依赖的和引用的其他 Class
也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入Class
,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类Class
都会被缓存,当程序需要使用某个 Class
对象时,类加载器先从缓存区中搜索该 Class
,只有当缓存区中不存在该 Class对象
时,系统才会读取该类对应的二进制数据,并将其转换成 Class对象
,存储到缓存区双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
一般情况下,由jvm指定的类加载器就是应用类加载器,jvm会自动调用其loadClass(String name)方法来开启类的加载过程。
## 双亲委派的优点
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
作用:保证应用程序的稳定有序。
## 虚拟机存储结构
(1)虚拟机栈:在方法调用和返回中发挥重要作用。每个线程都独享自己的虚拟机栈,栈中的存储单元是栈帧(Frames)。每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧
(2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一
(3)程序计数器:用于记录当前线程执行到哪里,保存指令执行的地址,方便线程切回后能继续执行代码
线程共享区:
(4)堆内存:Jvm进行垃圾回收的主要区域,存放对象信息,分为新生代和老年代
(5)方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
## JVM内存和直接内存
JVM内存和本地内存都属于(物理)内存的一部分,为什么要把它们分开讨论呢?因为目标不同,JVM是由JVM进程管理的一块内存空间,它可以对其中的内存进行自动垃圾收集。而本地内存是不受JVM管理,而且不受JVM内存设置的限制。
## 永久代(方法区)为什么要被元空间替换?
(1)为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。比如某个实际web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。
(2)对永久代进行调优是很困难的。
一个Native Method是一个Java调用非Java代码的接囗,该方法的实现由非Java语言实现,比如C。现在已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信。
Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界:它和虚拟机拥有同样的权限,并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
Java是编译与解释结合的语言,因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class
文件),这种字节码必须由 Java 解释器来解释执行。
ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。一旦线程不在存在,ThreadLocal 就应该被垃圾收集,但线程池有线程重用的功能,因此线程就不会被垃圾回收器回收,变量副本一直在内存中
## 解决方法
用完ThreadLocal一定要记得使用remove方法来进行清除。
JVM的主要任务是负责装载字节码到其内部,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令,与JVM运行时数据区一起完成代码执行流程。
JVM的执行引擎有两种:
解释器与JIT相辅相成,一般而言首先都是它发挥作用,不必等待JIT编译器全部编译后再执行,省去不必要的编译时间。并且随着程序的运行,JIT编译器会逐渐发挥作用,根据热点探测功能(即根据方法调用计数器或者回边计数器来确定热点代码, 一般而言for循环内的循环体会被确认为热点代码),将有价值的字节码编译成本地机器指令存储在方法区的JIT代码缓存中,换取更高的程序执行效率。
运行时,是一个封装了JVM的类。每一个JAVA程序实际上都是启动了一个JVM进程,每一个JVM进程都对应一个Runtime实例,此实例是由JVM为其实例化的。所以我们不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。一旦得到了一个当前的Runtime对象的引用,就可以调用Runtime对象的方法去控制Java虚拟机的状态和行为。
在完成初始化后,类就可以被应用程序正常使用了。当你调用一个方法时,JVM会为这个方法创建一个新的栈帧,并压入到当前线程的Java栈中。Java栈是线程私有的内存区域,用于存储每个方法调用的状态,包括局部变量、操作数栈、动态链接等信息。
先看这段代码:
public class Building {
private static final int CONSTRUCTION_YEAR = 1998;
public int calculateAge(int currentYear) {
return currentYear - CONSTRUCTION_YEAR;
}
}
public static void main(String[] args) {
Building building = new Building();
int age = building.calculateAge(2023);
}
building.calculateAge(2023)
时,首先JVM会通过对象引用(即building
)查找到类Building
,然后在类中查找calculateAge
方法的符号引用。Building
类中的符号引用找到calculateAge
方法在运行时常量池中的直接引用,获取改方法的内存地址。currentYear
和this
)存储到新栈帧的局部变量表中。calculateAge
方法的第一条字节码指令。calculateAge
方法的字节码。当执行到currentYear - CONSTRUCTION_YEAR
时,它会将currentYear
和CONSTRUCTION_YEAR
推入操作数栈,然后执行减法操作,并将结果推入操作数栈顶。calculateAge
方法后,JVM将操作数栈顶的结果(即年龄)作为方法返回值,并将calculateAge
方法的栈帧从Java栈中弹出。calculateAge
方法的返回值被推入调用者(即main
方法)的操作数栈中,并赋值给局部变量age
。main
方法的下一条指令。1. 局部变量表: 局部变量表被定义为一个数字数组, 保存返回地址参数、方法参数类型和方法体内的局部变量
注意: 基本数据类型(如byte/short/char)在存储前会被转换为int,boolean类型也会,对应的是0为false,1为true
2. 方法返回地址: 保存了PC寄存器的地址值,也就是调用者的PC计数器的值作为返回地址
3. 动态链接: 指向运行时常量池的方法引用。比如,描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
4. 操作数栈: 根据字节码指令,往栈中写入数据或提取数据。主要用于保存计算过程的中间结果,同时作为计算过程中临时变量的存储空间
## 符号引用 和 直接引用
符号引用(Symbolic Reference)是一种用来描述引用目标的一组符号,可以是任何形式的字面量,比如类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。符号引用是在编译期或者运行期间生成的,它不依赖于具体的内存地址,而是在运行时根据上下文信息来定位目标。符号引用的作用是为了在程序运行时能够找到对应的目标。
直接引用(Direct Reference)则是一种直接指向目标的内存地址或者偏移量,它可以是指向对象实例的指针、指向类的静态变量的指针、指向类的方法的指针等。直接引用是在程序运行时生成的,它依赖于具体的内存地址,可以直接被 CPU 所执行。
## 静态链接
在解析过程中,Java 虚拟机会将符号引用转换成直接引用,从而能够正确地执行程序。
## 动态链接
动态链接是指在程序运行时,根据需要动态地加载和链接代码。在Java中,我们可以使用类加载器来动态加载新的类,也可以使用反射机制来动态获取和调用类的方法和字段。如果直接使用直接引用,我们通常需要在编译时就确定所有的引用,而不能在运行时动态地加载和链接代码。
我们可以使用继承和多态来实现运行时多态。比如,如果一个子类重写了父类的方法,那么在程序运行时,如果我们调用该方法,就会根据实际的对象类型来选择调用子类的方法还是父类的方法。
这些特性都需要在程序运行时根据实际情况来动态地选择和加载代码,这就需要Java虚拟机能够动态地解析符号引用,找到对应的直接引用,从而实现动态链接和运行时多态。
静态链接和动态链接对应的方法的绑定机制为:早期绑定(Early Binding,在编译期可知,且运行期保持不变)和晚期绑定(Late Binding,在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
// 说明早期绑定和晚期绑定的例子
class Animal {
public void eat() {
System.out.println("动物进食");
}
}
interface Huntable {
void hunt();
}
class Dog extends Animal implements Huntable {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
@Override
public void hunt() {
System.out.println("捕食耗子,多管闲事");
}
}
class Cat extends Animal implements Huntable {
public Cat() {
super(); // 表现为:早期绑定
}
public Cat(String name) {
this(); // 表现为:早期绑定
}
@Override
public void eat() {
super.eat(); // 表现为:早期绑定
System.out.println("猫吃鱼");
}
@Override
public void hunt() {
System.out.println("捕食耗子,天经地义");
}
}
public class AnimalTest {
public void showAnimal(Animal animal) {
animal.eat(); // 表现为:晚期绑定
}
public void showHunt(Huntable h) {
h.hunt(); // 表现为:晚期绑定
}
}
jvm中的常量池分为三种
1.类文件常量池(Class Constant Pool) 也称静态常量池
2.运行时常量池(Runtime Constant Pool)
3.字符串常量池(String Constant Pool)
1.类文件常量池
我们写的每一个Java类被编译后,就会形成一份class文件(每个class文件都有一个class常量池)。 class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。
这些常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用在程序加载(解析过程)中变为直接引用,或运行时会被转变变为被加载到内存区域的代码的直接引用(即动态链接)。
2. 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。jdk1.8以前存在于永久代,jdk1.8之后存在于元空间。静态常量池中的内容,在类加载后会被存放到方法区的运行时常量池中。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中静态常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中。
3.字符串常量池
字符串常量池存在运行时常量池之中(在JDK7之前存在运行时常量池之中,在JDK7已经将其转移到堆中)。字符串常量池的存在使JVM提高了性能和减少了内存开销。
String类型的**静态变量**会被放到堆的**字符串常量池**中。它的目的就是为了减少相同字符串初始化带来的开销。s2.intern() 返回的是字符串常量池中的引用。
String s1 = "Building";
String s2 = new String("Building");
System.out.println(s1 == s2); // False
System.out.println(s1 == s2.intern()); // True
虚拟机启动过程中,会将各个Class文件中的常量池(存的是字面量和符号引用)载入到运行时常量池中。所以, Class常量池只是一个媒介场所。在JVM真的运行时,需要把常量池中的常量加载到内存中,进入到运行时常量池,由此可知,运行时常量池也是每个类都有一个。字符串常量池可以理解为运行时常量池分出来的部分。加载时,对于class的静态常量池,如果字符串会被装到字符串常量池中。
堆内存溢出:(1)当对象一直创建而不被回收时(2)加载的类越来越多时(3)虚拟机栈的线程越来越多时
栈溢出:方法调用次数过多,一般是递归不当造成
如果我们写一个懒加载,在使用时才初始化,那么我们的内存就会减少很多
public class ConfigManager {
private Map<String, Supplier<Config>> allConfigs = new HashMap<>();
public ConfigManager() {
// 在初始化阶段,只是将配置类的构造函数注册到map中
allConfigs.put("config1", Config1::new);
allConfigs.put("config2", Config2::new);
// ...
allConfigs.put("configN", ConfigN::new);
}
public Config getConfig(String name) {
return allConfigs.get(name).get();
}
}
单例模式
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
静态集合类
(HashMap、Vector 等集合生命周期和应用程序一致),长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收
连接(I/O)未释放
程序中创建或者打开一个流或者是新建一个网络连接的时候,JVM 都会为这些资源类分配内存做缓存,常见的资源类有网络连接,数据库连接以及 IO 流。如果忘记关闭这些资源,会阻塞内存,从而导致 GC 无法进行清理。
变量作用域过大
一个变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为 null,很可能导致内存泄漏的发生。
hash值发生改变
在HashMap和HashSet这种集合中,常常用到equal()和hashCode()来比较对象,如果重写不合理,会认为每次创建的对象都是新的对象,从而导致内存不断的增长.
第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称它为Mark Word,它是个动态的结构,随着对象状态变化。第二部分是类型指针,指向对象的类元数据类型(即对象代表哪个类)。此外,如果对象是一个Java数组,那还应该有一块用于记录数组长度的数据。
Java程序会通过栈上的reference数据(指向对象的引用)来操作堆上的具体对象,对象访问方式也是由虚拟机实现而定的,HotSpot虚拟机主要使用直接指针来进行对象访问,主流的访问方式主要有使用句柄和直接指针两种:
句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
优点:
reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销
优点:
速度更快,它节省了一次指针定位的时间开销
1、编译器通过对象的声明类型和和方法名,在此类型和其超类中寻找权限为public的方法。
2、编译器根据方法调用时传入的参数类型和个数判断调用哪一个1步骤中寻找到的方法
3、若是调用的方法是private、static、final声明的话,那么编译器将会清楚的知道调用哪一个方法,这一种调用方式称之为静态绑定。相反若不是上述声明则编译器根据对象声明的类型,动态的去寻找要调用的方法,这称之为动态绑定。
4、动态绑定时,编译器会根据声明的对象所引用的实际的对象类型,来寻找与声明对象最为合适的方法 比如在对象的多态中,声明为父类的对象,实际引用的是其子类,父类对象调用的方法应为子类中所覆写的方法。
每次调用方法都要进行方法搜索,时间开销相当大。因此,虚拟机预先为每个类创建一个方法表,其中列举了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。
Java中方法调用唯一目的就是确定要调用哪一个方法
非虚方法: 静态方法,私有方法,父类中的方法,被final修饰的方法,实例构造器,非虚方法的特点就是没有重写方法,适合在类加载阶段就进行解析(符号引用->直接引用) 【编译时就能够确定】
其他不是非虚方法的方法就是虚方法
public class Father {
public static void staticMethod(){
System.out.println("father static method");
}
public final void finalMethod(){
System.out.println("father final method");
}
public Father() {
System.out.println("father init method");
}
public void overrideMethod(){
System.out.println("father override method");
}
}
public interface TestInterfaceMethod {
void testInterfaceMethod();
}
public class Son extends Father{
public Son() {
//invokespecial 调用父类init 非虚方法
super();
//invokestatic 调用父类静态方法 非虚方法
staticMethod();
//invokespecial 调用子类私有方法 特殊的非虚方法
privateMethod();
//invokevirtual 调用子类的重写方法 虚方法
overrideMethod();
//invokespecial 调用父类方法 非虚方法
super.overrideMethod();
//invokespecial 调用父类final方法 非虚方法
super.finalMethod();
//invokedynamic 动态生成接口的实现类 动态调用
TestInterfaceMethod test = ()->{
System.out.println("testInterfaceMethod");
};
//invokeinterface 调用接口方法 虚方法
test.testInterfaceMethod();
}
@Override
public void overrideMethod(){
System.out.println("son override method");
}
private void privateMethod(){
System.out.println("son private method");
}
public static void main(String[] args) {
new Son();
}
}
-XX:+UseSerialGC
、-XX:+UseParallelGC
、-XX:+UseConcMarkSweepGC
、-XX:+UseG1GC
或-XX:+UseZGC
来选择特定的垃圾回收器。-Xms
和-Xmx
来设置堆的初始大小和最大大小。-Xmn
来设置新生代的大小。-Xlog:gc*
可以启用详细的GC日志,这对于性能分析和问题诊断非常有用。-XX:SurvivorRatio
、-XX:PermSize
和-XX:MaxPermSize
等。