深入解析Java内存区域:从程序计数器到本地方法栈的全面指南
一、Java内存区域 1、Java内存结构

内存结构
当前线程执行的字节码的行号指示器。如果当前方法是本机方法,则程序计数器的值未定义。
线程私有,Java内存区域中唯一不会发生OOM或StackOverflow的区域。
它通常被称为Java堆栈,它存储堆栈帧。局部变量表和其他信息存储在堆栈帧中。方法的执行到最后对应于栈帧的入栈到出栈。
线程私有,就会发生StackOverflow。
虚拟机栈的功能是一样的,只不过虚拟机栈是为Java方法服务的,而本地方法栈是为虚拟机调用Native方法服务的。
线程私有,就会发生StackOverflow。
Java虚拟机中最大的一块内存,几乎所有的对象实例都在这里分配内存。
它被所有线程共享,会发生OOM。
也称为非堆,用于存储虚拟机已加载的类信息、常量、静态变量和即时编译的代码等数据。
它被所有线程共享,会发生OOM。
它是方法区的一部分,存储常量(例如静态最终修饰,例如 String 字符串)和符号引用。
它被所有线程共享,会发生OOM。
2、创建对象时的堆内存分配算法
前提是堆内存必须绝对整齐。
所有已使用的内存放置在一侧,未使用的内存放置在另一侧,并且划分点的指示器放置在中间。当一个物体诞生时,它的大小就已经知道了。只需将指示器移至未使用的内存一侧即可。与对象大小相同的内存区域就足够了。
指针碰撞
如果堆内存不整齐,那么空闲列表是最合适的。
JVM 维护一个列表来记录哪些内存块可用。当一个对象被创建时,它会在链表中找到足够大的空间来分配给新对象,并将这块内存标记为已用内存。
空闲列表
3、对象在内存中的存储布局
分为三个部分:
包含两部分:它自己的运行时数据和类型指针。
自身的运行时数据包括:hashcode、gc分代年龄、锁状态标识符、线程持有的锁、偏向线程ID、偏向时间戳等。
对象指针是对象指向其类元数据的指针。虚拟机使用该指针来确定该对象是哪个类的实例。
用于存储对象的真实有效信息(包括从父类继承的和自定义的)
JVM要求对象的起始地址必须是8字节的整数倍(8字节对齐),所以如果不够的话就会用这部分来补充。
4. 如何定位物体
有以下两种类型。使用哪一种取决于 JVM。热点虚拟机采用直接指针的方式来定位对象。
堆栈上的引用直接指向堆中的对象。优点是速度快。没有额外费用。
Java堆会分离出一块独立的内存空间作为句柄池。这样,栈上的引用存储的是句柄地址而不是真正的对象地址,句柄中包含了对象的实例数据等信息。这样做的好处是,即使对象在堆中的位置发生了移动,堆栈上的引用也不需要改变。因为中间有一个把手。
5.判断对象是否可以回收的算法
向对象添加引用计数器。每当有对它的引用时,计数器的值就+1。当引用过期时,计数器的值为-1;当计数器的值为0时,jvm确定该对象。是一个垃圾对象。存在内存泄漏bug,比如循环引用,因此JVM虚拟机采用可达性分析方法。
有一些根节点GC Roots作为对象的起点。从这些节点开始向下搜索。搜索所经过的路径称为参考链。当一个对象没有任何引用链连接到GC Roots时,就证明该对象是垃圾。目的。
补充:哪些可以作为GC Root?
6. 如何判断一个对象是否可以被回收 7. Java堆内存的组成
堆组件
堆大小=年轻代+老年代。如果是Java8,就没有Permanent Generation。 Java8 用 Metaspace 取代了这个区域。
新生代(Young)分为Eden、S0(from)和S1(to)。
默认情况下 Edem : from : to = 8 : 1 : 1,这个比例可以通过 –XX:SurvivorRatio 设置
8. 什么时候抛出StackOverflowError?
当方法运行时栈深度超过虚拟机允许的最大深度时,不推荐递归的原因之一也是因为如此。效率低下,如果返回死了很容易导致StackOverflowError。
9. Java中是否存在内存泄漏?请简要描述。
虽然Java会自动GC,但是如果使用不当,仍然可能会发生内存泄漏,比如ThreadLocal忘记remove时。 (ThreadLocal太长,不适合放在这里,懂的人都懂,不懂的谷歌)
10.什么是栈帧?包含什么
栈帧存储局部变量、操作数栈、动态链接、方法出口等信息。栈帧中的局部变量表存储的是基本类型+对象引用+返回地址。局部变量所需的内存空间是在编译期间分配的。因为基本类型和对象引用可以决定占用多少个槽,并且这个大小在运行时无法更改。
11.简述一个方法的执行过程
方法的执行到最后其实就是从栈帧到栈的过程。方法的局部变量将存储在栈帧的局部变量表中。如果是递归的,总是会被压入栈中。执行后会弹出。所以效率低,因为总是压栈,而且栈很深。
12、方法区会被回收吗?
方法区的回收价值很低,主要是回收废弃的常量和无用的类。
如何识别无用的类:
13. 一个对象包含多少个字节?
将占用16个字节。例如
对象 obj = 新对象();
因为obj引用在栈上占用4个字节,新对象在堆上占用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的倍数,所以byte需要加上7个字节结盟。也就是说这个程序占用了24个字节。
14.为什么栈被分成两部分? 15、栈的起点在哪里?
main函数也是程序的起点。
16.为什么基本类型不放在堆中?
因为基本类型占用的空间一般为1-8个字节(需要的空间很少),而且由于是基本类型,不会有动态增长(长度是固定的),所以存放在栈上比较多合适的。相反,将其存储在动态增长的堆上没有什么意义。
17. Java参数是按值传递还是按引用传递?
按值传递。
当基本类型作为参数传递时,必须按值传递;当引用类型作为参数传递时,它也是按值传递,但“值”是相应的引用。假设方法参数是对象引用。当进入被调用的方法时,传递的引用的值将被程序解释为堆中的对象。只有这样,它才与真实的物体相对应。如果此时修改,修改的是Reference对应的对象,而不是引用本身,也就是说修改的是堆中的数据,而不是栈中的引用。
18. 为什么不推荐递归?
因为递归总是压入栈,短时间内无法弹出,所以栈的压力会很大。栈也深,容易爆炸,效率低。
19. 为什么大于2的参数要放在对象中?
因为除了double和long类型在局部变量表中占用2个槽位外,其他类型都占用1个槽位大小。如果参数太多,栈帧就会变大。因为槽很大,放一个对象的引用只会占用1个槽,增加了堆的压力,减少了栈的压力。堆有自己的GC,所以这个压力可以忽略不计。
20. 常见笔试题
问题:输出是什么?
答案:aaa、aaa、abc
原因:其实也是一个按值传递还是按引用传递的问题。具体核心原因:main函数的str引用和zcd都在栈上,其对应的值在方法区或者堆中。 test1、test2、test3的参数也在栈中。该空间与 main 上的空间不同,而是不同的堆栈框架。所以你在测试方法中修改的数据实际上对于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;
}
}