计算机是二进制的系统,他只认识 01010101,但像我们编写的HelloWord.java,计算机是不认识的,因此就需要编译,由javac编译成字节码文件.class,因为JVM只认识.class文件,再由JVM编译成计算机认识的文件,对于电脑系统来说,文件代表一切,这也是说Java是跨平台语言的原因。
再看看JDK、JRE、JVM的关系,JDK中包括了JRE,JRE中包括了JVM。以下是Java官方图片。
下面是Java程序运行机制详细说明
Java程序运行机制步骤
一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
JVM包含两个子系统和两个组件,
两个子系统:Class loader(类加载器)、Execution engine(执行引擎);
两个组件:Runtime data area(运行时数据区)、Native Interface(本地接口)。
作用 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
Java虚拟机主要分为以下五个区:
一、方法区(METHOD AREA)
二、Java堆 (HEAP)
三、虚拟机栈(JAVA STACK)
解析栈帧:
四、本地方法栈 (NATIVE METHOD STACK)
五、程序计数器 (PEOGRAM COUNTER REGISTER)
编译期
就确认,大小是固定的。堆:分配的内存是在运行期
确认的,大小不固定,但远大于栈。HotSpot是较新的Java虚拟机,用来代替JIT(Just in Time),Java原先是把源代码编译为字节码在虚拟机执行,但这样执行速度较慢,而HotSpot将常用的部分代码编译为本地(原生,native)代码,提高执行性能。
HotSpot包括一个解释器和两个编译器(client 和 server,二选一),解释与编译混合执行模式,默认启动解释执行。
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种:
总结:
Java
中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。内存泄漏:指无用的对象或者变量一直被占据在内存中。
理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。但是即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
内存溢出:指程序申请内存时,内存不够用,比如给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,此时就会报错OOM,即所谓的内存溢出。
通俗的说,就是停车场(Java堆)保安(GC)让很久不用的废弃车子(无用的对象)从车位上挪走,但是这个车子又没办法挪走。这就是内存泄漏。停车场所有的车位都有车子占用了,再来车子没地了,或者说给你一个小汽车的停车位(int),你非要停一辆高铁(Long),这就是内存溢出。
内存泄露量大到一定程度会导致内存溢出。但是内存溢出不一定是内存泄露引起的。
JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
垃圾回收机制简称GC(Gabage Collection)。GC主要用于Java堆的管理。Java 堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
因为程序在运行过程中,会产生大量的内存垃圾,GC是不定时自动到堆内存中清理不可达对象。程序员唯一能做的就是通过调用System.gc 方法来"建议"执行垃圾收集器。
优点:
原理:
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
可以。
程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
强引用:发生 gc 的时候不会被回收。
软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
弱引用:有用但不是必须的对象,在下一次GC时会被回收。
虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。
垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。
一般有两种方法来判断:
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区
(注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)
1).Mark-Sweep(标记-清除)算法
这是最基础的算法,该算法就是标记出需要被回收的对象,等到需要执行GC操作时将标记的对象一并清除,实现垃圾回收。改方法简单,效率高,但是有个缺点就是会导致内存碎片。
2).Copying(复制)算法
Copying算法是将内存区域划分为两块相同大小的子区域,并且在其中的一块中执行对象分配,等到这一块的内存用完了,就将该区域还存活着的对象复制到另外一块内存区域上面,然后再把已使用的内存空间一次清理掉,这样就不会导致内存碎片的问题,但是有个明显的缺点,每次只能使用到一半的内存,对内存压力大。
3).Mark-Compact(标记-整理)算法
Mark-Compact算法是在Mark-Sweep算法的基础上进行了改进,算法标记跟Mark-Sweep一样,只是在标记完之后,将标记的对象向一端移动,然后清理掉边界以外的内存区域,这样就解决了内存碎片化的问题
4).Generational Collection(分代收集)算法
这是目前Jvm使用的垃圾回收算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。分为老年代(Tenured Generation)和新生代(Young Generation)。老年代的内存区域的对象一般回收频率比较低,采用了Mark-Compact算法,而新生代的内存区域由于每次需要回收大量对象,回收频率较高,所以将该区域又划分成了一个较大的Eden空间和两个较小的Suivivor空间,每次使用Eden空间和一个Survivor空间的,当需要回收垃圾时,将Eden空间和该Survivor空间的存活对象复制到另外一块Survivor空间上,然后清理到Eden和刚才使用的Survivor空间,实现垃圾回收机制。
新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。
新生代收集器:
老年代收集器:
G1(Garbage First)收集器 (标记-整理算法):
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候会产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记整理的算法进行垃圾回收。
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
Java 自动内存管理最核心的功能是堆内存中对象的分配与回收。Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点新生代分为:Eden 空间、From Survivor、To Survivor 空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
JVM堆内存图
设置Survivor区的意义:减少进入老年代的对象。如果不设置幸存者区,那么每次MinorGC存活的对象都会进入老年代,当老年代内存满了之后会进行MajorGC,非常影响程序的性能。
设置两个Survivor区的目的:防止内存碎片化。因为MinorGC将Eden和SurvivorFrom中存活下来的对象复制到SurvivorTo区,From区和To区交换,所以每次存活的对象都会进入一个空的To区,然后依次分配,保证了分配的内存是连续的,也就不会出现内存碎片化。并且SurvivorFrom和SurvivorTo区的内存大小必须严格相等
MinorGC是指发生在新生代的GC,它决定了新生代对象的生命周期,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;MinorGC 的原理就是将Eden和SurvivorFrom中存活下来的对象复制到SurvivorTo区,From区和To区交换。
标记清除算法。Major GC/Full GC 是指发生在老年代的 GC,出现了Major GC通常会伴随至少一次Minor GC。Major GC的速度通常会比 Minor GC慢10倍以上。
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种 :
类加载器分类:
类装载分为以下 5 个步骤:
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载器无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
总结一下:当一个类加载器接收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。