目录
那么问题来了,1.GC是否涉及虚拟机栈? 2.栈内存是否分配越大越好?
内存结构:
首先我们来说说运行时数据区:
当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数据值是否相等(毕竟都拆了);