SUN公司的Hotspot VMBEA公司的JRockitIBM公司的J9
Hotspot有方法区,而JRockit和J9都没有方法区,我们大多数用的第一种
JVM的主要组成部分
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
Class loader(类装载):根据给定的全限定名类名(如java.lang.Object)来装载class文件到Runtime data area中的方法区Execution engine(执行引擎):执行classes中的指令。Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。 JVM 运行时数据区
程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。 Java程序运行机制
编写Java源代码,源文件的后缀为.java;再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;类加载器把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行命令解析器执行引擎(ExecutionEngine),将字节码翻译成底层系统指令交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
虚拟机类加载机制
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类
显式装载, 通过class.forname()等方法,显式加载需要的类.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中
类加载器Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
扩展类加载器(Extension ClassLoader):负责加载libext目录或Java、ext、dirs系统变量指定的路径中的所有类库;
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派机制
如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
例如:
package java.lang; //本地创建的String类public class String { public String toString(){ return "hello"; } public static void main(String[] args) { String s = new String(); s.toString(); }}
由于双亲委派机制,会一直往上寻找java.lang.String类,父加载器找到了,子加载器不会去加载
物理地址:
堆的物理地址分配对对象是不连续的。因此性能慢些。
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。内存分别:
堆因为是不连续的,所以分配的内存是在 运行期 确认的,因此大小不固定。一般堆大小远远大于栈。
栈是连续的,所以分配的内存大小要在 编译期 就确认,大小是固定的。存放的内容:
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。程序的可见度
堆对于整个应用程序都是共享、可见的。
栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。GC机制:针对的是堆
栈内存,主管程序的运行,生命周期和线程同步,线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就over 堆的分区
Eden区:新对象都是在此new出来的
老年代区:经过GC机制未清理的对象(长期存活的对象或者新生代无法容纳的大对象)进入老年代
元空间:逻辑上存在,物理上不存在这个区域常驻内存,用来存放JDK自身携带的Class对象,Interface元数据,java运行时的一些环境或类信息,不存在垃圾回收,关闭JVM就会释放这个区域的内存
元空间的发展历程: 1.6之前 永久代 常量池是在方法区中 1.7永久代 但慢慢退化了-有了去永久代的想法 常量池在堆中 1.8后 无永久代 常量池在元空间中 老年代中的对象: 年龄达到一定的程度(默认是15)的对象 在 survivor 空间中相同年龄所有对象大小的总和>survivor空间的一半
GC垃圾回收机制当程序运行时,至少会有两个线程开启启动,一个是我们的主线程,一个时垃圾回收线程,垃圾回收线程的priority(优先级)较低。垃圾回收器会对我们使用的对象进行监视,当一个对象长时间不使用时,垃圾回收器会在空闲的时(不定时)对对象进行回收,释放内存空间,程序员是不可以显示的调用垃圾回收器回收内存的,但是可以使用System.gc()方法建议垃圾回收器进行回收,但是垃圾回收器不一定会执行。
Java垃圾回收机制可以有效的防止内存溢出问题,但是它并不能完全保证不会出现内存溢出。
例如:
package com.heng;import java.util.Random;public class Text { public static void main(String[] args) { String s="liuheng"; while (true){ s+= s+new Random().nextInt(88888888)+new Random().nextInt(88888888); } }}
调参获取GC细节 -Xms8m -Xmx8m -XX:+PrintGCDetails
内存满了,内存发生溢出:
如何解决这个问题:
https://plugins.jetbrains.com/plugin/253-jprofiler/versions
然后把从下载的压缩包解压出来的JProfiler文件夹,copy到IDEA自定义插件目录,默认路径:C:UsersAdministrator.IntelliJIdea2017.2configplugins
JProfiler监控软件安装
官方下载地址:https://www.ej-technologies.com/download/jprofiler/version_92
idea集成一下
-Xms2g:初始化推大小为 2g;-Xmx2g:堆最大内存为 2g;
Xms 是设置初始化内存分配大小 默认是1/64
Xmx 是设置最大分配内存 默认是1/4-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 详细信息。 GC算法 标记-清除算法 标记阶段:标记出可以回收的对象。清除阶段:回收被标记的对象所占用的空间。
优点: 实现简单,不需要对象进行移动。
缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。 复制算法
它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。
优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。
优点:解决了标记-清理算法存在的内存碎片问题。
缺点:仍需要进行局部对象移动,一定程度上降低了效率。
总结:
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
JVM 有哪些垃圾回收器?每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达15(默认配置是15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。谁空谁是to
新生代回收器:Serial、ParNew、Parallel Scavenge老年代回收器:Serial Old、Parallel Old、CMS整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
不同收集器之间的连线表示它们可以搭配使用。
面试题:详细介绍一下 CMS 垃圾回收器?
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。 CMS 使用的是标记-清除的算法实现的,所以在 gc的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。