不在需要为每一个new操作去写配对的delete和free操作,不容易出现内存泄漏和内存溢出的问题。但是不好排除。
Java虚拟机在执行java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。以及创建和销毁的时间。
1)程序计数器:
一块较小的内存空间,看做线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复就是依赖程序计数器完成。
2)Java虚拟机栈:
描述Java方法执行的内存模型,与计数器一般,java虚拟机栈也是线程私有的,它的生命周期与线程相同,每个方法在执行的同时会创建一个栈帧,用于存储局部变量表,操作数栈,动态链表,方法出口等信息。每个方法从调用到执行完成过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
3)本地方法栈:
与虚拟机类似,区别是虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。也会抛出StackOverflowError异常,OutOfMemoryError异常
4)Java堆:
java 堆是虚拟机所管理的内存中最大的一块,java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,唯一作用是用来存放对象实例,几乎所有的对象实例以及数组都要在堆上分配地址。
5)方法区:
方法区与java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态常量,即编译器编译后的代码数据等。也称为永久代,
6)运行时常量池:
是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
直接内存:
1)对象的创建:
2)对象的内存布局:
在HotSpot虚拟机中,对象在内存中的存储分为三部分:对象头,实例数据,对齐填充。
3)对象的访问定位:
Java程序通过栈上的reference数据来操作堆上的具体对象,访问方式有使用句柄和直接指针两种。
虚拟机参数设置
1)Java堆溢出:
只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制。在对象到达最大堆的容量后就会产生内存溢出异常。
设置虚拟机参数;
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
//限制Java堆的大小为20MB,不可扩展,将堆的最小值与-Xms参数与最大堆-Xmx参数设置为一样即可以避免自动扩展。
//参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆储快照
package com.company;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: Liruilong
* @Date: 2019/7/13 10:03
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject{
}
public static void main(String[] args){
List list = new ArrayList<>();
while (true){
list.add(new OOMObject());
}
}
}
运行结果:
/*
java.lang.OutOfMemoryError: Java heap spaceDumping heap to java_pid13048.hprof ...
Heap dump file created [29202866 bytes in 0.128 secs]Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3204)
at java.util.Arrays.copyOf(Arrays.java:3175)
at java.util.ArrayList.grow(ArrayList.java:246)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:220)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:212)
at java.util.ArrayList.add(ArrayList.java:443)
at com.company.HeapOOM.main(HeapOOM.java:19)
*/
如果为内存泄漏:通过工具查看泄漏对象的到GC Roots的引用链。如果不存在泄漏就调整虚拟机参数。
2)虚拟机栈和本地方法栈溢出:
HotSpot虚拟机中并不区分本地方法栈和虚拟机栈, -Xoss可以设置本地方法栈的大小,栈容量由-Xss设置。关于虚拟机栈的异常,,描述为:
使用-Xss参数设置减少栈内存容量,抛出StackOverfiowError异常,堆栈深度相应缩小。
定义了大量的本地变量,增加方法帧中局部变量表中的长度,抛出StackOverfiowError异常,堆栈深度相应缩小。
设置虚拟机参数:
-Xss128k
//设置堆栈内存为128k
代码:
package com.company;
/**
* @Author: Liruilong
* @Date: 2019/7/13 18:00
* @VM Args: -Xss128k
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeng(){
stackLength++;
stackLeng();
}
public static void main(String[] args)throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try{
oom.stackLeng();
}catch (Throwable throwable){
System.out.println("sstack length:"+oom.stackLength);
throw throwable;
}
}
}
运行结果:
/*
sstack length:1001
Exception in thread "main" java.lang.StackOverflowError
at com.company.JavaVMStackSOF.stackLeng(JavaVMStackSOF.java:13)
at com.company.JavaVMStackSOF.stackLeng(JavaVMStackSOF.java:14)
………………
*/
单线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配内存时,都会抛出StackOverflowError异常。
虚拟机提供参数来控制Java堆和方法区这两部分内存的最大值。减去最大堆Xmx,在减去最大方法区MaxPermSize,程序计数器可以忽略,如果虚拟机本身的内存忽略不计,剩下的内存就由虚拟机和本地方法栈瓜分了。
创建多线程导致内存溢出。
package com.company;
/**
* @Author: Liruilong
* @Date: 2019/7/13 18:00
* @VM Args: -Xss128k
*/
public class JavaVMStackSOF {
private void dontStop(){
while (true){
}
}
public void stackLeaakByThread(){
while (true){
Thread thread = new Thread(() -> {dontStop();});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
oom.stackLeaakByThread();
}
}
//嗯,没有运行出来!!!
3)方法区和运行时常量池溢出
String.intern()是一个Native方法,当字符串对象已经包含一个String的字符串引用时,则返回字符串的引用,反之,将字符串添加到常量池中,并发挥Sting对象的引用。
在JDK1.6之前的版本,由于常量池分配永久代中,所以可以通过限制方法区的大小来限制常量池容量。
package com.company;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: Liruilong
* @Date: 2019/7/14 7:05
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持常量池的引用,避免Full GC回收常量池行为
List list = new ArrayList<>();
int i = 0;
while (true){
list.add(String.valueOf(i++).intern());
}
}
}
package com.company;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: Liruilong
* @Date: 2019/7/14 7:05
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimConstantPoolOOM {
public static void main(String[] args) {
String str = "Java";
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("Ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
这段代码在JDK1.6中执行,会返回两个 false,在JDK1.7及更高版本中,返回 true,false。
1.6中intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用。
而由StringBuilder创建的字符串实例在Java堆中,所以必然是不是同一个引用,返回 false 。
而jdk1.7中inter方法不再复制实例,只是在常量池中记录首次出现的实例的引用,
因此inter()返回的引用和StringBuilder创建的那个字符串是同一个引用,所以返回 true
返回 false 的原因是Java在执行str2.intern()时已经出现了不符合“首次出现的原则”,而计算机软件是首次出现的。
方法区用于存放Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。
4)本机直接内存溢出:
DirectMemory容量可以通过-XX:MaxDirectMemorySize指定。
1)程序计数器,虚拟机栈,本地方法栈这些随着线程而生,随线程而灭,栈中的栈帧随方法的进栈退栈而存在,这些私有内存不考虑垃圾回收。
2)Java堆和方法区的内存的回收和分配都是动态的 ,垃圾收集关注的也是这部分内容。
1)引用计数算法:
给对象添加一个引用计数器,每当有一个地方引用他时,计数器就加1,当引用失效时,计数器就减1,任何时对象引用计数器为0则回收。Java没有使用引用计数器算法来实现管理内存。他很难解决对象之间的循环调用问题。
2)可达性分析算法:
在主流的商用语言中都采用可达性分析算法来判断对象是否存活,基本思想是通过称为GC Roots的对象作为起点,从该节点开始向下搜索,搜索走过的路劲称为引用链,当一个对象到GC Roots没有任何引用链相连接的时候(即从GC Roots到这个对象不可达),即对象是不可用的。才被判定为可回收对象。
3)在Java语言中,可做为GC Roots的对象包括:
4)在谈引用:
在JDK1.2前,引用的定义为:如果reference类型的数据中存储的数值代表了另一块内存的其实地址,就称这块内存代表一个引用。
在JDK1.2后,分为强引用,软引用,弱引用,虚引用四种。
5)生存还是死亡:
如果对象在进行可达性分析之后发现没有GC Roots相连接的引用链,那将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,都视为没有必要执行。
如果被判定为有必要执行finalize(),那么对象将会放置一个叫F-Query的队列中,并在稍后由虚拟机自动建立Finalize线程去执行。
6)回收方法区:
在堆中,尤其是在新生代中,常规的应用进行一次垃圾收集一般可以回收70%-80%的空间,而永久代垃圾回收很低。
永久代的垃圾回收主要是两部分内容,废弃常量和无用的类,
回收废弃常量与回收Java堆中的对象非常类似。
以常量池中字面量的回收为例,当系统中没有任何一个String对象的引用常量池中的常量时,也没有其他地方引用这个字面量时,发生内存回收的话会被回收掉。
回收无用的类的条件:
1)标记-清除算法:
最基础的收集算法,算法分为两个阶段,首先标记出所需要回收的对象,在标记完成后统一回收所标记的对象,
缺点:效率不高,空间问题,标记清除会产生大量不连续的内存碎片,可能当需要分配较大对象的内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2)复制算法:
为了解决效率问题,将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次清理掉。即不用考虑内存碎片问题,缺点为实际使用内存变为原来的一半。一般用来回收新生代。使用额外空间对它进行分配担保。
复制算法在对象存活率较高时就要进行较多的复制操作,效率会变低,一般老年代不使用这种算法。
3)标记-整理算法:
根据老年代的特点,在标记清除算法的基础上不是直接对可回收对象进行清理。而是让所有存活的对象都向一端移动然后直接清理掉边界以外的内存。
4)分代收集算法:
根据对象的存活周期的不同将内存分为几块,即把Java堆内存分为新生代和老年代,然后根据各个年代的特点选择合适的收集算法,在新生代中,每次都有大量的对象死去,少量存活,采用复制算法,在老年代中,对象的存活率高,没有额外空间进行分配担保必须使用,就必须使用“标记清除法”和“标记整理法”进行回收。
这部分内容以后看,98~183
1)枚举根节点
2)安全点
3)安全区域
Class文件中包含了java虚拟机 指令集和符号表以及若干其他的辅助信息。任何一个Class文件都对应唯一一个类或接口的定义信息。但类或接口不一定要定义到文件里。
根据Java虚拟机规范,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,有两种数据类型,无符号数和表。
无符号数:基本的数据类型,以一,u1,u2,u4,u8来表示1,2,4,8个字节的无符号数,无符号数可以用来描述数字,索引引用,数值量,或者按照utf-8的编码构成字符串值。
表:由多个无符号数或者其他表作为数据项构的复合数据类型,所有表习惯以info结尾。描述有层次关系的复合数据结构。
虚拟机类加载机制:虚拟机把描述类的数据从Class文件加载到文件,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。
在Java语言中,类型的加载,连接和初始化过程都是在程序运行期间完成的。Java天生可以动态扩展的特性就是依赖运行期动态加载和动态连接特点实现的。
类从被加载到虚拟机内存中开始,到卸载出内存为止,生命周期包括:
加载阶段(加载class文件)与连接阶段(将二进制数据合并到JRE中)的部分内容是交叉进行的。
什么情况下开始类加载过程的
第一阶段:Java虚拟机闭并没有强制约束。
对初始化阶段,虚拟机严格规范了五种情况必须立即对类进行“初始化”。
有些只有这5中场景中的行为被称为主动引用。所有引用方式都不会触发初始化,称为被动引用。
package com.liruilong.jvmdemo;
/**
* @Description : 虚拟机代码
* 被动使用类字段演示一
* 通过子类引用父类的静态字段,不会导致子类初始化
* @Author: Liruilong
* @Date: 2019/8/21 14:14
*/
public class SuperClass {
static {
System.out.println("SuperClass init");
}
public static int value = 123;
}
package com.liruilong.jvmdemo;
/**
* @Description :
* @Author: Liruilong
* @Date: 2019/8/21 14:24
*/
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init");
}
}
package com.liruilong.jvmdemo;
/**
* @Description :
* @Author: Liruilong
* @Date: 2019/8/21 14:33
*/
public class NotInitialization {
public static void main( String[] args){
System.out.println(SubClass.value);
}
}
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化,而不会处触发子类的 初始化。
package com.liruilong.jvmdemo;
/**
* @Description : 被动使用类字段实例二
* 通过数组定义引用类,不会触发此类的初始化。
* @Author: Liruilong
* @Date: 2019/8/21 14:33
*/
public class NotInitialization {
public static void main( String[] args){
// System.out.println(SubClass.value);
SuperClass[] superClasses = new SuperClass[10];
}
}
通过数组定义引用类,不会触发此类的初始化。
package com.liruilong.jvmdemo;
/**
* @Description :被动使用类字段三,
* 常量在编译阶段会存入调用类的常量池,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
* @Author: Liruilong
* @Date: 2019/8/21 15:00
*/
public class CinstClass {
static {
System.out.println("CinstClass init ");
}
public static final String LIRUILPNG = "liruilong";
}
package com.liruilong.jvmdemo;
/**
* @Description :
* @Date: 2019/8/21 14:33
*/
public class NotInitialization {
public static void main( String[] args){
System.out.println(CinstClass.LIRUILPNG);
}
}
当一个类被初始化时,要求其父类全部都已经初始化过了,但是一个接口中初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口才会初始化。
1)加载(是类加载的第一阶段)
在加载阶段可以使用系统提供的引导类加载器完成,也可以使用用户自定义的类加载器完成。
数组类本身不通过类加载器去创建,而是由Java虚拟机直接创建,但数组类的元素类型最终要靠类加载器完成。
一个数组类创建过程遵循原则:
2)验证:
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机的自身安全。验证阶段:
3)准备:
4)解析:
5)初始化:
pubilce class Test{
static{
int i = 0; //给变量赋值可以正常编译通过
System.out.println(i ); // 这个话会提示非法先前引用
}
static int i =0;
}
// 父类中定义的静态预计块要优于子类的变量复制操作。 输出2 而不是1
static class Parent {
public static int A = 1;
static{
A = 2;
}
}
static class Sub extends Parent{
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
1)类与类的加载器
2)双亲引用类型
3)破坏双亲引用类型
1)局部变量表
2)操作数栈
3)动态连接
4)方法返回地址
5)附加信息
1)解析
2)分派
3)动态类型语言支持
1)解释执行
2)基于栈的指令集与基于寄存器的指令集
3)基于栈的解释器执行过程。