JVM 的内存区域主要有四个区:
JVM 运行时数据区域也叫内存布局,它和 Java 内存模型(Java Memory Model,简称JMM)完全不同,属于完全不同的两个概念。但是不同的厂商,JVM 的具体实现是不一样的。
程序计数器占的区域在内存中是最小的一块。
栈里面放的是 局部变量 和 方法调用信息。
有关栈的内容,和栈帧的内容,在 C语言 里面讲过:传送门:深入了解函数栈帧
每个进程只有一份堆,多个线程共用一个堆,new 出来的对象,就在堆上。
方法区里面放的是 “类对象” ,Java->class(二进制字节码)
类加载主要就是把 .class 文件加载到内存中,构建成类对象。主要有三个部分:loading、linking、initializing。从 Java SE 官方文档可以看到
找到对应的 JDK:
点击下面的 HTML,进入:
然后向下方就能找到讲解类加载的部分:
这里的 Loading 就是讲解 类加载的 :
真正对类对象进行初始化,尤其是针对静态成员。
先看代码:
class A {
public A() {
System.out.println("A 的构造方法");
}
{
System.out.println("A 的构造代码块");
}
static {
System.out.println("A 的静态代码块");
}
}
class B extends A{
public B() {
System.out.println("B 的构造方法");
}
{
System.out.println("B 的构造代码块");
}
static {
System.out.println("B 的静态代码块");
}
}
public class Test extends B{
public static void main(String[] args) {
new Test();
new Test();
}
}
这些执行的大的原则是:
双亲委派模型是 类加载 中的一个环节,描述的是 JVM 中的 类加载器,如何根据类的全限定名(java.lang.String)找到 .class 文件的过程。默认的类加载器如下:
程序员也可以自定义类加载器,来加载其他目录中的类,像 Tomcat 就自定义了类加载器,加载 webapps。
双亲委派模型,就描述了找目录过程中,也就是上面那三个类加载器是如何配合的:
加载标准库,假设是加载 java.lang.String :
a)程序启动,先进入 ApplicationClassLoader 类加载器。
b)ApplicationClassLoader 会检查它的父加载器是否已经加载过了,如果没有,就调用父加载器 ExtensionClassLoader。
c)ExtensionClassLoader 也会检查下,他的父加载器是否加载过了,如果没有,就调用父加载器 BootStrapClassLoader。
d)BootStrapClassLoader 也会检查它的父加载器是否加载,自己没有父亲,于是自己扫描自己负责的目录。
e)java.lang.String 这个类在标准库中能找到,直接由 BootStrapClassLoader 负责后续的加载过程,查找环节就结束了。
f)大致流程如下:
加载自己的类:
a)程序启动,先进入 ApplicationClassLoader 类加载器。
b)ApplicationClassLoader 会检查它的父加载器是否已经加载过了,如果没有,就调用父加载器 ExtensionClassLoader。
c)ExtensionClassLoader 也会检查下,他的父加载器是否加载过了,如果没有,就调用父加载器 BootStrapClassLoader。
d)BootStrapClassLoader 也会检查它的父加载器是否加载,自己没有父亲,于是自己扫描自己负责的目录,没扫描到,于是回到子加载器继续扫描。
e)ExtensionClassLoader 也扫描自己负责的目录,也没扫描到,回到子加载器继续扫描。
f)ApplicationClassLoader 也扫描自己负责的目录,能找到 Test 类,于是进行后续加载,查找目录的环节结束。
g)最终 ApplicationClassLoader 也找不到,就会抛出 ClassNotFoundException 异常。
f)流程如下:
上面这一套的查找规则,就称为 “双亲委派模型” 。更直观的就是我们有问题问班长,班长再问辅导员,然后辅导员告诉班长,班长再告诉我们。
JVM 这样设计的原因是:一旦程序员自己写的类,和标准库中的类,全限定类目重复了,也能够顺利加载到标准库当中的类。如果是自定义类加载器,不一定遵守双亲委派模型。
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("和限定类重复");
}
}
在写代码的时候,经常回申请内存,当不需要的时候,就需要回收内存。于是就有了内存的释放时机,早了不行,迟了也不行。
垃圾回收,本质上事考运行时环境,额外做了很多的工作,来自动释放内存。
垃圾回收的缺点:
内存是由很多种的:
堆中的内存布局是这样的:
像上面这种,一部分在使用,一部分不再使用,是不释放的。等对象完全不使用的时候,才真正释放。GC 会提高软件的开发效率。
垃圾回收的两个大阶段:
就是针对每个对象,都会额外引入一小块内存,保存这个对象有多少个引用指向它。假如说,这样的代码 Test t = new Test();
,他的引用计数就是这样的:
如果再加一个 Test t2 = t;
就是 t 和 t2 都是指向这个对象的引用,那么结果如下:
内存不在使用的时候,就该释放了。也就是引用计数为 0 的时候,就不再使用了。
基于引用计数的致命缺陷:
空间利用率低,每个 new 的对象都需要计数器,如果对象本身很小(比如说只有四个字节),那么多出的计数器,相当于空间浪费了一倍。
会有循环引用的问题。如下面的代码
class Test {
Test t = null;
}
Test t1 = new Test();
Test t2 = new Test();
t1.t = t2;
t2.t = t1;
意思就是 t2 赋给了 t1 里面的 t 属性。t1 赋给了 t2 里面的 t 对象。内存模型如下:
然后接下来,把指向置为 null :
t1 = null;
t2 = null;
然后两个对象的引用技术,不为 0,所以无法释放,但是由于引用长在了彼此的身上,外界的代码又无法访问到这个两个对象。初始窗口,这俩对象就被孤立了,既不能使用,又不能释放,就出现了 “内存泄漏” ,如下图:
就是通过额外的线程,定期的针对整个内存空间的对象进行扫描。有一些起始位置(GCRoots),会类似于 深度优先遍历一样,把可以访问到的对象进行标记,能标记的就是可达对象,如果没标记就是不可达,就是垃圾。
GCRoots:
a)栈上的局部变量
b)常量池中的引用指向的对象
c)方法区中的家庭成员指向的对象
可达性分析的优点,克服了 引用计数的两个缺点,自身的缺点就是系统开销大,遍历一次可能比较慢。
找垃圾的核心就是:没有引用指向,就不使用了,就是垃圾。
主要有三种策略:
标记就是可达性标记,清除就是直接释放内存,释放之后的内存可能是不连续的,就是内存碎片:
为了解决内存碎片,引入的复制算法。就是把申请的内存一分为二,然后不是垃圾的,拷贝到内存的另一边,然后把原来的一半内存空间整体都释放。
复制算法的问题:
a)内存空间利用率低。
b)如果保留的对象多,要释放的对象少,此时复制开销就很大。
就是针对复制算法,再做出改进。类似于顺序表删除中间元素,有一个搬运操作。
实际在使用的时候,是多种方法相结合起来的,也就是 “分代回收”
就是针对进行分类(根据对象的 “年龄” 分类),一个对象熬过一轮 GC 的扫描,就 “长了一岁”。针对不同年龄的对象,采取不同的方案。 一块内存,分为两半,一半放 “新生代” ,一半放 “老年代” ,然后新生代里面又有伊甸区和两个 “幸存区” 。幸存区大小一样:
分代回收中,还有一个特殊情况: 有一类对象可以直接进入老年代(大对象,占有内存多的对象),大对象拷贝开销比较大,不适合使用复制算法,所以直接进入老年代。
常用的垃圾回收器如下:
下面是新的垃圾回收器,核心思想就是:化整为零,: