JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。java
做用 :首先经过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操做系统去执行,所以须要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程当中须要调用其余语言的本地库接口(Native Interface)来实现整个程序的功能。程序员
下面是Java程序运行机制详细说明算法
Java程序运行机制步骤编程
从上图能够看,java文件经过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。
其实能够一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,而后在堆区建立一个 java.lang.Class对象,用来封装类在方法区内的数据结构。数组
Java 虚拟机在执行 Java 程序的过程当中会把它所管理的内存区域划分为若干个不一样的数据区域。这些区域都有各自的用途,以及建立和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而创建和销毁。Java 虚拟机所管理的内存被划分为以下几个区域:安全
不一样虚拟机的运行时数据区可能略微有所不一样,但都会听从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为如下 5 个部分:服务器
浅拷贝(shallowCopy)只是增长了一个指针指向已存在的内存地址,数据结构
深拷贝(deepCopy)是增长了一个指针而且申请了一个新的内存,使这个增长的指针指向这个新的内存,多线程
使用深拷贝的状况下,释放内存的时候不会由于出现浅拷贝时释放同一个内存的错误。并发
浅复制:仅仅是指向被复制的内存地址,若是原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。
物理地址
堆的物理地址分配对对象是不连续的。所以性能慢些。在GC的时候也要考虑到不连续的分配,因此有各类算法。好比,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。因此性能快。
内存分别
堆由于是不连续的,因此分配的内存是在运行期
确认的,所以大小不固定。通常堆大小远远大于栈。
栈是连续的,因此分配的内存大小要在编译期
就确认,大小是固定的。
存放的内容
堆存放的是对象的实例和数组。所以该区更关注的是数据的存储
栈存放:局部变量,操做数栈,返回结果。该区更关注的是程序方法的执行。
PS:
程序的可见度
堆对于整个应用程序都是共享、可见的。
栈只对于线程是可见的。因此也是线程私有。他的生命周期和线程相同。
队列和栈都是被用来预存储数据的。
说到对象的建立,首先让咱们看看 Java
中提供的几种对象建立方式:
Header | 解释 |
---|---|
使用new关键字 | 调用了构造函数 |
使用Class的newInstance方法 | 调用了构造函数 |
使用Constructor类的newInstance方法 | 调用了构造函数 |
使用clone方法 | 没有调用构造函数 |
使用反序列化 | 没有调用构造函数 |
下面是对象建立的主要流程:
虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,若是没有,必须先执行相应的类加载。类加载经过后,接下来分配内存。若Java堆中内存是绝对规整的,使用“指针碰撞“方式分配内存;若是不是规整的,就从空闲列表中分配,叫作”空闲列表“方式。划份内存时还须要考虑一个问题-并发,也有两种方式: CAS同步处理,或者本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。而后内存空间初始化操做,接着是作一些必要的对象设置(元信息、哈希码…),最后执行<init>
方法。
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:
选择哪一种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
对象的建立在虚拟机中是一个很是频繁的行为,哪怕只是修改一个指针所指向的位置,在并发状况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的状况。解决这个问题有两种方案:
Java
程序须要经过 JVM
栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM
虚拟机的实现。目前主流的访问方式有 句柄 和 直接指针 两种方式。
指针: 指向对象,表明一个对象在内存中的起始地址。
句柄: 能够理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。
Java
堆中划分出一块内存来做为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造以下图所示:
优点:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是很是广泛的行为)时只会改变句柄中的实例数据指针,而引用自己不须要修改。
若是使用直接指针访问,引用 中存储的直接就是对象地址,那么Java
堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。
优点:速度更快,节省了一次指针定位的时间开销。因为对象的访问在Java
中很是频繁,所以这类开销聚沙成塔后也是很是可观的执行成本。HotSpot 中采用的就是这种方式。
内存泄漏是指再也不被使用的对象或者变量一直被占据在内存中。理论上来讲,Java是有GC垃圾回收机制的,也就是说,再也不被使用的对象,会被GC自动回收掉,自动从内存中清除。
可是,即便这样,Java也仍是存在着内存泄漏的状况,java致使内存泄露的缘由很明确:长生命周期的对象持有短生命周期对象的引用就极可能发生内存泄露,尽管短生命周期对象已经再也不须要,可是由于长生命周期对象持有它的引用而致使不能被回收,这就是java中内存泄露的发生场景。
在java中,程序员是不须要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常状况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存
回收会致使程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能能够自动监测对象是否超过做用域从而达到自动
回收内存的目的,Java 语言没有提供释放已分配内存的显示操做方法。
java语言最显著的特色就是引入了垃圾回收机制,它使java程序员在编写程序时再也不考虑内存管理的问题。
因为有这个垃圾回收机制,java中的对象再也不有“做用域”的概念,只有引用的对象才有“做用域”。
垃圾回收机制有效的防止了内存泄露,能够有效的使用可以使用的内存。
垃圾回收器一般做为一个单独的低级别的线程运行,在不可预知的状况下对内存堆中已经死亡的或很长时间没有用过的对象进行清除和回收。
程序员不能实时的对某个对象或全部对象调用垃圾回收器进行垃圾回收。
垃圾回收有分代复制垃圾回收、标记垃圾回收、增量垃圾回收。
对于GC来讲,当程序员建立对象时,GC就开始监控这个对象的地址、大小以及使用状况。
一般,GC采用有向图的方式记录和管理堆(heap)中的全部对象。经过这种方式肯定哪些对象是"可达的",哪些对象是"不可达的"。当GC肯定一些对象为"不可达"时,GC就有责任回收这些内存空间。
能够。程序员能够手动执行System.gc(),通知GC运行,可是Java语言规范并不保证GC必定会执行。
垃圾收集器在作垃圾回收的时候,首先须要断定的就是哪些内存是须要被回收的,哪些对象是「存活」的,是不能够被回收的;哪些对象已经「死掉」了,须要被回收。
通常有两种方法来判断:
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就能够被回收了。
垃圾回收不会发生在永久代,若是永久代满了或者是超过了临界值,会触发彻底垃圾回收(Full GC)。若是你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为何正确的永久代大小对避免Full GC是很是重要的缘由。
垃圾回收不会发生在永久代,若是永久代满了或者是超过了临界值,会触发彻底垃圾回收(Full GC)。若是你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为何正确的永久代大小对避免Full GC是很是重要的缘由。请参考下Java8:从永久代到元数据区
(译者注:Java8中已经移除了永久代,新加了一个叫作元数据区的native内存区)
标记无用对象,而后进行清除回收。
标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段:
标记-清除算法之因此是基础的,是由于后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。
优势:实现简单,不须要对象进行移动。
缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提升了垃圾回收的频率。
标记-清除算法的执行的过程以下图所示
为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另一个区域中,最后将当前使用的区域的可回收的对象进行回收。
优势:按顺序分配内存便可,实现简单、运行高效,不用考虑内存碎片。
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
复制算法的执行过程以下图所示
在新生代中可使用复制算法,可是在老年代就不能选择复制算法了,由于老年代的对象存活率会较高,这样会有较多的复制操做,致使效率变低。标记-清除算法能够应用在老年代中,可是它效率不高,在内存回收后容易产生大量内存碎片。所以就出现了一种标记-整理算法(Mark-Compact)算法,与标记-整理算法不一样的是,在标记可回收的对象后将全部存活的对象压缩到内存的一端,使他们紧凑的排列在一块儿,而后对端边界之外的内存进行回收。回收后,已用和未用的内存都各自一边。
优势:解决了标记-清理算法存在的内存碎片问题。
缺点:仍须要进行局部对象移动,必定程度上下降了效率。
标记-整理算法的执行过程以下图所示
当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。通常包括年轻代、老年代 和 永久代,如图所示:
若是说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展现了7种做用于不一样分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不一样收集器之间的连线表示它们能够搭配使用。
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来得到最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器很是适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
CMS 使用的是标记-清除的算法实现的,因此在 gc 的时候回产生大量的内存碎片,当剩余内存不能知足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被下降。
新生代垃圾回收器通常采用的是复制算法,复制算法的优势是效率高,缺点是内存利用率低;老年代回收器通常采用的是标记-整理的算法进行垃圾回收。
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程以下:
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值以后就会触发全局垃圾收回,通常使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的总体执行流程。
所谓自动内存管理,最终要解决的也就是内存分配和内存回收两个问题。前面咱们介绍了内存回收,这里咱们再来聊聊内存分配。
对象的内存分配一般是在 Java 堆上分配(随着虚拟机优化技术的诞生,某些场景下也会在栈上分配,后面会详细介绍),对象主要分配在新生代的 Eden 区,若是启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数状况下也会直接在老年代上分配。总的来讲分配规则不是百分百固定的,其细节取决于哪种垃圾收集器组合以及虚拟机相关参数有关,可是虚拟机对于内存的分配仍是会遵循如下几种「普世」规则:
多数状况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。若是本次 GC 后仍是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
这里咱们提到 Minor GC,若是你仔细观察过 GC 平常,一般咱们还能从日志中发现 Major GC/Full GC。
所谓大对象是指须要大量连续内存空间的对象,频繁出现大对象是致命的,会致使在内存还有很多空间的状况下提早触发 GC 以获取足够的连续空间来安置新对象。
前面咱们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,若是大对象直接在新生代分配就会致使 Eden 区和两个 Survivor 区之间发生大量的内存复制。所以对于大对象都会直接在老年代进行分配。
虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。所以虚拟机给每一个对象定义了一个对象年龄的计数器,若是对象在 Eden 区出生,而且可以被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到必定程度(默认 15) 就会被晋升到老年代。
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终造成能够被虚拟机直接使用的java类型。
Java中的全部类,都须要由类加载器装载到JVM中才能运行。类加载器自己也是一个类,而它的工做就是把class文件从硬盘读取到内存中。在写程序的时候,咱们几乎不须要关心类的加载,由于这些都是隐式装载的,除非咱们有特殊的用法,像是反射,就须要显式的加载所须要的类。
类装载方式,有两种 :
1.隐式装载, 程序在运行过程当中当碰到经过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
2.显式装载, 经过class.forname()等方法,显式加载须要的类
Java类的加载是动态的,它并不会一次性将全部类所有加载后再运行,而是保证程序运行的基础类(像是基类)彻底加载到jvm中,至于其余类,则在须要的时候才加载。这固然就是为了节省内存开销。
实现经过类的权限定名获取该类的二进制字节流的代码块叫作类加载器。
主要有一下四种类加载器:
类装载分为如下 5 个步骤:
在介绍双亲委派模型以前先说下类加载器。对于任意一个类,都须要由加载它的类加载器和这个类自己一同确立在 JVM 中的惟一性,每个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,而后再转化为 class 对象。
类加载器分类:
双亲委派模型:若是一个类加载器收到了类加载的请求,它首先不会本身去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样全部的加载请求都会被传送到顶层的启动类加载器中,只有当父加载没法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
当一个类收到了类加载请求时,不会本身先去加载这个类,而是将其委派给父类,由父类去加载,若是此时父类不能加载,反馈给子类,由子类去完成类的加载。
JDK 自带了不少监控工具,都位于 JDK 的 bin 目录下,其中最经常使用的是 jconsole 和 jvisualvm 这两款视图监控工具。
做者:ThinkWon
来源: https://thinkwon.blog.csdn.ne...