2024年11月

一、概述

上篇文章介绍了木舟如何上传模块热部署,那么此篇文章将介绍如何利用HTTP网络组件接入设备,那么有些人会问木舟又是什么,是什么架构为基础,能做什么呢?

木舟 (Kayak) 是什么?

木舟(Kayak)是基于.NET6.0软件环境下的surging微服务引擎进行开发的, 平台包含了微服务和物联网平台。支持异步和响应式编程开发,功能包含了物模型,设备,产品,网络组件的统一管理和微服务平台下的注册中心,服务路由,模块,中间服务等管理。还有多协议适配(TCP,MQTT,UDP,CoAP,HTTP,Grpc,websocket,rtmp,httpflv,webservice,等),通过灵活多样的配置适配能够接入不同厂家不同协议等设备。并且通过设备告警,消息通知,数据可视化等功能。能够让你能快速建立起微服务物联网平台系统。

那么下面就为大家介绍如何从创建组件、协议、设备网关,设备到设备网关接入,再到设备数据上报,把整个流程通过此篇文章进行阐述。

二、网络组件

1.编辑创建HTTP协议的网络组件,可以选择共享配置和独立配置(独立配置是集群模式),然后可以选择开启swagger和webservice.

开启成功后,可以看看swagger 是否可以访问

又或者是访问一下中间服务,以上篇文章上传的Testapi 模块为例:

三、自定义协议

  • 如何创建自定义协议模块

如果是网络编程开发,必然会涉及到协议报文的编码解码处理,那么对于平台也是做到了灵活处理,首先是协议模块创建,通过以下代码看出协议模块可以添加协议说明md文档, 身份鉴权处理,HTTP路由,消息编解码,元数据配置。下面一一介绍如何进行编写

  public classDemo5ProtocolSupportProvider : ProtocolSupportProvider
{
public override IObservable<ProtocolSupport>Create(ProtocolContext context)
{
      var support = new ComplexProtocolSupport();
    support.Id = "demo5";
    support.Name = "演示协议5";
    support.Description = "演示协议5";
support.AddDocument(MessageTransport.Http,
"Document/document-http.md");
    support.AddAuthenticator(MessageTransport.Http,
newDemo5Authenticator());
     support.AddRoutes(MessageTransport.Http,
new List<BasicMessageCodec>() {
   BasicMessageCodec.DeviceOnline,
   BasicMessageCodec.ReportProperty,
  BasicMessageCodec.WriteProperty,
  BasicMessageCodec.ReadProperty,
BasicMessageCodec.Event
}.Select(p
=>HttpDescriptor.Instance(p.Pattern)
.GroupName(p.Route.GroupName())
.HttpMethod(p.Route.HttpMethod())
.Path(p.Pattern)
.ContentType(MediaType.ToString(MediaType.ApplicationJson))
.Description(p.Route.Description())
.Example(p.Route.Example())
).ToList());
support.AddMessageCodecSupport(MessageTransport.Http, ()
=> Observable.Return(newHttpDeviceMessageCodec()));
support.AddConfigMetadata(MessageTransport.Http, _httpConfig);
returnObservable.Return(support);

}

}

1. 添加协议说明文档如代码:
support.AddDocument(MessageTransport.Http,
"
Document/document-http.md
"
);,文档仅支持
markdown
文件,如下所示

### 使用HTTP推送设备数据

上报属性例子:

POST
/{productId}/{deviceId}/properties/report
Authorization:{产品或者设备中配置的Token}
Content
-Type: application/json

{
"properties":{"temp":11.5}
}

上报事件例子:

POST
/{productId}/{deviceId}/event/{eventId}
Authorization:{产品或者设备中配置的Token}
Content
-Type: application/json

{
"data":{"createtime": ""}
}

2. 添加身份鉴权如代码:
support.AddAuthenticator(MessageTransport.Http, new Demo5Authenticator()) ,自定义身份鉴权
Demo5Authenticator
代码如下:

       public classDemo5Authenticator : IAuthenticator
{
public IObservable<AuthenticationResult>Authenticate(IAuthenticationRequest request, IDeviceOperator deviceOperator)
{
var result = Observable.Return<AuthenticationResult>(default);if (request isDefaultAuthRequest)
{
var authRequest = request asDefaultAuthRequest;
deviceOperator.GetConfig(authRequest.GetTransport()
==MessageTransport.Http?"token": "key").Subscribe( config =>{var password = config.Convert<string>();if(authRequest.Password.Equals(password))
{
result
=result.Publish(AuthenticationResult.Success(authRequest.DeviceId));
}
else{
result
= result.Publish(AuthenticationResult.Failure(StatusCode.CUSTOM_ERROR, "验证失败,密码错误"));
}
});
}
elseresult= Observable.Return<AuthenticationResult>(AuthenticationResult.Failure(StatusCode.CUSTOM_ERROR, "不支持请求参数类型"));returnresult;
}
public IObservable<AuthenticationResult>Authenticate(IAuthenticationRequest request, IDeviceRegistry registry)
{
var result = Observable.Return<AuthenticationResult>(default);var authRequest = request asDefaultAuthRequest;
registry
.GetDevice(authRequest.DeviceId)
.Subscribe(
async p =>{var config= await p.GetConfig(authRequest.GetTransport() == MessageTransport.Http ? "token" : "key");var password= config.Convert<string>();if(authRequest.Password.Equals(password))
{
result
=result.Publish(AuthenticationResult.Success(authRequest.DeviceId));
}
else{
result
= result.Publish(AuthenticationResult.Failure(StatusCode.CUSTOM_ERROR, "验证失败,密码错误"));
}
});
returnresult;
}
}

3. 添加Http路由代码
support.AddRoutes,那么如何配置呢,代码如下:

    public static BasicMessageCodec ReportProperty =>
 new BasicMessageCodec("/*/properties/report", typeof(ReadPropertyMessage), route => route.GroupName("属性上报")
.HttpMethod(
"Post")
.Description(
"上报物模型属性数据")
.Example(
"{\"properties\":{\"属性ID\":\"属性值\"}}"));

4.添加消息编解码代码
support.AddMessageCodecSupport(MessageTransport.Http, () => Observable.Return(
new
HttpDeviceMessageCodec())), 可以自定义编解码,
HttpDeviceMessageCodec
代码如下:

  public classHttpDeviceMessageCodec : DeviceMessageCodec
{
private readonlyMessageTransport _transport;public HttpDeviceMessageCodec() : this(MessageTransport.Http)
{
}
private staticDefaultHttpResponseMessage Unauthorized(String msg)
{
return newDefaultHttpResponseMessage()
.ContentType(MediaType.ApplicationJson)
.Body(
"{\"success\":false,\"code\":\"unauthorized\",\"message\":\"" + msg + "\"}")
.Status(HttpStatus.AuthorizationFailed);
}
private staticDefaultHttpResponseMessage BadRequest()
{
return newDefaultHttpResponseMessage()
.ContentType(MediaType.ApplicationJson)
.Body(
"{\"success\":false,\"code\":\"bad_request\"}")
.Status(HttpStatus.RequestError);
}
publicHttpDeviceMessageCodec(MessageTransport transport)
{
_transport
=transport;
}
public override IObservable<IDeviceMessage>Decode(MessageDecodeContext context)
{
if (context.GetMessage() isHttpRequestMessage)
{
returnDecodeHttpRequestMessage(context);
}
return Observable.Return<IDeviceMessage>(default);
}
public override IObservable<IEncodedMessage>Encode(MessageEncodeContext context)
{
return Observable.Return<IEncodedMessage>(default);
}
private IObservable<IDeviceMessage>DecodeHttpRequestMessage(MessageDecodeContext context)
{
var result = Observable.Return<IDeviceMessage>(default);var message =(HttpExchangeMessage)context.GetMessage();

Header
? header = message.Request.GetHeader("Authorization");if (header == null || header.Value == null || header.Value.Length == 0)
{
message
.Response(Unauthorized(
"Authorization header is required")).ToObservable()
.Subscribe(p
=> result = result.Publish(default));returnresult;
}
var httpToken = header.Value[0];var paths = message.Path.Split("/");if (paths.Length == 0)
{
message.Response(BadRequest()).ToObservable()
.Subscribe(p
=> result = result.Publish(default));returnresult;
}
String deviceId
= paths[1];
context.GetDevice(deviceId).Subscribe(
async deviceOperator =>{var config = deviceOperator==null?null: await deviceOperator.GetConfig("token");var token = config?.Convert<string>();if (token == null || !httpToken.Equals(token))
{
awaitmessage
.Response(Unauthorized(
"Device not registered or authentication failed"));
}
else{var deviceMessage = awaitDecodeBody(message, deviceId);if (deviceMessage != null)
{
await message.Success("{\"success\":true,\"code\":\"success\"}");
result
=result.Publish(deviceMessage);
}
else{awaitmessage.Response(BadRequest());
}
}
});
returnresult;
}
private async Task<IDeviceMessage> DecodeBody(HttpExchangeMessage message,stringdeviceId)
{
byte[] body = new byte[message.Payload.ReadableBytes];
message.Payload.ReadBytes(body);
var deviceMessage = awaitTopicMessageCodec.Dodecode(message.Path, body);
deviceMessage.DeviceId
=deviceId;returndeviceMessage;
}
}

5.添加元数据配置代码
support.AddConfigMetadata(MessageTransport.Http, _httpConfig);
_httpConfig
代码如下

        private readonly DefaultConfigMetadata _httpConfig = newDefaultConfigMetadata("Http认证配置","token为http认证令牌")
.Add(
"token", "token", "http令牌", StringType.Instance);
  • 如何加载协议模块,协议模块包含了协议模块支持添加引用加载和上传热部署加载。

引用加载模块

上传热部署协议模块

四、设备网关

创建设备网关

五、产品管理

以下是添加产品。

设备接入

六、设备管理

添加设备

HTTP认证配置

创建告警阈值

七、测试

利用Postman 进行测试,以调用http://127.0.0.1:168/{productid}/{deviceid}/properties/report 为例,Authorization设置:123456

1.正常数据测试

2. 如果是选用Get方式调用,会因为找不到ServiceRoute而返回错误。

3. 把Authorization改成1111,会返回错误
Device not registered or authentication failed
,从而上报数据失败

以上上传的数据可以在设备信息-》运行状态中查看

告警信息可以在超临界数据中查看

七、总结

以上是基于HTTP网络组件设备接入,现有平台网络组件可以支持TCP,MQTT,UDP,CoAP,HTTP,Grpc,websocket,rtmp,httpflv,webservice,tcpclient, 而设备接入支持TCP,UDP,HTTP网络组件,后面会陆续添加支持所有网络组件接入,后面我也会陆续介绍其它网路组件设备接入 ,  然后定于11月20日发布1.0测试版平台。也请大家到时候关注捧场。

如果你的
Streamlit App
中使用的数据的比较敏感,那么,保护这个
App
及其背后的数据免受未授权访问变得至关重要。

无论是出于商业机密的保护、用户隐私的维护,还是为了满足日益严格的合规要求,确保只有经过验证的用户才能访问特定的数据和功能,已成为大部分应用的一个基本需求。

登录认证
,作为访问控制的基础,是保护
Streamlit App
安全的第一道防线。

通过实施登录认证机制,我们可以确保只有合法的用户才能访问敏感数据、执行关键操作或查看特定页面。

本文将探讨如何在
Streamlit
多页应用中添加登录认证功能。

从为什么需要登录认证,到如何实现这一功能,最后再构建一个示例来演示如何在自己的
Streamlit App
中添加登录认证功能。

1. 为什么需要登录认证

在构建
Streamlit
多页应用时,添加登录认证功能并不是多余的步骤,而是确保应用安全、高效和用户友好的重要一环。

一般来说,我们会在以下一些场景时需要登录认证功能。

1.1. 有数据安全性要求

如果我们的
Streamlit App
所处理的数据包含敏感或机密信息,比如客户数据、财务数据或研究数据。

那么,未授权的访问可能会导致数据泄露,带来不必要的麻烦。

这时,通过登录认证,至少可以确保只有经过验证的用户才能访问这些数据,能有效降低了数据泄露的风险。

另外,许多行业(如金融、医疗、教育)都有严格的数据保护规定,要求对个人信息和敏感数据进行加密存储和访问控制。登录认证是实现这些合规要求的关键组成部分。

1.2. 有用户管理需求

如果你的应用有不同角色的用户(比如分了管理员,编辑者和查看者等等),每种角色有不同的权限。

那么,首先就要实现登录认证的功能,才能进一步将你的
Streamlit App

RBAC(Role Based Access Control)
系统对接,实现基于角色的访问控制。

1.3. 提高用户体验

当用户看到
Streamlit App
采取了登录认证等安全措施时,他们会更加信任该应用,更愿意分享个人信息或使用敏感功能。

此外,登录认证允许
Streamlit App
识别并记住用户,从而提供个性化的体验。

例如,可以根据用户的偏好设置界面主题、保存用户的工作进度等等。

2. 如何实现登录认证

实现一个登录认证功能,主要包含以下4个部分:

  1. 认证方法
    :常见有用户名+密码;邮箱或手机接受验证码;基于第三方的认证(
    OAuth
    /
    OpenID
    )等等
  2. 用户信息数据库
    :一般用关系数据库来保存用户信息,用户信息一般包含用户ID、用户名、密码哈希、角色/权限等字段
  3. 登录页面
    :根据选择的
    认证方法
    ,用
    Streamlit
    实现一个页面来处理用户的输入和登录请求
  4. 后端逻辑
    :根据选择的
    认证方法
    ,后端实现对用户输入信息是否合法的判断

后端逻辑
中,除了判断用户输入信息的合法性之外,有时候为了更高的安全性要求,还会加入一些密码策略(比如要求密码长度、包含特殊字符、定期更换密码等),防暴力破解的机制(比如限制登录频率,登录失败过多锁定账户等),以及其他一些攻击手段的预防。

3. 登录认证示例

最后,通过一个简化的示例演示如何在
Streamlit App
加入登录认证机制。

本示例主要演示
Streamlit
是如何限制未登录认证的用户访问具体功能页面的,不包含数据库和安全性的部分。

首先,构建一个多页应用,先不加登录功能。

工程目录结构如下:

$ tree /A /F .
登录认证
|   app.py
|
\---func_pages
        page1.py
        page2.py
        __init__.py

app.py

import streamlit as st

page1 = st.Page("pages/page1.py", title="查看数据集")
page2 = st.Page("pages/page2.py", title="绘制折线图")

pg = st.navigation({"主要功能": [page1, page2]})
pg.run()

page1.py

page2.py
分别模拟了不同的功能页面。

运行效果如下:

接下来添加登录认证的功能,为了简化,登录的用户名和密码固定写死,登录状态放在
session
中。


app.py
改造如下

# 初始化会话状态
if "logged_in" not in st.session_state:
    st.session_state.logged_in = False


# 默认用户
USERNAME = "admin"
PASSWORD = "adminadmin"


# 登录页面
def login():
    st.header("登录")
    st.divider()

    username = st.text_input("用户名")
    password = st.text_input("密码", type="password")

    if st.button("Login"):
        if username == USERNAME and password == PASSWORD:
            st.session_state.logged_in = True
            st.success("登录成功!")
            time.sleep(0.5)
            st.rerun()
        else:
            st.error("用户名或密码错误")


page1 = st.Page("func_pages/page1.py", title="查看数据集")
page2 = st.Page("func_pages/page2.py", title="绘制折线图")
login_page = st.Page(login, title="登录")

# 默认只有login页面
pg = st.navigation([login_page])

if st.session_state.logged_in:
    pg = st.navigation({"主要功能": [page1, page2]})

pg.run()

通过状态
st.session_state.logged_in
来判断用户是否已经登录,

登录成功后进入【主要功能】页面,否则停留在【登录】页面。

再添加一个退出登录的函数,基于上面的代码修改如下:

def logout():
    if st.button("Logout"):
        st.session_state.logged_in = False
        st.rerun()

logout_page = st.Page(logout, title="退出登录")

if st.session_state.logged_in:
    pg = st.navigation(
        {
            "账户管理": [logout_page],
            "主要功能": [page1, page2],
        }
    )

运行效果如下:

4. 总结

本文只提供了一个基本的登录认证实现示例,实际应用中可能需要根据具体需求进行定制和扩展。

例如,可以考虑添加多因素认证、用户注册和找回密码功能、以及与第三方身份提供者(如
OAuth
)的集成等。

原文链接

代码:快速使用kappa

首先的首先,可以先去了解一下
lambda架构

Abstract

在本文中提出了Kappa,一个简化无服务器开发的框架。它使用
检查点
来处理lambda函数超时,并提供
并发机制
,实现并行计算和协调

1 Introduction

无服务器计算是一种新的云范例,在这种范例中,租户不是配置虚拟机(VM),而是向平台注册事件处理程序(例如 Python 函数)。当事件发生时,平台会在 lambda 函数(一种短暂的无状态执行环境)上调用处理程序。lambda 函数在终止前可以执行一段有界的时间(例如在 AWS 上为 15 分钟)
存在两个主要挑战:(1)程序员必须手动划分计算以适应 lambda 函数的时间限制;(2)程序员没有可用的并发或同步原语(如线程、锁、信号量等),因此必须要么实现这样的原语,要么限制自己使用无共享并行性,要么避免使用为简化开发而开发的并行 lambda 函数

2 Background and Motivation

2.1 Comparison to Existing Framework

2.2 Lambda Function Time Limit

无服务器计算中的Lambda函数存在时间限制的原因主要是为了优化运营商的任务分配和资源管理,运营商不再需要预测任务的完成时间或进行复杂的迁移操作,能够更灵活地进行任务分配和资源利用

3 Kappa Design

Kappa 有三个组成部分:(1)一个协调器,负责启动和恢复任务以及实现 Kappa 的并发原语;(2)一个编译器,负责生成检查点所需的代码;(3)一个库,供任务用于检查点、并发处理和同步

3.1 Coordinator

Kappa 协调器负责在 lambda 函数上安排任务、实现同步和跨任务通信、跟踪任务元数据(包括检查点)以及提供容错功能,协调器本身通过跨备份存储(例如,Redis 集群)复制其状态来支持容错,通过管理远程过程调用 (RPC) 来实现这一点,其中包括生成新任务、检查点、消息排队和检索任务结果等操作。

3.2 Checkpointing

Kappa使用检查点来容忍lambda函数超时并防止RPC重复,在任务执行的某些关键点创建“检查点”,将任务当前的运行状态(包括变量、控制流等)记录下来。使用了一种称为“continuations(延续)”的技术来创建检查点,延续是一种保存程序控制流的方式,这种方式无需依赖传统服务器持久化状态,而是通过序列化将任务的状态数据保存到外部存储(如Redis或S3)中,提供同步和异步两种检查点模式。同步检查点在保存状态时会短暂停止任务执行,而异步检查点则允许任务继续执行,检查点数据在后台保存,检查点数据分布存储在多个节点上,并支持多任务同时创建检查点


(b) 显示了由编译器生成的延续函数,用来保存检查点后的执行流程,(c) 使用异常处理机制在暂停点恢复,确保任务可以从中断处继续执行

3.3 Concurrency API

两种基本并发机制:任务启动和任务同步

任务启动 (spawn) 并行机制:spawn RPC 用于启动一个新任务,以并行的方式执行某个函数调用(如f(args)),并返回一个Future对象,用于跟踪任务的结果。工作机制是通过创建一个初始检查点。当系统恢复这个检查点时,会执行相应的函数调用。此时,协调器(Coordinator)会调用一个新的lambda函数,从该检查点恢复并执行任务。

先进先出(FIFO)队列机制:如果一个任务尝试向已满的队列入队或从空队列出队,任务将被阻塞。这个机制不仅可以用来实现任务间通信,还可以作为锁和信号量,控制资源的并发访问。

4 Implementation

编译器是用Python编写的,通过Python的pickle库进行状态序列化,每个任务由一个Go协程(goroutine)管理,任务间的同步使用Go的通道(channel)实现,通过锁机制和Redis事务来确保状态更新的原子性

5 Evaluation

检查点开销测试:通过让Lambda函数每100ms创建一次检查点,测量同步和异步检查点的延迟。同步检查点会暂停应用处理直到检查点数据持久化;异步检查点在后台完成持久化操作,允许前台的计算继续

并发操作性能测试:使用多生产者多消费者FIFO队列的任务间消息传递,以评估任务通信的延迟

端到端应用评估:测试包括五个Kappa应用场景:TPC-DS SQL查询、字数统计(Word Count)、并行Grep(Parallel Grep)、流处理(Streaming),以及网络爬虫(Web Crawler)

6 Limitations

Kappa编译器尚未完全支持Python的一些特性,包括try/except、yield、async/await、嵌套函数定义和全局变量,只能在由其编译器转换的代码中进行检查点操作,垃圾回收机制,缺乏静态检查可序列化。

上一篇我们介绍了如何在本地部署 ollama 运行 llama3 大模型。过程是相当简单的。但是现在给大模型交流只能在命令行窗口进行。这样的话就只能你自己玩了。独乐乐不如众乐乐嘛。我们接下来说一下如何部署 open-webui 给 ollama 加一个 webui,这样用户就可以通过浏览器访问我们的本地大模型了,体验非常类似 chatGPT。

Open-WebUI

Open-WebUI 是一个开源的用户界面框架,旨在提供简便的工具和接口,帮助用户轻松地访问和管理各种深度学习模型,尤其是大规模预训练语言模型。以下是对Open-WebUI的简要介绍:

  • 开源框架: Open-WebUI 是一个开源项目,提供了灵活且可定制的用户界面,用于与各种深度学习模型进行交互。

  • 模型管理: 通过 Open-WebUI,用户可以方便地加载、配置和管理多个深度学习模型,包括 GPT-4、BERT 等大规模预训练模型。

  • 用户友好: 它提供了直观的界面,简化了模型使用过程,使非技术用户也能轻松上手进行自然语言处理任务。

  • 集成支持: Open-WebUI 支持与多种后端深度学习框架(如 TensorFlow、PyTorch)集成,提供高效的推理和训练功能。

扩展性强: 用户可以根据需求自定义和扩展界面功能,以适应不同的应用场景和任务需求。
总之,Open-WebUI 为用户提供了一个高效、直观的界面,使得大规模深度学习模型的使用更加便捷和高效。

地址:
https://github.com/open-webui/open-webui

使用 Docker 部署

使用 Docker 部署非常简单。如果 ollama 跟 open-webui 部署在同一个机器上,那么只需要运行一下代码就可以。

docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main

如果 ollama 部署在其他服务器就用如下命令:

docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main

OLLAMA_BASE_URL 是指 ollama 暴露的API地址,一般为服务器地址加 11434。如:OLLAMA_BASE_URL=http://192.168.0.111:11434

使用 Open-WebUI

部署完之后,我们在浏览器里打开
http://localhost:3000
,就会出现 Open-WebUI 的界面。看起来跟 chatGPT 不能说一模一样么,也是毫无区别。随便填写一个邮箱后就可以注册第一个账户。

在右上角可以选择已经存在的模型。也可以搜索其他模型,然后直接安装,这个就非常方便了。

让我们下载一个传说中巨牛比的国产大模型 Qwen2 试试。随便问个问题,好像还不错。

总结

这一篇内容比较短,就是演示了一下如何使用 Open-WebUI 项目搭建一个本地的 chat 服务。这样就可以把本地大模型共享出去。这样你全家人都可以访问你部署的大模型了。当然你要是部署到外网的服务器上那就是给全世界人用了。

当然本地大模型所能回答的问题都是公开领域的知识,比如你问它你们家有几口人肯定是不知道的。下次我们会将如何让大模型学习你的私有知识,也就是搭建一个本地的知识库。

关注我的公众号一起玩转技术

如何判断一个引用是否存活

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。

优点:可即刻回收垃圾,当对象计数为0时,会立刻回收;

弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。JVM不用这种算法

可达性分析算法

通过 GC Root 对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到 GC Root没有任何的引用链相连时,说明这个对象是不可用的。

  • JVM中的垃圾回收器通过可达性分析来探索所有存活的对象

  • 扫描堆中的对象,看能否沿着GC Root对象为起点的引用链找到该对象,如果找不到,则表示可以回收

GC Root的对象有哪些?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,例如各个线程被调用的方法栈用到的参数、局部变量或者临时变量等。

  • 方法区中类静态属性引用的对象或者说Java类中的引用类型的静态变量。

  • 方法区中常量引用的对象或者运行时常量池中的引用类型变量。

  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

  • JVM内部的内存数据结构的一些引用、同步的监控对象(被修饰同步锁)。

方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载。

在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。

类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:

  • 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。

  • 加载该类的 ClassLoader 已经被回收。

  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。

finalize()

finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。(Java 9中已弃用)

当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。

引用类型

四个引用的特点:

  • 强引用:gc时不会回收

  • 软引用:只有在内存不够用时,gc才会回收

  • 弱引用:只要gc就会回收

  • 虚引用:是否回收都找不到引用的对象,仅用于管理直接内存

强引用

平时常见的

Object object = new Object();

只要一个对象有强引用,垃圾回收器就不会进行回收。即便内存不够了,抛出OutOfMemoryError异常也不会回收。因此强引用是造成java内存泄漏的主要原因之一。 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相 应(强)引用赋值为 null,就是可以被垃圾收集的了,具体回收时机还是要看垃圾收集策略。

/**
 * 一个对象
 * 重写finalize方法,可以知道已经被回收的状态
 */
public class OneObject {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("啊哦~OneObject被回收了");
    }
}

/**
 * 强引用例子
 */
public class ShowStrongReference {
    public static void main(String[] args) {
        // 直接new一个对象,就是强引用
        OneObject oneObject = new OneObject();
        System.out.println("输出对象地址:" + oneObject);
        System.gc();
        System.out.println("第一次gc后输出对象地址:" + oneObject);
        oneObject = null;
        System.gc();
        System.out.println("置为null后gc输出对象地址:" + oneObject);
    }
}

//输出:
输出对象地址:com.esparks.pandora.learning.references.OneObject@72ea2f77
第一次gc后输出对象地址:com.esparks.pandora.learning.references.OneObject@72ea2f77
置为null后gc输出对象地址:null
啊哦~OneObject被回收了

软引用

特点:软引用通过java.lang.SoftReference类实现。只有在内存不够用时,gc才会回收

软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null;否则该方法返回队列中前面的一个Reference对象。

SoftReference<OneObject> oneObjectSr = new SoftReference<>(new OneObject());

当内存足够的时候,垃圾回收器不会进行回收。当内存不够时,就会回收只存在软引用的对象释放内存。

常用于本地缓存处理。

/**
 * 软引用
 * 内存不够了就会回收
 * 注意,运行时需要保证heap大小为35m,即小于实验中全部对象的大小,才能触发gc
 * -Xmx35m
 *
 */
public class ShowSoftReference {
    public static void main(String[] args) {
        // 我们需要通过SoftReference来创建软引用
        SoftReference<OneObject> oneObjectSr = new SoftReference<>(new OneObject());
        // 我们这里创建一个大小为20m的数组
        SoftReference<byte[]> arraySr = new SoftReference<>(new byte[1024 * 1024 * 20]);
        System.out.println("软引用对象oneObjectSr的地址:" + oneObjectSr);
        System.out.println("通过oneObjectSr关联的oneObject对象的地址:" + oneObjectSr.get());
        System.out.println("数组的地址:" + arraySr);
        System.out.println("通过arraySr关联的byte数组的地址:" + arraySr.get());
        System.gc();
        System.out.println("正常gc一次之后,oneObject对象并没有回收。地址" + oneObjectSr.get());

        // 再创建另一个大小为20m的数组,这样heap就不够大了,从而系统自动gc。如果依旧不够,会把已有的软引用关联的对象都回收掉。
        System.out.println("创建另一个大小为20m的数组otherArray");
        byte[] otherArray = new byte[1024 * 1024 * 20];
        System.out.println("otherArray的地址:" + otherArray);

        // gc后,软引用对象还在,但是通过软引用对象创建的对象就被回收了
        System.out.println("现在软引用对象oneObjectSr的地址:" + oneObjectSr);
        System.out.println("通过oneObjectSr关联的oneObject对象的地址:" + oneObjectSr.get());
        System.out.println("现在数组的地址:" + arraySr);
        System.out.println("现在arraySr中关联的byte数组的地址:" + arraySr.get());
    }
}

执行代码,可以看到以下输出:

软引用对象oneObjectSr的地址:java.lang.ref.SoftReference@4f8e5cde
通过oneObjectSr关联的oneObject对象的地址:test.niuke.Test1$OneObject@504bae78
数组的地址:java.lang.ref.SoftReference@3b764bce
通过arraySr关联的byte数组的地址:[B@759ebb3d
正常gc一次之后,oneObject对象并没有回收。地址test.niuke.Test1$OneObject@504bae78
创建另一个大小为20m的数组otherArray
otherArray的地址:[B@484b61fc
现在软引用对象oneObjectSr的地址:java.lang.ref.SoftReference@4f8e5cde
通过oneObjectSr关联的oneObject对象的地址:null
现在数组的地址:java.lang.ref.SoftReference@3b764bce
现在arraySr中关联的byte数组的地址:null

弱引用

特点:弱引用通过WeakReference类实现。只要gc就会回收

弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

WeakReference<OneObject> oneObjectWr = new WeakReference<>(new OneObject());

只要发生gc,就会回收只存在弱引用的对象。

常用于Threadlocal。

/**
 * 弱引用
 * 只要gc就会回收
 */
public class ShowWeakReference {
    public static void main(String[] args) {
        // 我们需要通过WeakReference来创建弱引用
        WeakReference<OneObject> objectWr = new WeakReference<>(new OneObject());
        System.out.println("弱引用objectWr的地址:" + objectWr);
        System.out.println("弱引用objectWr关联的oneObject对象的地址:" + objectWr.get());

        System.gc();

        // gc后,弱引用对象还在,但是通过弱引用对象创建的对象就被回收了
        System.out.println("gc后,弱引用objectWr的地址:" + objectWr);
        System.out.println("gc后,弱引用objectWr关联的oneObject对象的地址:" + objectWr.get());
    }
}

执行代码,可以看到以下输出:

弱引用objectWr的地址:java.lang.ref.WeakReference@72ea2f77
弱引用objectWr关联的oneObject对象的地址:com.esparks.pandora.learning.references.OneObject@33c7353a
gc后,弱引用objectWr的地址:java.lang.ref.WeakReference@72ea2f77
gc后,弱引用objectWr关联的oneObject对象的地址:null
啊哦~OneObject被回收了

虚引用

特点:虚引用也叫幻象引用,通过PhantomReference类来实现。是否回收都找不到引用的对象,仅用于管理直接内存

无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。

private ReferenceQueue<OneObject> queue = new ReferenceQueue<>();
PhantomReference<OneObject> oneObjectPr = new PhantomReference<>(new OneObject(), queue);

无论是否gc,其实都获取不到通过PhantomReference创建的对象。

其仅用于管理直接内存,起到通知的作用。

这里补充一下背景。因为垃圾回收器只能管理JVM内部的内存,无法直接管理系统内存的。对于一些存放在系统内存中的数据,JVM会创建一个引用(类似于指针)指向这部分内存。

当这个引用在回收的时候,就需要通过虚引用来管理指向的系统内存。这里还需要依赖一个队列来实现。当触发gc对一个虚引用对象回收时,会将虚引用放入创建时指定的ReferenceQueue中。之后单独对这个队列进行轮询,并做额外处理。

/**
 * 虚引用
 * 只用于管理直接内存,起到通知的作用
 */
public class ShowPhantomReference {
    /**
     * 虚引用需要的队列
     */
    private static final ReferenceQueue<OneObject> QUEUE = new ReferenceQueue<>();

    public static void main(String[] args) {
        // 我们需要通过 PhantomReference来创建虚引用
        PhantomReference<OneObject> objectPr = new PhantomReference<>(new OneObject(), QUEUE);
        System.out.println("虚引用objectPr的地址:" + objectPr);
        System.out.println("虚引用objectPr关联的oneObject对象的地址:" + objectPr.get());

        // 触发gc,然后检查队列中是否有虚引用
        while (true) {
            System.gc();
            Reference<? extends OneObject> poll = QUEUE.poll();
            if (poll != null) {
                System.out.println("队列里找到objectPr啦" + poll);
                break;
            }
        }
    }
}

输出:

虚引用objectPr的地址:java.lang.ref.PhantomReference@72ea2f77
虚引用objectPr关联的oneObject对象的地址:null
队列里找到objectPr啦null
队列里找到objectPr啦java.lang.ref.PhantomReference@72ea2f77

终结器引用

所有的类都继承自Object类,Object类有一个finalize方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize方法。调用以后,该对象就可以被垃圾回收了如上图,B对象不再引用A4对象。这时终结器对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的finalize方法。调用以后,该对象就可以被垃圾回收了

引用队列

软引用和弱引用可以配合引用队列

在弱引用和虚引用所引用的对象被回收以后,会将这些引用放入引用队列中,方便一起回收这些软/弱引用对象

虚引用和终结器引用必须配合引用队列

虚引用和终结器引用在使用时会关联一个引用队列

四种引用的应用场景

  • 强引用:普通用法

  • 软引用:缓存,软引用可以用于缓存非必须的数据

  • 弱引用:防止一些关于map的内存泄漏。Threadlocal中防内存泄漏;线程池,当一个线程不再使用时,垃圾回收器会回收其所占用的内存空间,以便释放资源。

  • 虚引用:用来管理直接内存

垃圾回收简介

Minor GC、Major GC、Full GC

JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。

针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:部分收集(Partial GC),整堆收集(Full GC)

  • 部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:


    • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集

    • 老年代收集(Major GC/Old GC):只是老年代的垃圾收集


      • 目前,只有 CMS GC 会有单独收集老年代的行为

      • 很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收

    • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集


      • 目前只有 G1 GC 会有这种行为
  • 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾

对象在堆中的生命周期

  1. 在 JVM 内存模型的堆中,堆被划分为新生代和老年代


    • 新生代又被进一步划分为
      Eden区Survivor区From SurvivorTo Survivor
  2. 当创建一个对象时,对象会被优先分配到新生代的 Eden 区


    • 此时 JVM 会给对象定义一个
      对象年轻计数器
      -XX:MaxTenuringThreshold
  3. 当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC)


    • JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1

    • 对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1

  4. 如果分配的对象超过了 -XX:PetenureSizeThreshold
    直接被分配到老年代

内存分配策略

  • 对象优先在 Eden 分配:
    大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,触发 Minor GC

  • 大对象直接进入老年代:
    当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代,最典型的大对象有长字符串和大数组。可以设置JVM参数 -XX:PretenureSizeThreshold ,大于此值的对象直接在老年代分配。

  • 长期存活的对象进入老年代:
    通过参数 -XX:MaxTenuringThreshold 可以设置对象进入老年代的年龄阈值。对象在 Survivor 区每经过一次 Minor GC ,年龄就增加 1 岁,当它的年龄增加到一定程度,就会被晋升到老年代中。

  • 动态对象年龄判定:
    并非对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需达到 MaxTenuringThreshold 年龄阈值。

  • 空间分配担保:
    在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 是安全的。如果不成立的话虚拟机会查看HandlePromotionFailure 的值是否允许担保失败。如果允许,那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次Minor GC是有风险的;(也就是说,会把原先新生代的对象挪到老年代中) ;如果小于,或者 HandlePromotionFailure 的值为不允许担保失败,那么就要进行一次 Full GC 。

下面解释一下空间分配担保时的 “冒险”是冒了什么风险?

新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。但前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:•

  • 用 System.gc():
    只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

  • 老年代空间不足:
    老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组、注意编码规范避免内存泄露。除此之外,可以通过 -Xmn 参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

  • 空间分配担保失败:
    当程序创建一个大对象时,Eden区域放不下大对象,使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。

  • JDK 1.7 及以前的永久代空间不足:
    在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError 。(
    JDK 8以后元空间不足

  • Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

Java对象内存分配过程

对象的分配过程:

  1. 编译器通过逃逸分析优化手段,确定对象是否在栈上分配还是堆上分配。
  2. 如果在堆上分配,则确定是否大对象,如果是则直接进入老年代空间分配, 不然则走3。
  3. 对比tlab, 如果tlab_top + size <= tlab_end, 则在tlab上直接分配,并且增加tlab_top值,如果tlab不足以空间放当前对象,则重新申请一个tlab尝试放入当前对象,如果还是不行则往下走4。
  4. 分配在Eden空间,当eden空间不足时发生YGC, 幸存者区是否年龄晋升、动态年龄、老年代剩余空间不足发生Full GC 。
  5. 当YGC之后仍然不足当前对象放入,则直接分配老年代。

TLAB
作用原理
:Java在内存新生代Eden区域开辟了一小块线程私有区域,这块区域为TLAB,默认占Eden区域大小的1%, 作用于小对象,因为小对象用完即丢,不存在线程共享,快速消亡GC,JVM优先将小对象分配在TLAB是线程私有的,所以没有锁的开销,效率高,每次只需要线程在自己的缓冲区分配即可,不需要进行锁同步堆 。

对象除了基本类型的不一定是在堆内存分配,在JVM拥有逃逸分析,能够分析出一个新的对象所拥有的范围,从而决定是否要将这个对象分配到堆上,是JVM的默认行为;Java 逃逸分析是一种优化技术,可以通过分析 Java 对象的作用域和生命周期,确定对象的内存分配位置和生命周期,从而减少不必要的内存分配和垃圾回收。可以在栈上分配,可以在栈帧上创建和销毁,分离对象或标量替换,同步消除。

垃圾回收算法

标记清除算法

定义:
标记清除算法顾名思义,将存活的对象进行标记,然后清理掉未被标记的对象,给堆内存腾出相应的空间

  • 这里的腾出内存空间并不是将内存空间的字节清0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存

优点:

  • 实现简单,与其他算法组合也简单

  • 与保守式GC算法兼容,因为他们都不需要移动对象。

缺点:

  • 碎片化。简单来说就是随着分配和回收的进行会产生很多小的空闲对象散落在堆中,彼此也不连续。碎片化带来的问题是无法分配大的空闲空间,尽管总的空闲空间是够用的。碎片化带来的另一个问题是局部性原理失效,因为具有引用关系的数据分配到的空闲空间并不连续。

  • 分配速度慢。因为空闲链表是单链表结构,分配时需要遍历链表,时间复杂度是O(n)。

  • 与写时复制不兼容。因为标记阶段会修改堆内对象,导致大量拷贝。

标记整理

GC标记压缩算法分为
标记阶段

压缩阶段
。它是将GC标记清除算法的清除阶段换成了压缩,而且这里的压缩不是将活动对象从一个空间复制到另一个空间,而是将活动对象整体前移,挤占非活动对象的空间。

优点:

  • 堆的利用率高

  • 分配速度快

  • 不会产生碎片化

GC标记压缩算法缺点是吞吐量低。因为在压缩阶段我们需要遍历堆3次,耗费时间与堆大小成正比,堆越大,耗费时间越久。

复制算法

GC复制算法的思路是将堆一分为二,暂时叫它们A堆和B堆。申请内存时,统一在A堆分配,当A堆用完了,将A堆中的活动对象全部复制到B堆,然后A堆的对象就可以全部回收了。这时不需要将B堆的对象又搬回A堆,只需要将A和B互换以下就行了,这样原来的A堆变成B堆,原来的B堆变成了A堆。经过这一轮复制,活动对象搬了新家,垃圾也被回收了。

GC复制算法就是在两个堆之间来回倒腾。JVM中的新生区就是使用的这种方式,总结为:在幸存区中,谁空谁是to

由于对象地址发生了变化,GC复制算法在复制过程中还需要重写指针。从复制的角度来看,活动对象是从A堆复制到B堆。因此我们也将A堆称为From空间,将B堆称为To空间。经过复制,原本散落在From空间中的活动对象被集中放到了To空间开头的连续空间内,这一过程也叫做压缩。

现在的虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。

HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象

优点:

  • 吞吐量优秀。这得益于GC复制算法只会搜索复制活动对象,能在较短时间内完成GC,而且时间与堆的大小无关,只与活动对象数成正比。相比于需要搜索整个堆的GC标记清除算法,GC复制算法吞吐量更高,而且堆越大,差距越明显。

  • 分配速度快。因为不需要搜索空闲链表,在O(1)的时间复杂度就能完成分配。

  • 不会发生碎片化。因为每次复制都会执行压缩。

  • 与缓存兼容。因为复制过程中使用了深度优先遍历,具有引用关系的对象会被复制到相邻的位置,局部性原理可以很好发挥作用。

缺点:

  • 堆的使用效率低。这是一个最显眼的问题,因为要留一半的空间用来复制,所以堆的利用率总小于50%。

  • 不兼容保守式GC。因为GC复制算法需要移动对象。

  • 复制时存在递归调用,需要消耗栈空间,并可能导致栈溢出。

分代回收

根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般将堆分为新生代和老年代。

  • 新生代使用: 复制算法

  • 老年代使用: 标记 - 清除 或者 标记 - 整理 算法

开始新创建的对象都被放在了新生代的伊甸园中

当多创建几个对象的后发现伊甸园装不下了。

当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做轻GC Minor GC。一次小的垃圾回收,根据可达性算法找到不能被回收的,把这些不能被回收的对象复制到幸存区To中(用的复制算法),然后把幸存的对象的寿命+1,然后回收掉伊甸园里面的全部对象。

再把From区和To区的指向互换,那么这是to区就又空出来了

再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),

这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,From也会被垃圾回收检查,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1。

这里1是从伊甸园区新进幸存区的对象,2是原本就存活在幸存区寿命+1后为2

这时就有对象在两次GC中存活下来那么他的存活次数就会是2,如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),那么就会晋升到老年代中去

因为老年代的垃圾回收频率比较低,这个对象在新生代里面反复GC都没有回收掉说明长时间在用,那么就没有必要在新生代中反复GC

如果新生代老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收。如果两次GC后还是放不下, 就会报OOM异常

在报堆内存溢出之前,还会去尝试minorGC一次如果minorGC了释放出来的空间还是放不下,那么就会触发一次fullGC(类似于大扫除,老年代和新生代都会被GC),如果还是没办法放下那么就会报java.lang.OutOfMemoryError: Java heap space

总结:
在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,使用复制算法比较合适,只需要付出少量存活对象的复制成本就可以完成收集。老年代对象存活率高,适合使用标记-清理或者标记-整理算法进行垃圾回收。

并发标记算法(三色标记法)

CMS和G1在并发标记时使用的是同一个算法:三色标记法,使用白灰黑三种颜色标记对象。白色是未标记;灰色自身被标记,引用的对象未标记;黑色自身与引用对象都已标记。

GC 开始前所有对象都是白色,GC 一开始所有根能够直达的对象被压到栈中,待搜索,此时颜色是灰色。然后灰色对象依次从栈中取出搜索子对象,子对象也会被涂为灰色,入栈。当其所有的子对象都涂为灰色之后该对象被涂为黑色。当 GC 结束之后灰色对象将全部没了,剩下黑色的为存活对象,白色的为垃圾。

面试题专栏

Java面试题专栏
已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。