赞
踩
JVM 其实本质上就是一个 Java 进程,JVM 启动之后就会从操作系统申请到一大块内存,在程序启动时,JVM 这个 java 进程就会对它申请到的这块内存空间划分多个区域,每个区域都有自己的功能。
堆中存放的时程序
new
出来的对象
方法区中存放的是
类对象
一个 .java
程序启动时,就会生成一个 .class
文件,JVM 会将这个 .class 进行加载,加载到内存中 → 就变成了类对象
static
成员程序计数器是内存中最小的一个部分,它里面存放的只是一个
内存地址
,就是程序接下来要执行的指令地址。
栈中存放程序中的
局部变量
。
本地方法栈: 指的是给 JVM 内部的方法(使用 C++ 实现的方法)去使用的
虚拟机栈:给上层的 Java 代码来使用的。
在 JDK1.8 之后这两块区域已经没有了本质区分
类加载其实是 JVM 中的一个非常核心的流程,他所做的事情就是把
.class
文件,转换成 JVM 中的类对象。
当源代码编译成可执行程序后再加载成类对象之后,才能开始执行。
进行类加载,其中一个非常重要的环节,就是根据这个类的名字:例如“java.lang.String”,找到对应的 .class 文件,在 JVM 中,有三个类加载器(三个特殊的对象)来负责进行这里的找文件操作。
背景:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查请求类是否被加载过了 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果父类加载器抛出 ClassNotFoundException,说明父类加载器无法完成加载请求 // from the non-null parent class loader } if (c == null) { // 在父类加载器无法加载时,再调用本身的 findClass 方法进行类加载 long t1 = System.nanoTime(); c = findClass(name); // 记录统级信息 ... } } if (resolve) { resolveClass(c); } return c; } }
当一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,
而是把这个请求委派给父类加载器去完成
,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载
。
我们首先要知道,垃圾回收,回收的是 “内存”, 前面讲到说 JVM 其实就是一个 Java 进程,一个进程是会持有很多的硬件资源的(CPU,内存,硬盘,带宽资源…),但是系统的内存总量是一定的,程序在使用内存的时候,必须先申请才能使用,内存要同时给很多个进程来使用,所以当前进程在使用完之后,就需要进行释放。
对于 Java 来说,代码中的任何地方都可以申请内存,然后由 JVM 统一进行释放,具体来说,就是在 JVM 内部,专门持有一组负责垃圾回收的线程来进行这样的工作。
- 日常讨论的 “垃圾回收” 主要就是指 “堆上内存的回收”;因为在 Java堆中存放着几乎所有的对象实例,
程序员从代码编写的角度来看,内存的申请时机,是非常明确的。但是内存的释放时机,很多时候不太明确
。- 而 栈上的内存是和“具体的线程” 绑定在一起的,
会随着线程启动创建,线程代码块执行结束,内存就自动释放了
。
在堆上,主要的内存使用就是 new 出了很多对象。
此时针对堆上的对象,也分为三种:
- 完全要使用
- 完全不适用
- 一半要使用,一半不使用
GC 中回收内存,其实就是回收这些
完全不使用
的对象。
- 先找出要回收的垃圾对象
- 再去回收垃圾对象的内存空间
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就 +1;当引用失效时,计数器就 -1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。
引用计数的优点:
规则简单,实现方便,比较高效(程序运行效率高)
引用计数的缺点:
空间利用率比较低
存在循环引用的问题(致命伤)
所以我们在 Java 中并没有使用引用计数这种方式来判定对象是否“死亡”,而是用了以下的方式:
此算法的核心思想为 : 从一组初始的位置(GC Roots)出发,向下进行深度遍历,把所有能够访问到的对象都标记成“可达”(可以被访问到),对应的,不可达对象(没有标记的对象),就是“死亡”对象。
在Java语言中,可作为GC Roots的对象包含下面几种:
不管是引用计数,还是可达性分析,判定原则其实都是看当前这个对象是否有引用来指向,是通过引用来决定 “对象的生死”。
当堆中的有效内存空间被耗尽的时候,会停止整个程序(也称stop the world),然后进行两项工作, 第⼀项 是标记,第⼆项则是清除。
被标记的空间并不是一整块连续的空间,而是和存活对象穿插着的一小块一小块的碎片空间。如果我们将标记的空间进行删除,再次使用申请空间时,很多时候我们是需要申请一块连续存储的内存空间的,虽然内存空间足够,但是由于内存碎片,空间不连续,也会导致内存分配失败。
"复制"算法是为了解决"标记-清理"的效率问题。 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。
这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为"标记-整理算法"。
标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存
。
这样的操作,就可以有效避免内存碎片,同时也能提高空间利用率。
在这个搬运的过程中,也会是一个很大的开销,这个开销要比复制算法里面的复制对象的开销甚至更大。
分代算法和上面讲的 3 种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收。
当前 JVM 垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法
。
在 JVM 中,进行垃圾回收扫描(可达性分析)也是周期性的。 这个对象每次经历了一个扫描周期,就认为 “长了一岁”。
就根据这个对象的年龄,来对整个内存进行了分类,把年龄短的对象放一起,年龄长的放一起。
不同年龄的对象,就可以采取不同的垃圾回收算法来进行处理了。
因为:如果这个大对象直接放在新生代,来回拷贝的开销会非常大,就会直接将这样的大对象放在老年代。
一个对象的一生:我是一个普通的 Java 对象,我出生在 Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden
区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了 Survivor 区的 “From” 区(S0 区),自从去了
Survivor 区,我就开始漂了,有时候在 Survivor 的 “From” 区,有时候在 Survivor 的 “To” 区(S1
区),居无定所。直到我 18
岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在老年代里,我生活了很多年(每次GC加一岁)然后被回收了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。