JVM探究

JVM内存结构

JDK1.8为何用元空间取代永久代?

类及方法的信息等比较难确定其大小,因此对于永久代大小指定比较困难,太小容易出现永久代溢出,太大容易出现老年代溢出。

而且永久代为GC带来不必要的复杂度,且回收效率低。

字符串常量、类文件常量池所在区域

字符串常量在堆中分配,类文件常量池位于方法区,即元空间。

类加载过程

  • 加载:堆中生成class字节码对象
  • 验证:校验字节码文件正确性,确保class文件的字节流信息符合JVM要求
  • 准备:为类的静态变量分配内存并将其初始化为默认值
  • 解析:JVM将常量池内的符号引用替换成直接引用
  • 初始化:执行类构造器初始化的过程

什么时候发生类的初始化

  • 初始化main方法时
  • new一个类的对象
  • 调用类的静态成员和方法时
  • 使用java.lang.reflect包的方法对类进行反射调用

对象的创建过程

常见类加载器

-   Bootstrap ClassLoader(启动类加载器,c语言写的,负责加载jre/bin下的核心类库,如rt.jar)
    |
-   Extension ClassLoader(扩展类加载器)
    |
-   Application ClassLoader(应用程序类加载器,加载自己写的class)

什么是双亲委派机制

当一个类收到类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果父类不能加载,这时会反馈给子类,再由子类去完成。

也就是说如果一个类可以被委派最基础的ClassLoader加载,就不能让高层的ClassLoader加载。

这样可以保证我们不能修改java.已有的类

破坏双亲委派机制的实例

打破双亲委派机制典型的两个方法:

  • 自定义类加载器,重写loadClass方法
  • 使用线程上下文类加载器

首先看一下JDBC连接数据库加载驱动的代码

再看看DriverManager 的初始化方法loadInitialDrivers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private static void loadInitialDrivers() {
String drivers;
try {
//读取系统属性
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}


//通过SPI加载驱动类
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});

println("DriverManager.initialize: jdbc.drivers = " + drivers);

if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
//通过类加载器加载
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}

可以看到先是获取jdbc.drivers属性,得到了的路径,然后通过应用程序类加载器加载

堆和栈的区别

存放内容不同:

堆存放对象的实例和数组,因此更关心数据存放

栈存放局部变量、操作数栈和返回结果,所以更关心程序的执行

内存区别:

堆不连续,所以分配内存在 运行期 确认,大小不固定

栈是连续的,分配内存在 编译器 确认,大小固定

什么是OOM

(Out Of Memory),指内存不足,内存溢出了

当JVM没有足够内存来为对象分配并且垃圾回收器也没有空间回收时会出现该情况。

常见的OOM有:

  • 堆内存溢出(最常见):一般由于内存泄漏申请使用完内存没有释放导致虚拟机不能再次使用该内存)*或堆大小设置不当引起.*(解决的话用JVM配置参数-Xmx设置堆大小)
  • 栈内存溢出一般由死循环或深度递归造成

四大垃圾回收算法

标记清除算法(Mark-Sweep)

(标记无用对象,然后进行清除回收),它将垃圾收集分为两个阶段:标记阶段、清除阶段。其优点是实现简单,缺点是效率低,且产生大量不连续的内存碎片

复制算法(Copy)

(把内存空间分为两个相等的区域,每次只使用一个,垃圾回收时,遍历当前区域,把存活的对象复制到另一个区域,最后将可回收对象进行回收)

  • 优点是运行高效,不考虑内存碎片。
  • 缺点是可用内存大小为原来的一半,对象存活率高时要频繁复制操作。

扩充:Java堆中被分为了新生代和老年代,这样的划分是方便GC。Java堆中的新生代就使用了GC复制算法。在新生代中又分为了三个区域:Eden、To Survivor、 From Survivor,默认比例是8:1:1

标记整理算法(Mark-Compact)

(标记可回收对象,再将存活的对象移动到内存另一端,清空占用内存边界以外的内存)

  • 优点:不考虑内存碎片
  • 缺点:对象移动的代价比较大,会降低效率。

分代收集算法

分为新生代和老年代,新生代默认占总空间的1/3,老年代默认占2/3.

新生代使用的复制算法,有三个分区:Eden、To Survivor、 From Survivor,默认比例是8:1:1。它的执行流程是:

  1. 把Eden + From Survivor存活的对象放入To Survivor区;
  2. 清空Eden和From Survivor分区;
  3. From Survivor和To Survivor分区交换,From Survivor变成To Survivor,反之

每移动一次,对象年龄加1,当年龄大于阈值15时,会移动到老年代。

老年代当空间占用达到某个值之后就会触发全局垃圾回收,一般用标记整理算法。

JVM如何判断哪些对象需要删除?

引用计数器法:通过一个对象的引用计数器的值来判断该对象是否被引用了。但是存在循环引用的情况,两个对象之间彼此引用,但没有被其他对象引用,缺不能回收,所以一般不用。

可达性分析算法:根据GC Roots进行查找标记,依次向下搜索,搜素走过的路径称为引用链(Reference Chain),当一个对象不能和任何一个GC Root产生关系时,就判定为垃圾。

可作为GC Roots的对象

  • 虚拟机栈中引用的对象
  • 方法区中常量引用的对象
  • 方法区中类静态属性引用的对象
  • Native方法引用的对象

强引用、软引用、弱引用、虚引用

(被引用的对象一定能存活吗?)

  • 强引用:普通的对象引用关系,如String s=new String(“jin”);只有设置obj==null,JVM才会回收
  • 软引用:一般用于维护可有可无的对象,只有在内存不足时才会被回收。
  • 弱引用:在JVM进行回收时,无论内存是否充足,都会被回收
  • 虚引用:用的不多,主要是用来跟踪对象被GC的活动。

CMS垃圾回收器

CMS(Concurrent Mark-Sweep ),Concurrent是指多线程,Mark-Sweep说明它是使用标记清除算法实现,所以在gc的时候会产生大量的内存碎片。

收集过程:

  • 初始标记:只是标记GC Roots能直接关联的对象,速度很快,会暂停工作线程。
  • 并发标记:标记GC Roots间接相关对象,和用户线程一起执行,不需要暂停工作线程
  • 重新标记:重新标记新产生的垃圾,因用户线程运行时会导致某些标记产生变动。
  • 并发清除:并发清除垃圾。

G1垃圾收集器

它是别称为垃圾回收器里最前沿的成果,它最突出的改进是:

  1. 基于标记-整理算法,不产生内存碎片
  2. 在不牺牲吞吐量前提下,实现低停顿垃圾回收

是把堆内存分为大小固定的独立区域,并跟踪这些区域的垃圾收集进度,优先回收垃圾最多的区域。

JVM调优中查看JVM参数默认值

jsp -v 查看jvm进程显示指定的参数

jinfo 可实时查看调整虚拟机各项参数

常见的JVM配置参数有哪些?

-Xmx:设置堆的最大内存,默认为物理内存的1/4

-Xms:设置堆初始化内存大小,默认为物理内存的1/64

-Xmn:设置新生代内存大小

-Xss:设置线程栈大小

请我喝杯咖啡吧~

支付宝
微信