JVM
内存结构
JVM的内存结构包括了两个部分:线程公有和线程私有。线程公有:堆和方法区。线程私有:虚拟机栈、本地方法栈和程序计数器。
虚拟机栈
虚拟机栈中存储的是一个个的栈帧的信息。当执行一个方法的时候,就会创建一个栈帧。
栈帧中存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧的入栈和出栈。当编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经确定,不会随着程序的运行而变化。
局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表以变量槽为最小的单位,每个变量槽可以存放一个32位以内的数据类型。访问局部变量表的方式是通过索引的方式访问局部变量表。
一个局部变量可以保存一个类型为boolean、byte、short、int、float、reference和returnAddress类型的数据。reference表示对一个实例对象的引用。通过reference,虚拟机可以直接或者间接的找到对象在Java堆中的数据存放的其实地址索引和所属数据类型在方法区中存储的类型数据。
操作数栈
操作数栈和局部变量表类似,都是一组操作指令的存储空间。只不过访问操作数栈的方式和局部变量表不一样,访问操作数栈是通过压栈和出栈的方式访问并执行对应的指令。
本地方法栈
本地方法栈和虚拟机栈的作用类似,区别在于作用的对象不同。虚拟机栈存储的栈是为非native服务,对于native方法的调用,需要使用本地方法栈。
程序计数器
程序计数器包含了当前线程执行的指令的位置,是一个类指针的数据结构。
当一个线程正在执行一个Java方法的时候,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,这个计数器值则为空。
堆
Java中的堆是对象存放的位置。堆主要分为了两个大的部分。新生代和老年代。对于小对象的创建,会直接在新生代申请内存。对于满足晋升老年代的对象,会存放在老年代中。
堆中的垃圾回收参考垃圾回收。
新生代可以划分为两个部分:一个Eden区和Surivior区。
新生代晋升规则:
-
年龄判断
当进行一次Young GC的时候,存活的对象年龄都会加1,如果对象的年龄大于等于15就会移动到老年代(参数配置:XX:MaxTenuringThreshold)。
-
动态年龄判断
如果是仅根据年龄判断,可能有的大对象会不断的在新生代移动,降低效率。如果当一批对象的总大小超过了新生代一半内存的时候,此时大于这批对象最大年龄的对象,会移动到老年代。
-
大对象
如果对象的大小超过了配置的XX:PretenureSizeThreshold,则会进入到老年代
可以使用一下参数配置各大小的值:
- JVM运行时堆的大小
- -Xms:堆的最小值
- -Xmx:堆的最大值
- 新生代堆空间大小调整
- -XX:NewSize:新生代的最小值
- -XX:MaxNewSize:新生代的最大值
- -XX:NewRatio:设置新生代与老年代在堆空间的大小
- -XX:SurvivorRatio:新生代中Eden所占区域的大小
- 永久代大小调整
- -XX:MaxPermSize
- 其他
- -XX:MaxTenuringThreshold:设置将新生代对象转到老年代时需要经过多少次垃圾回收,但是仍然没有被回收
方法区
方法区和堆一样,都是线程共享的内存空间。方法区中会存储每个类的结构、运行时常量池、字段和方法数据以及方法和构造函数的代码,包括类和实例初始化以及接口初始化的特殊方法和类的常量池信息等。
垃圾回收
Java中不需要程序员手动的释放对象占用的内存,也不需要手动的调用相关的接口去清理内存,JVM会自动的在后台使用一个守护线程在适当的时机执行垃圾回收,根据判断GC Roots判断对象是否存活,然后回收未存活的对象的内存。
原理
当垃圾回收线程回收垃圾的时候,会通过遍历GC ROOT根节点,找出所有被GC Root直接或间接引用的对象。然后标记该对象,最终清除未被标记的对象即可。
如此看来,便可以找到所有的不活跃的对象了。但是如何找到所有的GC Roots 呢?
成为GC Roots的对象一般是在栈帧中。如果扫描全部的栈帧,然后逐步的判断,一定是可以找到所有的GCC Root,但是需要遍历所有的栈帧,时间消费比较高。所以JVM选择了使用空间换时间。因为在编译的时候,对于每一个栈帧,都可以知道其对应的变量表,就可以将变量表的信息存放到一个Map中,对于其他的信息,都存放在类的常量池和运行是常量池中,类常量池在编译的时候也可以知道其对应的信息,运行时常量池可以很方便的遍历。因此下次遍历GC Root的时候,就可以通过遍历上面的Map,即可找出所有存活的对象。
GC Roots
可以成为GC Root的对象有哪些?
- 当前活跃线程的栈帧中指向堆的对象引用
- 类的引用类型的静态变量
- 方法区中常量池引用的对象
- 本地方法栈中引用的对象
类型
垃圾回收器有多种类型,可以根据其使用的位置进行区分:
新生代
作用于新生代的垃圾回收器只会在出发Young-GC 的时候,回收新生代的对象。
-
Serial:复制算法
Serial收集器是新生代的单线程垃圾回收器,优点是简单高效。由于是单线程的,所以在收集垃圾的时候,必须暂停其他所有的工作线程,直到他收集完成。Serial依然是虚拟机运行在Client模式下默认的新生代收集器。
-
PraNew:复制算法
PraNew是新生代的并行垃圾收集器,是Serial收集器的多线程版本。
-
Parallel Scavenge:复制算法
Parallel Scavenge收集器是新生代并行收集器
老年代
作用与老年代的垃圾回收器,只会在出发Full-GC 的时候,回收老年代的对象。
-
Serial Old:标记整理算法
Serial Old是Serial的老年代收集版本,单线程,也是在Client模式下使用。如果在server模式下,主要有两个作用:在JDK1.5之前的版本中,与Parallel Scavenge收集器搭配使用;作为CMS收集器的后备预案,在并发收集发生Concurrent Model Failure错误的情况下使用。
-
Parallel Old:标记整理算法
Parallel Old 是Parallel Scavenge的老年代版本
-
CMS:标记整理算法
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。
CMS的整个步骤分为4步:
-
初始标记:STW
-
并发标记
-
重新标记:STW
-
并发清除
优点
-
低停顿
-
并发收集
缺点
- 无法处理浮动垃圾,可能会出现Concurrent Mode Failure,而导致另一次的Full GC
- 产生大量的空间碎片
-
整个堆
-
G1收集器:标记整理算法
- 使用分区算法,不要求eden、新生代、老年代的空间都连续
- 并行性:回收期间,多个线程同时工作
- 空间整理:回收过程会进行适当的空间移动,减少空间碎片。
- 可预见性:可选取部分区域进行回收,缩小可回收的范围,减少全局停顿。
G1收集器的执行步骤
- 初始标记(它标记了从GC Root开始直接可达的对象);
- 并发标记(从GC Roots开始对堆中对象进行可达性分析,找出存活对象);
- 最终标记(标记那些在并发标记阶段发生变化的对象,将被回收);
- 筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)。
对于CMS产生的内存碎片,可以使用
UseCMSCompactAtFullCollection参数在进行多次的垃圾回收之后,进行一次内存空间的整理,以减少内存碎片。
算法
垃圾回收算法主要有三个:标记清除、标记整理、标记复制。
标记清除
标记清除算法会经过GC Roots的标记后,将未被标记的对象清除,完成清除操作。标记清除会产生大量的内存碎片,当后续申请大的空间的时候,可能会再次出发GC。
标记整理
标记整理在标记清除的基础上,会将存活的对象移动的一起,减少内存碎片。缺点是需要进行一次内存的压缩,会耗费更多的时间。
标记复制
标记复制会把标记的对象,移动到一个未使用的区域,然后将旧区域的数据全部清除。缺点是会浪费一定的内存空间。
类加载
Java中的类加载器主要有三类:应用类加载器、扩展类加载器、启动类加载器。不同的类加载器加载数据的对象不一致。
类的加载过程中,满足双亲委派模型,只有在父类都未加载的情况下,才会进行加载。类的加载过程是:
- 加载:通过一个类的完全限定名查找此类的class文件,并利用class文件一个Class对象
- 验证:目的是为了保证Class文件的信息符合JVM的规范,不会危害JVM自身的安全。主要包括四种验证:文件格式验证、元数据验证、字节码验证和符号引用验证
- 准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
- 解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析。
- 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。
类加载器
启动类加载器
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
扩展类加载器
扩展类加载器是指Sun公司实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
应用类加载器
也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
通过这种模式可以避免一个类被重复加载;也可以避免Java提供的一些核心的包被修改。例如如果想要加载一个java.lang.TestInteger,加载时候会抛出异常。
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:656)
at java.lang.ClassLoader.defineClass(ClassLoader.java:755)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:419)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
at java.lang.ClassLoader.loadClass(ClassLoader.java:352)
at Main.main(Main.java:7)
引用
强引用
被强引用关联的对象,无论如何都不会被垃圾回收器回收。
软引用
如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用所引用对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的,较新的"软对象会被虚拟机尽可能保留,这就是引入引用队列ReferenceQueue的原因。
弱引用
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会在下一次垃圾回收的时候回收它的内存。
虚引用
虚引用主要用来跟踪对象被垃圾回收器回收的活动。 虚引用与软引用和弱引用的一个区别在于:
虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。当虚引用的对象会被回收的时候,会放在queue中,以达到通知的效果。