1.JVM的位置
2.JVM的体系结构
3.类加载器
作用:加载Class文件
①虚拟机自带的加载器
②启动类(根)加载器
③扩展类加载器
④应用程序加载器
基本过程:首先类加载器收到类加载的请求,然后将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器。
检查是否能够加载当前类,能加载则结束该过程,不能就抛出异常,通知子加载器进行加载。
Native
凡是带了native关键字的,说明java的作用范围达不到了,此时会去调用底层C语言的库,会进入本地方法栈,调用本地方法本地接口JNI。
JNI作用:扩展Java的使用,融合C、C++语言为Java所用。
它在内存区域专门开辟了一块标记区域:Native Method Stack,登记native方法,最终执行的时候,加载本地方法库中的方法通过JNI。
If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError
.
OOM排错
GC
- 伊甸园区、新生区、老年区
- 引用计数法
- 复制算法:
4.标记清除算法
先对对象进行标记,然后把没有标记的对象清除。
优点:不需要额外空间去存储算法信息了。
缺点:两次扫描过程,耗时较大。
于是对其进行压缩
(把所有要清除的对象标记了以后,全部排放起来,放到最前面)
1.JVM内存模型是什么?
JVM内存模型是JVM用来管理和组织内存的方式。它定义了不同类型的数据存储在哪里,如何访问这些数据,以及如何管理这些数据的生命周期。可以把JVM的内存模型想象成一栋大楼,不同的房间存放不同类型的东西。
2.JVM内存模型的主要组成部分
JVM内存模型主要分成以下几个区域:
-
方法区(Method Area):
- 这是一个公共的区域,所有线程共享。方法区存储了类的信息、常量、静态变量和编译后的代码等。可以把它想象成一个图书馆,里面有所有书(类和方法)的目录和一些共享的资源。
-
堆(Heap):
- 这是所有线程共享的区域,用来存放所有的对象和数组。可以把堆想象成一个大的仓库,里面存放着你程序运行时创建的所有对象。
-
栈(Stack):
- 这是每个线程私有的区域,用来存放局部变量、方法调用信息(比如方法的参数、返回值等)。可以把栈想象成一个快递员的背包,里面放着他当前任务(方法)的所有必需品。
-
程序计数器(Program Counter Register):
- 这是每个线程私有的一个小区域,记录了当前线程正在执行的字节码的地址。可以把它想象成一个书签,指示了当前线程读到了哪一页。
-
本地方法栈(Native Method Stack):
- 这是用来执行本地方法(Native Methods)的栈,每个线程都有自己的本地方法栈。可以把它想象成一个工具箱,里面有一些特殊工具,用来执行非Java语言编写的方法。
2.JVM内存模型中堆和栈的区别
假设你正在写一个程序,这个程序需要计算一些数学公式并输出结果。以下是这些计算如何在JVM内存模型中处理的:
- 类和方法的信息存储在方法区。
- 当你创建一个新的对象(比如一个计算器对象)时,这个对象会被存储在堆中。
- 当你调用计算方法时,方法的参数和局部变量会被存储在栈中。
- 程序计数器会记录当前正在执行的指令地址。
- 如果你调用了一个需要使用本地方法(比如一个用C语言编写的高效数学计算函数),这个调用会使用本地方法栈。
想象你开了一家餐馆,餐馆里有一个大仓库和每个厨师的工作台。餐馆的运行可以类比一个Java程序的运行,而仓库和工作台则对应于JVM内存模型中的堆和栈。
堆(Heap) - 仓库
- 共享的存储空间:堆就像餐馆的大仓库,里面放着所有的食材和工具。这些物品是餐馆里所有厨师(线程)共享的。
- 存储对象:堆里存储的是所有的食材(对象),比如面粉、鸡蛋、牛奶等。每当需要用到某种食材时,厨师就会从仓库里拿。
- 生命周期长:仓库里的东西只要有需要,就会一直存在。比如,你买了一袋面粉,只要还没用完,它就会一直在仓库里。
栈(Stack) - 厨师的工作台
- 私有的工作空间:栈就像每个厨师的工作台,每个厨师有自己的工作台,其他厨师不能随便动你的东西。
- 存储局部变量和方法调用信息:工作台上放的是厨师当前工作所需的工具和材料,比如正在准备做一盘菜所需要的所有配料和工具(局部变量和方法参数)。
- 生命周期短:当厨师完成一道菜(方法执行完毕),工作台上的东西就会马上被清理掉,为下一道菜准备。比如,当你切完了菜,剩下的菜叶和切菜板会被清理掉。
实际例子
假设你要做一道菜:
-
进入厨房(调用方法):
- 你开始准备做一道菜(方法调用),于是走到你的工作台(栈)。
-
从仓库拿食材(创建对象):
- 你需要一些食材,于是从仓库(堆)里拿出面粉、鸡蛋和牛奶(对象),放到你的工作台(栈)上。
-
做菜过程(方法执行):
- 你在工作台上操作这些食材,比如把面粉倒进碗里、打鸡蛋、搅拌等。这些操作过程中用到的每一个步骤和工具(局部变量)都在你的工作台上。
-
做完一道菜(方法执行完毕):
- 菜做好了,你把菜端出去(方法返回),然后清理工作台,准备做下一道菜。
-
食材的存储:
- 如果你需要再次做这道菜,食材还在仓库(堆)里,你可以随时拿。
关键区别
-
作用范围:
- 堆:共享的,所有的对象都存储在这里,可以被所有线程访问。
- 栈:私有的,每个线程有自己的栈,只能存储当前方法的局部变量和调用信息。
-
生命周期:
- 堆:对象在堆中存活,只要有引用存在,它们就不会被回收。
- 栈:局部变量在方法运行时创建,方法结束时销毁。
-
访问速度:
- 栈:访问速度快,因为它是直接存储在每个线程的内存区域中。
- 堆:访问速度相对较慢,需要通过对象引用来访问。
3.栈里面存的是指针还是对象
在JVM内存模型中,栈里面存储的是指向对象的引用(也可以称作指针),而不是对象本身。对象实际存储在堆中。
通俗易懂的解释
继续用餐馆的例子来解释这个问题:
厨师的工作台(栈)
- 工作台上放的是你当前做菜所需的一些工具和食材的“位置标签”(引用或指针)。
- 比如,你在工作台上有一个小便签,写着“冰箱里的面粉”(指向堆中的对象)。
餐馆的仓库(堆)
- 仓库里存放的是实际的食材和工具。
- 比如,冰箱里面真正存放着面粉(对象)。
举个具体的例子
假设你编写了以下Java代码:
public class Example {
public static void main(String[] args) {
int a = 10;
String str = new String("Hello");
}
}
栈中的内容
-
int a = 10;
- 基本类型变量
a
存储在栈中,直接存储值10
。
- 基本类型变量
-
String str = new String("Hello");
str
是一个引用变量,存储在栈中,但它指向的是堆中实际的String
对象。
堆中的内容
new String("Hello")
- 创建的
String
对象实际存储在堆中,包含了字符串数据 "Hello"。 - 栈中的
str
变量指向这个堆中的String
对象。
- 创建的
更详细的解释
-
基本类型:
- 对于基本类型(如
int
,char
,float
等),变量的值直接存储在栈中。例如,int a = 10;
中,a
存储在栈中,值为10
。
- 对于基本类型(如
-
引用类型:
- 对于引用类型(如对象、数组等),栈中存储的是指向堆中对象的引用(地址)。例如,
String str = new String("Hello");
中,str
变量在栈中,存储的是堆中String
对象的引用,而实际的字符串内容 "Hello" 存储在堆中。
- 对于引用类型(如对象、数组等),栈中存储的是指向堆中对象的引用(地址)。例如,
关键点
- 栈中存储:局部变量、方法调用信息(如方法参数、返回地址等)、基本类型的值、和指向对象的引用。
- 堆中存储:所有的对象和数组。
4.JVM中的堆分成哪些部分?
-
新生代(Young Generation):
- 新生代是用来存储新创建的对象。大多数对象在这里创建和销毁。由于大多数对象的生命周期较短,因此新生代的垃圾回收(GC)会更频繁。
- 新生代又进一步细分为三个区域:
- 伊甸园区(Eden Space):大部分新对象在这里分配。大多数对象会在这里很快被垃圾回收。
- 幸存者区(Survivor Space):又分为两个部分,称为 Survivor 0(S0 或 From) 和 Survivor 1(S1 或 To)。对象在垃圾回收过程中会在这两个区之间移动。
-
老年代(Old Generation 或 Tenured Generation):
- 老年代存储那些生命周期较长的对象。新生代中经历了多次垃圾回收依然存活的对象会被移动到老年代。
- 老年代的垃圾回收频率较低,但一次垃圾回收需要的时间通常较长,因为老年代中的对象数量较多且存活时间较长。
-
永久代(Permanent Generation 或 PermGen,已被元空间 Metaspace 替代):
- 在JDK 1.8之前,永久代用于存储类的元数据(如类信息、方法信息、常量池等)。永久代的大小通常是固定的,不会随程序运行动态增长。
- 从JDK 1.8开始,永久代被移除了,取而代之的是元空间(Metaspace)。元空间不再使用堆内存,而是使用本地内存来存储类的元数据。
5、内存泄漏、内存溢出
内存泄漏 (Memory Leak)
定义:内存泄漏是指程序在运行过程中分配了内存,但由于某种原因没有释放,导致这部分内存无法被使用或回收。内存泄漏会逐渐消耗系统的内存,最终可能导致系统性能下降甚至崩溃。
通俗例子:
想象你在家里有一堆空水瓶,每次你喝完水后都把空瓶子随手丢在地上,而不是扔进垃圾桶。随着时间的推移,地上的空瓶子越来越多,你的活动空间变得越来越小,最终你可能会被瓶子淹没,无法继续正常生活。
在程序中,内存泄漏就像是那些没有被清理的空瓶子。即使这些内存不再被使用,但它们仍然占用着系统资源,最终可能导致系统崩溃。
内存溢出 (OutOfMemory)
定义:内存溢出是指程序在运行过程中需要分配的内存超过了系统所能提供的最大内存量,导致程序无法继续正常运行。
通俗例子:
想象你有一个水池,水池的容量是有限的。如果你不断地往水池里倒水,最终水池会装满,水就会溢出来,导致地板被淹。
在程序中,内存溢出就像是水池的水溢出。当程序需要的内存量超过系统所能提供的最大内存量时,就会发生内存溢出,程序会崩溃并抛出内存溢出错误。
总结
- 内存泄漏:内存被分配但没有被释放,随着时间的推移,未释放的内存越来越多,最终可能耗尽系统资源。
- 类似于:喝完水的瓶子随手扔在地上,最终堆满整个房间。
- 内存溢出:程序需要的内存超过了系统所能提供的最大内存量,导致程序崩溃。
- 类似于:不断往一个有限容量的水池倒水,最终水溢出。
1. 静态属性导致内存泄漏
场景:静态属性持有对象的引用,导致这些对象在程序运行期间一直存在,无法被垃圾回收。
示例:Java 中静态属性导致内存泄漏
import java.util.ArrayList;
import java.util.List;
public class StaticMemoryLeakExample {
private static List<String> list = new ArrayList<>();
public static void addToList(String str) {
list.add(str);
}
public static void main(String[] args) {
while (true) {
addToList("This is a memory leak example");
}
}
}
解释:
- 静态属性
list
持有所有添加的字符串对象的引用。 - 由于
list
是静态的,它的生命周期与程序的生命周期一样长,所以这些字符串对象永远不会被垃圾回收,导致内存泄漏。
2. 未关闭的资源导致内存泄漏
场景:打开的资源(如文件、数据库连接、网络连接等)未被正确关闭,导致系统资源无法释放。
示例:Java 中未关闭的资源导致内存泄漏
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ResourceLeakExample {
public static void main(String[] args) {
while (true) {
try {
BufferedReader reader = new BufferedReader(new FileReader("test.txt"));
// 这里没有关闭 reader,导致资源泄漏
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
解释:
- 每次循环都会创建一个新的
BufferedReader
对象,但没有关闭它。 - 导致文件描述符等资源被耗尽,最终可能导致系统崩溃。
对象的生命周期
对象的生命周期包括创建、使用和销毁三个阶段:
- 创建:对象通过关键字
new
在堆内存中被实例化,构造函数被调用,对象的内存空间被分配。 - 使用:对象被引用并执行相应的操作,可以通过引用访问对象的属性和方法,在程序运行过程中被不断使用。
- 销毁:当对象不再被引用时,通过垃圾回收机制自动回收对象所占用的内存空间。垃圾回收器会在适当的时候检测并回收不再被引用的对象,释放对象占用的内存空间,完成对象的销毁过程。
在Java中,类加载器(Class Loader)是一个负责加载类文件的组件。Java虚拟机(JVM)内置了几个主要的类加载器,每个类加载器都有其特定的作用。以下是一些主要的类加载器及其作用:
1. 启动类加载器(Bootstrap Class Loader)
- 作用:负责加载Java核心类库(如
java.lang.*
、java.util.*
等),这些类库位于JDK的lib
目录下的rt.jar
文件中。 - 特点:
- 它是JVM的一部分,用C/C++编写。
- 不继承自
java.lang.ClassLoader
类。 - 负责加载JDK核心库,无法直接引用。
2. 扩展类加载器(Extension Class Loader)
- 作用:负责加载JRE扩展目录中的类库,通常是
JAVA_HOME/lib/ext
目录下的JAR文件。 - 特点:
- 继承自
java.lang.ClassLoader
。 - 加载路径可以通过系统属性
java.ext.dirs
进行设置。
- 继承自
3. 应用程序类加载器(System/Application Class Loader)
- 作用:负责加载应用程序的类路径(classpath)下的类文件。
- 特点:
- 继承自
java.lang.ClassLoader
。 - 通常是应用程序默认的类加载器。
- 加载用户类路径下的类文件,如
CLASSPATH
环境变量或-cp
、-classpath
命令行参数指定的路径。
- 继承自
4. 用户自定义类加载器(Custom Class Loader)
- 作用:用户可以根据需要实现自己的类加载器,用于加载特殊位置或具有特定加载机制的类。
- 特点:
- 用户可以继承
java.lang.ClassLoader
并重写findClass
方法来实现自定义的类加载逻辑。 - 可以用于实现模块化、热部署等功能。
- 用户可以继承
类加载器的双亲委派模型(Parent Delegation Model)
Java类加载器采用双亲委派模型来加载类。其作用是确保Java核心类库的安全和统一性,避免自定义类覆盖JDK核心类。具体流程如下:
- 委派机制:当一个类加载器收到类加载请求时,它不会直接去加载类,而是先把请求委派给它的父类加载器。
- 父类加载器加载:如果父类加载器能够完成类加载任务,则返回成功。
- 本地加载:如果父类加载器无法完成类加载任务,则由当前类加载器尝试加载。
类加载器之间的关系
- 启动类加载器:没有父类加载器。
- 扩展类加载器:父类加载器是启动类加载器。
- 应用程序类加载器:父类加载器是扩展类加载器。
- 自定义类加载器:通常设定应用程序类加载器为父类加载器,但可以自定义。
示例代码
以下是一个自定义类加载器的简单示例:
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}private byte[] loadClassData(String name) {
// 加载类文件字节码的逻辑
// 例如:从文件系统或网络中读取类文件字节码
return null;
}public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
try {
Class<?> clazz = classLoader.loadClass("com.example.MyClass");
System.out.println("Class loaded: " + clazz.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}