内存结构
当前线程所执行字节码的行号指示器。若当前方法是native的,那么程序计数器的值就是undefined。
线程私有,Java内存区域中唯一一块不会发生OOM或StackOverflow的区域。
就是常说的Java栈,存放栈帧,栈帧里存放局部变量表等信息,方法执行到结束对应着一个栈帧的入栈到出栈。
线程私有,会发生StackOverflow。
与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的。
线程私有,会发生StackOverflow。
Java 虚拟机中内存最大的一块,几乎所有的对象实例都在这里分配内存。
是被所有线程共享的,会发生OOM。
也称非堆,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
是被所有线程共享的,会发生OOM。
是方法区的一部分,存常量(比如static final修饰的,比如String 一个字符串)和符号引用。
是被所有线程共享的,会发生OOM。
前提要求堆内存的绝对工整的。
所有用过的内存放一边,没用过的放另一边,中间放一个分界点的指示器,当有对象新生时就已经知道大小了,指示器只需要像没用过的内存那边移动与对象等大小的内存区域即可。
指针碰撞
假设堆内存并不工整,那么空闲列表最合适。
JVM维护一个列表 ,记录哪些内存块是可用的,当对象创建时从列表中找到一块足够大的空间划分给新生对象,并将这块内存标记为已用内存。
空闲列表
分为三部分:
包含两部分:自身运行时数据和类型指针。
自身运行时数据包含:hashcode、gc分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等
对象指针就是对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例
用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
JVM要求对象起始地址必须是8字节的整数倍(8字节对齐),所以不够8字节就由这部分来补充。
如下两种,具体用哪种有JVM来选择,hotspot虚拟机采取的直接指针方式来定位对象。
栈上的引用直接指向堆中的对象。好处就是速度快。没额外开销。
Java堆中会单独划分出一块内存空间作为句柄池,这么一来栈上的引用存储的就是句柄地址,而不是真实对象地址,而句柄中包含了对象的实例数据等信息。好处就是即使对象在堆中的位置发生移动,栈上的引用也无需变化。因为中间有个句柄。
给对象添加一个引用计数器,每当有一个地方引用他的时候该计数器的值就+1,当引用失效的时候该计数器的值就-1;当计数器的值为0的时候,jvm判定此对象为垃圾对象。存在内存泄漏的bug,比如循环引用的时候,所以jvm虚拟机采取的是可达性分析法。
有一些根节点GC Roots作为对象起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的时候,则证明此对象为垃圾对象。
补充:哪些可作为GC Roots?
堆组成部分
堆大小 = 新生代 + 老年代。如果是Java8则没有Permanent Generation,Java8将此区域换成了Metaspace。
其中新生代(Young) 被分为 Eden和S0(from)和S1(to)。
默认情况下Edem : from : to = 8 : 1 : 1,此比例可以通过 –XX:SurvivorRatio 来设定
方法运行的时候栈的深度超过了虚拟机容许的最大深度的时候,所以不推荐递归的原因之一也是因为这个,效率低,死归的话很容易就StackOverflowError了。
虽然Java会自动GC,但是使用不当的话还是存在内存泄漏的,比如ThreadLocal忘记remove的情况。(ThreadLocal篇幅过长,不适合放到这里,懂者自懂,不懂Google)
栈帧中存放的是局部变量、操作数栈、动态链接、方法出口等信息,栈帧中的局部变量表存放基本类型+对象引用+returnAddress,局部变量所需的内存空间在编译期间就完成分配了,因为基本类型和对象引用等都能确定占用多少slot,在运行期间也是无法改变这个大小的。
方法的执行到结束其实就是栈帧的入栈到出栈的过程,方法的局部变量会存到栈帧中的局部变量表里,递归的话会一直压栈压栈,执行完后进行出栈,所以效率较低,因为一直在压栈,栈是有深度的。
方法区回收价值很低,主要回收废弃的常量和无用的类。
如何判断无用的类:
会占用16个字节。比如
Object obj = new Object();
因为obj引用占用栈的4个字节,new出来的对象占用堆中的8个字节,4+8=12,但是对象要求都是8的倍数,所以对象的字节对齐(Padding)部分会补齐4个字节,也就是占用16个 字节。
再比如:
public class NewObj {
int count;
boolean flag;
Object obj;
}
NewObj obj = new NewObj();
这个对象大小为:空对象8字节+int类型4字节+boolean类型1字节+对象的引用4字节=17字节,需要8的倍数,所以字节对齐需要补充7个字节,也就是这段程序占用24字节。
main函数,也是程序的起始点。
因为基本类型占用的空间一般都是1-8个字节(所需空间很少),而且因为是基本类型,所以不会出现动态增长的情况(长度是固定的),所以存到栈上是比较合适的。反而存到可动态增长的堆上意义不大。
值传递。
基本类型作为参数被传递时肯定是值传递;引用类型作为参数被传递时也是值传递,只不过“值”为对应的引用。假设方法参数是个对象引用,当进入被调用方法的时候,被传递的这个引用的值会被程序解释到堆中的对象,这个时候才对应到真正的对象,若此时进行修改,修改的是引用对应的对象,而不是引用本身,也就是说修改的是堆中的数据,而不是栈中的引用。
因为递归一直在入栈入栈,短时间无法出栈,导致栈的压力会很大,栈也有深度的,容易爆掉,所以效率低下。
因为除了double和long类型占用局部变量表2个slot外,其他类型都占用1个slot大小,如果参数太多的话会导致这个栈帧变大,因为slot大,放个对象的引用上去的话只会占用1个slot,增加堆的压力减少栈的压力,堆自带GC,所以这点压力可以忽略。
问题:输出结果是什么?
答案:aaa、aaa、abc
原因:其实也是值传递还是引用传递的问题。具体核心原因:main函数的str引用和zcd在栈上,而其对应的值在方法区或堆上。test1、test2、test3的参数也在栈上,这个空间和main上的不是同一块,是不同的栈帧。所以你修改test方法的数据对于main函数其实是无感知的。但是对象的引用的话修改的是堆内存中的对象属性值,所以有感知,那为什么test2输出的是aaa而不是abc呢?因为test2把堆中的对象都给换了,重新生成一个全新对象,对main上的引用来讲是看不到的,具体如下三幅图:
(1)aaa
题1
(2)aaa
题2
(3)abc
题3
public class TestChuandi {
public static void main(String[] args) {
String str = "aaa";
test1(str);
// aaa
System.out.println(str);
Zhichuandi zcd = new Zhichuandi();
zcd.setName("aaa");
// aaa
test2(zcd);
System.out.println(zcd.getName());
// abc
test3(zcd);
System.out.println(zcd.getName());
}
private static void test1(String s) {
s = "abc";
}
private static void test2(Zhichuandi zcd) {
zcd = new Zhichuandi();
zcd.setName("abc");
}
private static void test3(Zhichuandi zcd) {
zcd.setName("abc");
}
}
class Zhichuandi {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!
加入交流群
请使用微信扫一扫!