JVM主要由四个部分组成:
1. 类加载器(ClassLoader) 2. 运行时数据区(Runtime Data Area) 3. 执行引擎(Execution Engine) 4. 本地库接口(Native Interface)各组件的作用:首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载,类加载通过后,接下来分配内存,若Java堆中内存是绝对规整的,使用“指针碰撞”方式分配内存;如果不是规整的,就从空闲列表中分配,叫做“空闲列表”方式。划分内存时还需要考虑一个并发问题,也有两种方式:CAS同步处理或者本地线程分配缓冲,然后内存空间初始化操作,接着是做一些必要的对象设置,最后执行<init>方法。
Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有 句柄 和 直接指针 两种方式。
指针: 指向对象,代表一个对象在内存中的起始地址。 句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。
句柄访问 Java堆中划分出一块内存来作为句柄池,Java栈的局部变量表中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造如下图所示: 优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
直接指针 如果使用直接指针访问,引用中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。 优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中,虽然Java由GC垃圾回收机制,不再被使用的对象会被GC自动回收,但是还是存在内存泄漏问题,比如:
长生命周期的对象持有短生命周期对象的引用,尽管短生命周期已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。监听器:释放对象的时候没有删除监听器;各种连接:比如数据库连接(dataSourse.getConnection()),网络连接(socket) 和 IO 连接,除非其显式的调用了其 close() 方法将其连接关闭,否则是不会自动被 GC 回收的;垃圾回收不会发生在永久代,如果永久代满了或是超过了临界值,会触发完全垃圾回收(Full GC),另外,Java8中已经移除了永久代。
新生代:主要以复制算法为主
年轻代内存按照8:1:1的比例分为一个Eden区和两个Survivor(Survivor0、Survivor1)区,大部分对象在Eden区中生成,回收时先将Eden区存活对象复制到一个Survivor0区,然后清空Eden区,当这个Survivor0区也存满了时,则将Eden区和Survivor0区存活的对象复制到另一个Survivor1区,然后清空Eden区和这个Survivor0区,此时Survivor0区是空的,然后将Survivor0区和Survivor1区交换,即保持Survivor1区为空,如此往复。当Survivor1区不足以存放Eden区和Survivor0区存活的对象时,则将存活对象直接放到老年代,若是老年代也满了,就会触发一次Full GC(Major GC),年轻代和老年代都进行回收。年轻代发生的GC叫做Minor GC,Minor GC发生的频率比较高(不一定等Eden区满了才触发)每次从Survivor0到Survivor1移动存活的对象,年龄就加1,当年龄到达15时(默认值),升级为老年代;大对象直接进入老年代 老年代:主要以标记整理为主在年轻代中经历了N次垃圾回收仍然存活的对象就会被放到老年代中,因此,可以认为老年代中存放的都是一些生命周期比较长的对象;老年代内存比年轻代内存大很多,当老年代内存满时会触发Major GC(Full GC)。CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的Java类型。 注意:类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类,如果一次性加载所有类,那么会占用很大的内存。
类的加载方式有两种:
隐式加载,程序在运行过程中遇到new等方式生成对象时,隐式调用类加载器来加载对应的类到JVM中;显式加载:通过class.forName()等方法显式加载需要加载的类。加载: 完成以下三件事:
通过类的完全限定名称获取定义该类的二进制字节流;将该字节流表示的静态存储结构转换为方法区的运行时存储结构;在内存中生成一个代表该类的Class对象,作为方法区中该类各种数据的访问入口;获取二进制字节流有以下几种方式:
从ZIP包读取,常见的有:JAR,WAR从网络中获取,如:Applet运行时计算生成,如:动态代理技术,使用ProxyGenerator.generateProxyClass的代理类的二进制字节流验证: 确保Class文件的字节流中包含的信息是符合当前虚拟机的要求,不会危害虚拟机自身的安全
准备: 为类变量分配内存并设置初始值,使用的是方法区的内存。 注意:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。(单例实现,为什么非静态方法中不能引用静态方法,等等)
解析: 将常量池的符号引用替换为直接引用的过程。
初始化: 初始化阶段才是真正开始执行类中定义的Java程序代码,初始化阶段是虚拟机执行类构造器方法的过程,并且根据程序去初始化类变量和其他资源。 注意的是:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。
public class Test { static { i = 0; // 给变量赋值可以正常编译通过 System.out.print(i); // 这句编译器会提示“非法向前引用” } static int i = 1; }使用:
卸载:
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
主要有以下四种类加载器:
启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定在JVM中的唯一性,每个类加载器都有一个独立的类名称空间,类加载器就是根据指定的全限定名称将class文件加载到JVM内存,然后再转化为java.lang.Class对象。 双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成(注意:这里的父子关系一般是通过组合关系来实现的,而不是继承实现的),每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
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 详细信息。