JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native interface(本地接口)。
- Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装在class文件到Runtime data area中的method area。
- Execution engine(执行引擎):执行classes中的指令。
- Native interface(本地接口):与native libraries交互,是其他编程语言交互的接口。
- Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
作用:
首先通过编译器吧Java代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是JVM的一套指令集规范,而不能直接交到底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由CPU去执行,而这个过程中需要调用其他语言的本地库接口(Native interface)来实现整个程序的功能。
Java程序运行机制步骤:
首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;
运行字节码的工作是由解释器(java命令)来完成的。
从上图可以看出,java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。
其实就是一句话:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
Java虚拟机在执行Java程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java虚拟机所管理的内存被划分为如下几个区域:
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从Java虚拟机规范,Java虚拟机规范规定的区域分为以下5个部分:
- 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成。一块较小的内存空间,可以看做当前线程所执行字节码的行号指示器。在JVM规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;
- Java虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息。线程私有,每个方法在执行的同时都会创建一个栈帧,每个栈帧对应一个被调用的方法,栈帧中用于存储局部变量表、操作数栈、动态链表、方法出口等信息。每一个方法从开始执行到结束都对应一个栈帧在虚拟机栈中入栈和出栈的过程;
- Java 堆(Java Heap):它是java内存管理的核心区域,用来放置java对象实例,几乎创建的java对象实例都是被直接分配到堆上。堆被所有的线程共享,在虚拟机启动时,我们指定的“Xmx"之类参数就是用来指定最大堆空间等指标;
- 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
- 方法区(Methed Area):这也是所有线程共享的一块内存区域,用于存储所谓的元数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等;运行时常量池,这也是方法区的一部分,用于存放编译期间生成的各种字面量和符号引用;
Java堆从GC的角度还可以细分为:新生代(Eden区,From Survivor区和 To Survivor区)和老年代。
新生代:
是用来存放新生的对象。一般占据堆的1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾 回收。新生代又分为Eden 区、ServivorFrom、 ServivorTo 3个区。新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1
执行流程:
- 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
- 清空 Eden 和 From Survivor 分区;
- From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
- 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老年代:
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC 前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。
当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出****OOM (Out of Memory)异常。
指内存的永久保存区域,主要存放Class 和Meta (元数据)的信息,Class在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class 的增多而胀满,最终抛出OOM异常。
JAVA8与元数据 :
在Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
类的元数据放入native memory,字符串池和类的静态变量放入java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
Minor GC:简单理解就是发生在年轻代的GC。三步(复制–清空–互换)
Minor GC的触发条件为:当产生一个新对象,新对象优先在Eden区分配。如果Eden区放不下这个对象,虚拟机会使用复制算法发生一次Minor GC,清除掉无用对象,同时将存活对象移动到Survivor的其中一个区(fromspace区或者tospace区)。
虚拟机会给每个对象定义一个对象年龄(Age)计数器,对象在Survivor区中每“熬过”一次GC,年龄就会+1。待到年龄到达一定岁数(默认是15岁),虚拟机就会将对象移动到年老代。
如果新生对象在Eden区无法分配空间时,此时发生Minor GC。发生MinorGC,对象会从Eden区进入Survivor区,如果Survivor区放不下从Eden区过来的对象时,此时会使用分配担保机制将对象直接移动到年老代。
1.第一次Yong GC(Minor GC)后,Eden区还存活的对象复制到Surviver区的“To”区,“From”区还存活的对象也复制到“To”区,
2.再清空Eden区和From区,这样就等于“From”区完全是空的了,而“To”区也不会有内存碎片产生,
3.等到第二次Yong GC时,“From”区和“To”区角色互换,很好的解决了内存碎片的问题。
Major GC的触发条件:
Major GC又称为Full GC。当年老代空间不够用的时候,虚拟机会使用“标记—清除”或者“标记—整理”算法清理出连续的内存空间,分配对象使用。
1、加载:主要是将.class文件中的二进制字节流读入到JVM中
2、验证:连接阶段的第一步,为了确保.class文件的信息符合当前虚拟机要求,并且不会危害虚拟机的自身安全。
3、准备:是正式为静态变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配
4、解析:是虚拟机将符号引用替换为直接引用的过程
5、初始化:是类加载过程的最后一步,是一个执行类构造器()方法的过程,就是给static变量赋予用户指定的值以及执行静态代码块
6、使用
7、卸载
OOM(OutOfMemoryError):JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error
原因:
1、原生内存不足(操作系统不允许申请更大的内存)
2、永生代或元空间不足
3、JVM执行GC耗时太久按照JVM规范,除了程序计数器不会抛出OOM外,其他各个内存区域都可能会抛出OOM。
情况:
java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。
java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况
java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。
1、设定堆内存大小
-Xmx:堆内存最大限制
2、设定新生代大小,新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小
-XX:NewRatio:新生代和老年代占比
-XX:SurvivorRatio:Eden区空间和Survivor区的占比
3、设定垃圾回收器:
年轻代:-XX:+USeParNewGC
老年代:-XX:+UseConcMarkSweepGC
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。
物理地址
堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
内存分别
堆因为是不连续的,所以分配的内存是在
运行期
确认的,因此大小不固定。一般堆大小远远大于栈。 栈是连续的,所以分配的内存大小要在
编译期
就确认,大小是固定的。存放的内容
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
PS:
- 静态变量放在方法区
- 静态的对象还是放在堆。
程序的可见度
堆对于整个应用程序都是共享、可见的。
栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收
怎么来判断一个对象是否应该被回收
- 可达性分析
通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,基本就成为可回收对象了。
- 引用计数器法:
为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。
1、强引用
如果一个对象具有强引用,他就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出OutOfMemoryError错误,使程序异常终止。如果想中断强引用和某个对象的之间的关联,可以显式的将引用赋值为null,这样JVM在合适的时间就会回收该对象;
2、软引用
在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存空间不足时,软引用才会被垃圾回收器回收;
3、弱引用
具有弱引用的对象拥有的生命周期更短暂。因为当JVM进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象;
4、虚引用
顾名思义就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。
虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否加入了虚引用,来了解被引用的对象是否要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
GC最基础的算法有三种:
标记-清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法
标记-清除算法:
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
复制算法:
“复制”的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存使用完了,就将还存活的对象复制到另一块上面,然后再把已经使用完了的内存空间一次清理掉。
标记-压缩算法:
标记过程仍然和“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法:
把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法
Serial收集器:
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
ParNew收集器:
ParNew收集器其实就是Serial收集器的多线程版本
Parallel收集器:
Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量
Parallel Old收集器:
是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
CMS收集器:
是一种以获取最短回收停顿时间为目标的收集器
G1收集器:
G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器.以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
Minor GC:
从年轻代空间(包括Eden和Survivor区域)回收内存
Major GC
是清理老年代
Full GC:
是清理整个堆空间(年轻代和老年代)
Minor GC触发条件:当Eden区满时,触发Minor GC
Full GC触发条件:
1、调用System.gc时,系统建议执行Full GC,但是不必然执行
2、老年代空间不足
3、方法区空间不足
4、通过Minor GC后进入老年代的平均大小大于老年代的可用空间
5、由Eden区,from Space区向To Space区复制时,对象大小大于To Space可存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小;
Full GC:
是清理整个堆空间(年轻代和老年代)
1、新生代设置过小:
一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入老年代,占据了老年代剩余空间,诱发Full GC
2、新生代设置过大:
一是新生代设置过大会导致老年代过小,二是新生代GC耗时大幅度增加
3、Survivor设置过小:
导致对象从Eden直接进入老年代
4、Survivor设置过大:
导致Eden过小,增加了GC频率
一般来说新生代占整个堆1/3比较合适
GC策略的设置方式:
1、吞吐量优先:-XX:GCTimeRatio=n来设置
2、暂停时间优先:-XX:MaxGcPauseRatio=n来设置
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种 :
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
2.显式装载, 通过class.forname()等方法,显式加载需要的类
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器:
- 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
1、加载:主要是将.class文件中的二进制字节流读入到JVM中
2、验证:连接阶段的第一步,为了确保.class文件的信息符合当前虚拟机要求,并且不会危害虚拟机的自身安全。
3、准备:是正式为静态变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配
4、解析:是虚拟机将符号引用替换为直接引用的过程
5、初始化:是类加载过程的最后一步,是一个执行类构造器()方法的过程,就是给static变量赋予用户指定的值以及执行静态代码块
6、使用
7、卸载
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
- jconsole:用于对 JVM 中的内存、线程和类等进行监控;
- jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
- -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 详细信息。