JVM-01(阶段性学习)

发布于:2023-01-20 ⋅ 阅读:(248) ⋅ 点赞:(0)

目录

内存结构:

程序计数器:

流程:

栈:

那么问题来了,1.GC是否涉及虚拟机栈? 2.栈内存是否分配越大越好?

问:方法内的变量什么时候是线程安全的?

栈内存溢出:

栈帧:

 局部变量表:

slot复用问题?

操作数栈

 栈中可能出现的异常:

设置栈参数:

方法区

常量池有什么用?

Integer常量池:

IntegerCache:

拆箱操作:




内存结构:

 

首先我们来说说运行时数据区:

当JVM在执行java程序的时候会把它管理的内存分为若干个不同的数据区域,然后每个区域都有他自己的功能;

JVM运行数据区:

主要包括堆、栈(虚拟机栈、本地方法栈)、方法区、程序计数器(寄存器)

线程共享数据区: 堆与方法区;

程序计数器:

你可以理解为当前线程执行字节码.class的行动指示器,指向下一个要被执行的指令代码

它只负责指向(就跟内奸一样),而执行引擎—>用来读取下一条指令;

每个线程都有属于自己独立的程序计数器,每个线程的程序计数器互不影响、独立储存;

程序计数器不会发生内存溢出(OOM)问题;

方法被执行时,每行代码都会被解释器执行(javac.exe),经常被调用的会被即时编译器调用(运行时的优化),不用则垃圾回收gc();

流程:

.java—>.class—>加载到虚拟机内存中(寄存器当做程序计数器,因为需要频繁读取地址-程序计数器会记住下一条JVM指令的地址(就是上面的0、1...,然后将指令)—>给到解释器—>机器码—>CPU分配内存);

栈:

JVM中栈包括虚拟机栈本地方法栈;

区别:虚拟机栈->为JVM执行本地方法服务,而本地方法栈->为JVM使用到的Native方法服务;

Native方法是什么?

它是由本地方法实现的,也就是C,C++,我理解的Native方法是直接与操作系统直接交互的,就比如说System.gc()就是由native修饰的;

还有Object中的方法其实都是本地方法,java代码会调用接口来实现本地方法;

——这也就是本地方法栈中放入的东西;

public final class System {
    public static void gc() {
        Runtime.getRuntime().gc();
    }
}
 
public class Runtime {
    //使用native修饰
     public native void gc();

那么问题来了,1.GC是否涉及虚拟机栈? 2.栈内存是否分配越大越好?

答:不涉及,因为虚拟机栈是由一个个栈帧组成的,就像递归,方法执行完后,对应的栈帧会被弹出栈,所以无需GC;

由于物理内存是一定的,如果你栈内存很大,那么能够创建线程数目就会变少;

众所周知,栈里面的栈帧(也就是方法)是包含->1.局部变量 2.参数 3.返回值地址 这几个玩意的

问:方法内的变量什么时候是线程安全的?

答:当方法内的局部变量没有逃离方法的作用范围(返回值,参数,对象引用),则线程是安全的;

例子:

像这种局部变量就是在每个线程中 ,它们是属于私有的,所以其他线程是不能够访问的;

如果方法内的局部变量没有逃离方法的作用访问,那么线程是安全的;

像上面图片的m2与m3就不是线程安全的,前者参数并不是局部变量,所以说可能被其他线程进行修改,后者返回值sb进行返回,其他线程可能拿到;


栈内存溢出:

1.栈帧过多;

2.每个栈帧所占的内存过大;

Java.lang.stackOverflowError

栈帧:

栈帧是栈的元素,每个方法在执行时都会创建一个栈帧——>(局部变量表,操作数栈(也就是参数),方法的出口与入口) ——>每个方法从调用到运行结束,就对应这一个栈帧在栈中压栈到出栈的过程。

 

 局部变量表:

用来储存基本数据类型(局部变量+参数)+对象的引用(String、数组、对象...),但是不存储内容;

然后这些东西的内存是在编译期间就分配好了的(我理解的编译时期错误估计就有发生在这个局部变量表中)—> 在方法运行期间不会改变局部变量表的大小;

局部变量的容量以变量槽(slot)为最小单位,最大存储32位的数据类型,如果大于它,就搞两个slot(JVM给的);

JVM是通过索引定位的方式来使用局部变量表;

slot复用问题?

 当方法中定义的局部变量作用域<整个方法时,那么当方法运行时,如果已经超出了某个变量的作用域,那么变量失效了,而这个变量所对应的SLot可以交给其他变量使用(跟连接池有异曲同工之处);

例子:

public void test(boolean flag)
{
    if(flag)
    {
        int a = 66;
    }
    
    int b = 55;
}


当虚拟机运行 test 方法,就会创建一个栈帧,并压入到当前线程的栈中。当运行到 int a = 66时,在当前栈帧的局部变量中创建一个 Slot 存储变量 a,当运行到 int b = 55时,此时已经超出变量 a 的作用域了(变量 a 的作用域在{}所包含的代码块中),此时 a 就失效了,变量a 占用的 Slot 就可以交给b来使用,这就是 Slot 复用。
副作用:

public class TestDemo {
 
    public static void main(String[] args){
        
        byte[] placeholder = new byte[64 * 1024 * 1024];
        
        System.gc();
    }
}

当我们在虚拟机上打上-verbose:gc:打印CG回收信息;

 会发现没有回收,为什么?

因为当执行垃圾回收时,byte数组还在作用域范围内,所以说虚拟机思不会回收的;

那么怎么办呢?参考之前的,引入一个比他更大作用域的变量,这样GC就可以将byte数组回收了,因为他的slot直接失效给到了被引入的新的变量;

public class TestDemo {
 
    public static void main(String[] args){
        {
            byte[] placeholder = new byte[64 * 1024 * 1024];
        }
        int a = 0;
        System.gc();
    }
}

 会发现被回收了,关键就在于:局部变量表中Slot是否还有比byte数组的引用


操作数栈

里面的元素可以是任意的Java数据类型,可以理解为栈帧中用于计算的临时数据储存区

public class OperandStack{
 
    public static int add(int a, int b){
        int c = a + b;
        return c;
    }
 
    public static void main(String[] args){
        add(100, 98);
    }
}

使用javap -v OperandStack.class进行反编译:

得到操作数栈的运行流程:

 解析:当add()方法刚开始执行时,操作数栈为null,iload_0:将局部变量0压入栈(也就是100这个数被压入);

然后执行iload_1:局部变量1压栈(98);接着执行iadd,弹出两个变量(100与98会出操作数栈)

——>对其求和,将198压栈(压入操作数栈),然后istore_2将198出栈放于局部变量2....

 栈中可能出现的异常:

StackOverflowError(栈溢出):

当一个线程计算时所需要的栈大小>配置中所允许的最大栈大小:那么就会抛出异常;

OutOfMemoryError(内存不足):

栈进行动态扩展的时候——>(比如说:在运行时动态加载出其他方法,栈帧一加,然后发现内存不够,就会抛出异常);

设置栈参数:

使用-Xss设置栈大小—>因为栈是线程私有的,线程数越多,那么栈空间被占用就会越大;

可以这么说,函数调用的深度就和栈有着直接关系——>因为你每次调用方法都会创建栈帧,当一定次数时,如果大小>JVM运行配置的最大栈参数,就会抛出StackOverflowError;


是JVM所管理的内存中最大的一块储存区域,堆内存被所有线程共享;主要是放使用new关键字来创建的对象;

所有对象的实例以及数组都要在堆上分配,而垃圾收集器其实就是根据GC算法——>收集堆上对象所占用的内存空间;

Java堆分为:年轻代(生产区+幸存区(FromSpace+ToSpace))和老年代

年轻代(YoungGen)存储新创建的对象,当占满后,会被清理;

老年代(OldGen)存储长期存活的对象(经过多次GC还能获得对象就会被放在老年代中进行存储);

当满了后会触发Full GC——>清理整个堆空间(年轻代和老年代都会被清理);如果之后无法存储对象——>OutOfMemoryErrory异常;

堆中常用参数:

方法区

方法区通俗点理解就是虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,

方法区同Java堆一样也是被线程共享的区间—>被JVM加载的Class,常量,静态变量;(即编译器编译后的代码)

常量池是方法区的一部分;

注:JDK1.8后使用的都是MetaSpace元空间来代替方法区,而元空间并不在JVM中,也就是说此时方法区是在操作系统中(本地内存当中)

MetaSpace元空间的几个参数:

1.MetaSpaceSize:初始化元空间大小,控制发生GC闸值

2.MaxMetaspaceSize:限制元空间大小上限,防止异常占用了太多的物理内存

注意:

JDK1.8串池StringTable是在堆中的,而JDK1.6版本串池是在常量池中的;

常量池有什么用?

优点:常量池避免了频繁创建而导致创建和销毁对象从而影响性能,实现了对象的共享;

例子:

Integer常量池:

 众所周知==基本数据类型比较的是数值,而引用数据类型比较的则是内存地址;

public void TestIntegerCache()
{
    public static void main(String[] args)
    {
        
        Integer i1 = new Integer(66);
        Integer i2 = new integer(66);
        Integer i3 = 66;
        Integer i4 = 66;
        Integer i5 = 150;
        Integer i6 = 150;
        System.out.println(i1 == i2);//false
        System.out.println(i3 == i4);//true
        System.out.println(i5 == i6);//false
    }
    
}

1、i1和i2使用的是new关键字,从而在堆中创建一个新的对象,所以第一个为false;

2、第二个涉及装箱操作,Integer i3=66—>(会将int类型的66装箱成Integer,然后使用valueOf()方法):

public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

作用:判断变量是否在IntegerCache中(-128~127)之间,如果在这期间就返回常量池中的内容,否则就new一个Integer对象;

至于IntegerCache,他是个Integer的静态内部类,作用就是将[-128-127]之间的数缓存在cache数组当中,而valueOf()方法目的就是调用常量池中的cache数组,所以说i3和i4变量引用指向的是常量池,没有创建对象,所以为true;

IntegerCache:

private static class IntegerCache {
        static final int low = -128;//最小值
        static final int high;//最大值
        static final Integer cache[];//缓存数组
 
        //私有化构造方法,不让别人创建它。单例模式的思想
        private IntegerCache() {}
 
        //类加载的时候,执行静态代码块。作用是将-128到127之间的数缓冲在cache[]数组中
        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;
 
            cache = new Integer[(high - low) + 1];//初始化cache数组,根据最大最小值确定
            int j = low;
            for(int k = 0; k < cache.length; k++)//遍历将数据放入cache数组中
                cache[k] = new Integer(j++);
 
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }
 
    }

而上面的i5与i6为false,是因为值超出了范围,所以说它们会new一个对象,故此,为false;

拆箱操作:

public static void main(String[] args){
       Integer i1 = new Integer(4);
       Integer i2 = new Integer(6);
       Integer i3 = new Integer(10);
       System.out.print(i3 == i1+i2);//true
    }

i1与i2会进行自动拆箱操作,因为Integer是不能进行+这种运算符的,然后i3也会拆箱因为他要进行比较,所以说实际上它们比较的就是int数据值是否相等(毕竟都拆了);


网站公告

今日签到

点亮在社区的每一天
去签到